《Windows内存管理及C++语言内存分配》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
Windows内存管理及C++语言内存分配本文背景:在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用;根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。本文目的:对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。本文内容:本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管理及C++内存分配实例(二)(三)(四)(五)和(六)。1.进程地址空间1.1地址空间•32164位的系统ICPU操作系统运行在硬件CPU上,32位操作系统运行于32位CPU上,64位操作系统运行于64位CPU上;目前没有真正的64位CPU。32位CPU一次只能操作32位二进制数;位数多CPU设计越复杂,软件设计越简单。软件的进程运行于32位系统上,其寻址位也是32位,能表示的空间是232=4G,范围从0x0000000〇〜OxFFFFFFFF。•NULL指针分区范围:0x00000000-0x0000FFFF作用:保护内存非法访问例子:分配内存时,如果由于某种原因分配不成功,则返回空指针0x00000000;当用户继续使用比如改写数据时,系统将因为发生访问违规而退出。那么,为什么需要那么大的区域呢,“个地址值不就行了吗?我在想,是不是因为不让8或16位的程序运行于32位的系统上呢?!因为NULL分区刚好范围是16的进程空间。•独享用户分区范围:0x00010000~0x7FFEFFFF作用:进程只能读取或访问这个范围的虚拟地址;超越这个范围的行为都会产生违规退出。
1例子:程序的二进制代码中所用的地址大部分将在这个范围,所有exe和dll文件都加载到这个。每个进程将近2G的空间是独享的。注意:如果在boot.ini上设置了/3G,这个区域的范围从2G扩大为3G:0x00010000~0xBFFEFFFF。•共享内核分区范围:0x80000000~0xFFFFFFFF作用:这个空间是供操作系统内核代码、设备驱动程序、设备1/O高速缓存、非页面内存池的分配、进程目表和页表等。例子:这段地址各进程是可以共享的。注意:如果在boot.ini上设置了/3G,这个区域的范围从2G缩小为1G:OxCOOO0000~0xFFFFFFFF〇通过以上分析,可以知道,如果系统有n个进程,它所需的虚拟空间是:2G*n+2G(内核只需2G的共享空间)。1.1地址映射•区域区域指的是上述地址空间中的・片连续地址。区域的大小必须是粒度(64k)的整数倍,不是的话系统自动处理成整数倍。不同CPU粒度大小是不ー样的,大部分都是64Ko区域的状态有:空闲、私有、映射、映像。在你的应用程序中,申请空间的过程称作保留(预订),可以用VirtualAlloc;删除空间的过程为释放,可以用VirtualFree。在程序里预订了地址空间以后,你还不可以存取数据,因为你还没有付钱,没有真实的RAM和它关联。这时候的区域状态是私有;默认情况下,区域状态是空闲:当exe或DLL文件被映射进了进程空间后,区域状态变成映像;当一般数据文件被映射进了进程空间后,区域状态变成映射。
2•物理存储器Windows各系列支持的内存上限是不ー样的,从2G到64G不等。理论上32位CPU,硬件上只能支持4G内存的寻址;能支持超过4G的内存只能靠其他技术来弥补。顺便提ー,下,Windows个人版只能支持最大2G内存,Intel使用AddressWindowsExtension(AWE)技术使得寻址范围为236=64G=当然,也得操作系统配合。内存分配的最小单位是4K或8K,-一般来说,根据CPU不同而不同,后面你可以看到可以通过系统函数得到区域粒度和页面粒度。•页文件页文件是存在硬盘上的系统文件,它的大小可以在系统属性里面设置,它相当于物理内存,所以称为虚拟内存。事实上,它的大小是影响系统快慢的关键所在,如果物理内存不多的情况下。每页的大小和上述所说内存分配的最小单位是ー样的,通常是4K或8K。•访问属性物理页面的访问属性指的是对页面进行的具体操作:可读、可写、可执行。CPU一般不支持可执行,它认为可读就是可执行。但是,操作系统提供这个可执行的权限。PAGE_NOACCESSPAGE_READONLYPAGE_READWRITEPAGE_EXECUTEPAGE_EXECUTE_READPAGE_EXECUTE_READWRITE这6个属性很好理解,第一个是拒绝所有操作,最后一个是接受收有操作;PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY这两个属性在运行同一个程序的多个实例时非常有用;它使得程序可以共享代码段和数据段。一般情况下,多个进程只读或执行页面,如果要写的话,将会Copy页面到新的页面。通过映射exe文件时设置这两个属性可以达到这个目的。PAGE_NOCACHEPAGEWRITECOMBINE
3这两个是开发设备驱动的时候需要的。PAGEGUARD当往页面写入ー个字节时,应用程序会收到堆栈溢出通知,在线程堆栈时有用。•映射过程进程地址空间的地址是虚拟地址,也就是说,当取到指令时,需要把虚拟地址转化为物理地址才能够存取数据。这个工作通过页目和页表进行。页表:111024102410241页目:从图中可以看出,页目大小为4K,其中每ー项(32位)保存ー个页表的物理地址;每个页表大小为4K,其中每・项(32位)保存一个物理页的物理地址,共有1024个页表。利用这4K+4K*1K=4.4M的空间可以表示进程的1024*1024*(ー页4K)=4G的地址空间。进程空间中的32位地址如下:高10位中10位低12位高10位用来找到1024个页目项中的ー项,取出页表的物理地址后,利用中!0位来得到页表项的值,根据这个值得到物理页的地址,由于•页有4K大小,利用低12位得到单元地址,这样就可以访问这个内存单元了。每个进程都有自己的ー个页目和页表,那么,刚开始进程是怎么找到页目所在的物理页呢?答案是CPU的CR3寄存器会保存当前进程的页目物理地址。当进程被创建时,同时需要创建页目和页表,一共需要4.4M。在进程的
4空间中,OxCO3O0000~0xC030OFFF是用来保存页目的4k空间。OxCOOO0000~0xC03FFFFF是用来保存页表的4M空间。也就是说程序里面访问这些地址你是可以读取页目和页表的具体值的(要工作在内核方式下)。有一点我不明白的是,页表的空间包含了页目的空间!至于说,页目和页表是保存在物理内存还是页文件中,我觉得,页目比较常用,应该在物理内存的概率大点,页表需要时再从页文件导入物理内存中。页目项和页表项是ー个32位的值,当页目项第〇位为1时,表明页表已经在物理内存中;当页表项第〇位为1时,表明访问的数据已经在内存中。还有很多数据是否已经被改变,是否可读写等标志。另外,当页目项第7位为1时,表明这是ー个4M的页面,这值已经是物理页地址,用虚拟地址的低22位作为偏移量。还有很多:数据是否已经被改变、是否可读写等标志。1.1ー个例子・编写生成软件程序exe软件描述如下:Main()(1:定义全局变量2:处理函数逻辑(Load所需DLL库,调用方法处理逻辑)3:定义并实现各种方法(方法含有局部变量)4:程序结束}将程序编译,生成exe文件,附带所需的DLL库。・exe文件格式exe文件有自己的格式,有若干节(section):.text用来放二进制代码(exe或dll);.data用来放各种全局数据。.text指令1:movea,b指令2:adda,b
5.data数据1:a=2数据2:b=l这些地址都是虚拟地址,也就是进程的地址空间。・运行exe程序建立进程:运行这个exe程序时,系统会创建一个进程,建立进程控制块PCB,生成进程页目和页表,放到PCB中。数据对齐:数据的内存地址除以数据的大小,余数为〇时说明数据是对齐的。现在的编译器编译时就考虑数据对齐的问题,生成exe文件后,数据基本上是对齐的,CPU运行时,寄存器有标志标识CPU是否能够自动对齐数据,如果遇到不能对齐的情况,或者通过两次访问内存,或者通知操作系统处理。要注意的是,如果数据没有对齐,CPU处理的效率是很低的。文件映射:系统不会将整个exe文件和所有的DLL文件装载进物理内存中,同时它也不会装载进页面文件中。相反,它会建立文件映射,也就是利用exe本身当作页面文件。系统将部分二进制代码装载进内存,分配页面给它。假设分配了一个页面,物理地址为0x0232FFF1。其中装载的ー个指令虚拟地址为0x40001001=01000000000000000001000000000001〇—个页面有4K,系统会将指令保存在低12位0x0001的地址处。同时,系统根据高10位0x0100找到页目项,如果没有关联的页表,系统会生成一个页表,分配ー个物理页;然后,根据中10位0x0001找到表项,将物理地址0x0232FFF!存进去。执行过程:执行时,当系统拿到ー个虚拟地址,就根据页目和页表找到数据的地址,根据页目上的值可以判断页表是在页文件中还是在内存中;如果在页文件中,会将页面导入内存,更新页目项。读取页表项的值后,可以判断数据页文件中还是在物理内存中;如果在页文件中,会导入到内存中,更新页表项。最终,拿到了数据。在分配物理页的过程中,系统会根据内存分配的状况适当淘汰暂时不
6用的页面,如果页面内容改变了(通过页表项的标志位),保存到页文件中,系统会维护内存与页文件的对应关系。由于将exe文件当作内存映射文件,当需要改变数据,如更改全局变量的值时,利用Copy-On-Write的机制,重新生成页文件,将结果保存在这个页文件中,原来的页文件还是需要被其他进程实例使用的。在清楚了指令和数据是如何导入内存,如何找到它们的情况下,剩下的就是CPU不断的取指令、运行、保存数据的过程了,当进程结束后,系统会清空之前的各种结构、释放相关的物理内存和删除页文件。1.内存状态査询函数1.1系统信息Windows提供API可以查询系统内存的ー些属性,有时候我们需要获取ー些页面大小、分配粒度等属性,在分配内存时用的上。请看以下C++程序:SYSTEMJNFOsyslnfo;GetSystemInfo(&sysInfo);coutvv”机器属性:"vvendl;coutvv"页大小ビ"sysInfo.dwPageSizevvendl;cout"”分酉己粒度二""sysInfo.dwAllocationGranularityvvendl;cout"”用户区最小值=""sysInfo.lpMinimumApplicationAddress"endl;cout"”用户区最大值=”< 7•内存状态可以获取总内存和可用内存,包括页文件和物理内存。请看以下C++程序:MEMORYSTATUSmemStatus;GlobalMemoryStatus(&memStatus);coutvv”内存初始状态:“vvendl;coutvv”内存繁忙程度二"vvmemStatus.dwMemoryLoadvvendl;coutvv”总物理内存="vvmemStatus.dwTotalPhysvvendl;coutvv”可用物理内d^=,,«memStatus.dwAvailPhys«endl;coutvv”总页文件二"vvmemStatus.dwTotalPageFilevvendl;cout«,rnj用页文件二"vvmemStatus.dwAvai1PageFile«endl;coutvv”总进程空间二”vvmemStatus.dwTotalVirtuakvendl;coutvv”可用进程空间二”vvmemStatus.dwAvailVirtualvvendkvendl;结果如下:207840620447445205172192215955400233222108715757542.•=0=791=态度=1存57=1=2间薈存内=2在空始忙内理件文空程初繁理物文豊进存居用内内总司总可总可可以看出,总物理内存是1G,可用物理内存是510兆,总页文件是2.5G,这个是包含物理内存的页文件;可用页文件是1.9G。这里还标识了总进程空间,还有可用的进程空间,程序只用了22兆的内存空间。这里说的都是大约数。内存繁忙程序是标识当前系统内存管理的繁忙程序,从。到100,其实用处不大。•在函数里而静态分配ー些内存后,看看究竟发生什么charstatf65536];MEMORYSTATUSmemStatus1;GlobalMemoryStatus(&memStatus1);coutvv”静态分配空间:n«endl;printf(”指针地址二%x/n”,stat);coutv〈”减少物理内存二”vvmemStatus.dwAvailPhys-memStatusl.dwAvailPhysvvendl; 8coutvぐ减少可用页文件=,,«memStatus.dwAvailPageFile-memStatusl.dwAvailPageFile«endl;coutvv”减少可用进程空间メvvmemStatus.dwAvailVirtuaレmemStatusl.dwAvailVirtual«endl«endl;结果如下:C=0间:e0=0件空间2f存文程空=1受进配址理用用分地物可可态针少少少建减减减可以看出,物理内存、可用页文件和进程空间都没有损耗。因为局部变量是分配在线程堆栈里面的,每个线程系统都会建立一个默认1M大小的堆栈给线程函数调用使用。如果分配超过1M,就会出现堆栈溢出。•在函数里面动态分配300M的内存后,看看究竟发生什么char*dynamic=newchar[300*1024*1024];MEMORYSTATUSmemStatus2;GlobalMemoryStatus(&memStatus2);coutvv”动态分配空间:n«endl;printf("指针地址二%x/n”,dynamic);coutvv”减少物理内存=n«memStatus.dwAvailPhys-memStatus2.dwAvailPhys«endl;coutvv”减少可用页文件=',«memStatus.dwAvailPageFile-memStatus2.dwAvailPageFile«endl;coutvv”减少可用进程空间=,'«memStatus.dwAvailVirtuaI-memStatus2.dwAvailVirtual«endl«endl;结果如下:指针地址=10540040减少物理内存=315551744减少可用页文件=315518976減少可用进程空间=314576896动态分配情况下,系统分配直到内存页文件使用完为止,当然,系统要留・下系统使用的页面。 92.3进程区域地址査询在给定一个进程空间的地址后,可以查询它所在区域和相邻页面的状态,包括页面保护属性、存储器类型等。•C++静态分配了两次内存,一次是4K大一点,•个是900K左右。chararrayA[4097];chararrayB[900000];第一次查询:longlen=sizeof(MEMORY_BASICJNFORMATION);MEMORY_BASIC」NFORMATIONmbiA;VirtualQuery(arrayA,&mbiA,len);coutvv”静态内存地址属性:"vvendl;coutvv”区域基地址二"vvmbiA.AHocationBasevvendl;coutvv”区域邻近页面状态=”vvmbiA.Statevvendl;coutvv”区域保护属性=”vvmbiA.AllocationProtectvvendl;coutvv”页面基地址二”vvmbiA.BaseAddressvvendl;printf(narrayA指针地址二%x/n”,arrayA);coutvV”从页面基地址开始的大小二”号mbiA.RegionSizevvendl;cout"”邻近页面物理存储器类型二”"mbiA.Typevvendl;cout"”页面保护属性二”"mbiA.Protect"endl"endl;第二次查询:MEMORY_BASIC」NFORMATIONmbiB;VirtuaIQuery(arrayB,&mbiB,len);cout"”静态内存地址属性:”"endl;cout"”区域基地址二”"mbiB.AllocationBasevvendl;cout"”区域邻近页面状态二”"mbiB.Statevvendl;cout"”区域保护属性二”"mbiB.AllocationProtectvvendl;cout"”页面基地址二”"mbiB.BaseAddressvvendl;printf("arrayB指针地址二%x/n”,arrayB);cout"”从页面基地址开始的大小二”"mbiB.RegionSizevvendl; 10cout<<"邻近页面物理存储器类型="< 11•C++动态分配了两次内存,一次是1K大一点,ー个是64K左右。所以应该不会在ー个区域。char*dynamicA=newchar[1024];char*dynamicB=newcharf65467];VirtuaIQuery(dynamicA,&mbiA,len);cout«n动态内存地址属性:n«endl;coutvv”区域基地址="v 12态域域域面na页近面内基邻保基mi面页保拿近护地CA基面护属00状=400地开存=4址=0面性=0苴理性地址址指地鷲性30态aトゝw094/4-03勺S0=自口ぎ39址始储个=8192型=131072态域域域面na页近面内基邻保基mi面血保近护地CB基面护属0E状=40E地开存=4址=0面性=0基理性地址費址指地闇性10态06^01014/4-00=e的器10址始储2369这里是动态分配,dynamicA和dynamicB处于两个不同的区域;同样,页面都受页文件支持,并且区域都是提交的。第二个区域是比64K大的,由分配粒度可知,区域至少是128K。那么,剩下的空间也是提交的吗,如果是的话那就太浪费了。看看就知道了:0x00E21000n定在这个空间里,所以查询如下:VirtualQuery((char*)0xE23390,&mbiB,len);coutくぐ动态内存地址属性:"< 13属0E状=40E地开存=0址=0面性=0基理性地址畫址指地闇拿近护地CB基面护内基邻保基mi面页保态域域域面na页近面区区页My从矇ー性10态21址始储00『的器0ーー=00^©!02ユノキ.可以看出,邻近页面状态为保留,还没提交,预料之中;OxOOEl0000这个区域的大小可以计算出来:69632+978944=1024Ko系统动态分配了1M的空间,就为了64K左右大小的空间。可能是为了使得下次有要求分配时时不用再分配了。3.内存管理机制ー一虚拟内存(VM)・虚拟内存使用场合虚拟内存最适合用来管理大型对象或数据结构。比如说,电子表格程序,有很多单元格,但是也许大多数的单元格是没有数据的,用不着分配空间。也许,你会想到用动态链表,但是访问又没有数组快。定义二维数组,就会浪费很多空间。它的优点是同时具有数组的快速和链表的小空间的优点。・分配虚拟内存如果你程序需要大块内存,你可以先保留内存,需要的时候再提交物理存储器。在需要的时候再提交オ能有效的利用内存。一般来说,如果需要内存大于1M,用虚拟内存比较好。・保留用以下Windows函数保留内存块VirtualAlloc(PVOID开始地址,SIZE_T大小,DWORD类型,DWORD保护属性)一般情况下,你不需要指定“开始地址’’,因为你不知道进程的那段空间是不是已经被占用了;所以你可以用NULL。“大小”是你需要的内存字节;“类型”有MEM_RESERVE(保留)、MEM.RELEASE(释放)和MEM.COMMIT(提交)。“保护属性”在前面章节有详细介绍,只能用前六种属性。如果你要保留的是长久不会释放的内存区,就保留在较高的空间区域,这样不会产生碎片。用这个类型标志可以达到;MEMRESERVEIMEMTOPDOWN〇 14C++程序:保留1G的空间LPVOIDpV=VirtualAlloc(NULL,l000*1024*1024,MEM_RESERVEIMEM_TOP_DOWN,PAGE_READWRITE);if(pV==NULL)cout<ぐ没有那么多虚拟空间!"< 15if(pP==NULL)cout<ぐ没有那么多物理空间!”< 16中,系统没有RAM页面后,会将这个页面暂时写进虚拟内存页文件中,这样来回的倒腾系统会很慢;如果那ー页数据已经不需要的话,系统可以直接使用。当程序需要它那ー页时,系统会分配另ー页给它。VirtualAlk>c(PVOID开始地址,SIZE_T大小,DWORD类型,DWORD保护属性)“大小”如果小于ー个页面的话,函数会执行失败,因为系统使用四舍五入的方法;“类型,,是MEM_RESETo有人说,为什么需要清除呢,释放不就行了吗?你要知道,释放了后,程序就无法访问了。现在只是因为不需要结构的内容了,顺便提高一下系统的性能;之后程序仍然需要访问这个结构的。C++程序:清除1M的页面:PVOIDre=VirtuaiAlioc(pV,1024*1024,MEM_RESET,PAGE_READWRITE);if(re==NULL)cout<〈"清除失败!”< 17cout<<"可用物理内ir="«memStatus.dwAvailPhys«endl;cout<<“总页文件="< 18可以释放整个保留的空间,或者只释放分配的・些物理内存。释放特定分配的物理内存:如果不想释放所有空间,可以只释放某些物理内存。“开始地址”是页面的基地址,这个地址不一定是第一页的地址,ー个窍门是提供ー页中的某个地址就行了,因为系统会做页边界处理,取该页的首地址;“大小''是页面的要释放的字节数;“类型”是MEM.DECOMMIToC++程序:〃只释放物理内存VirtualFree((int*)pV+2OOO,5O*1024*1024,MEM_DECOMMIT);int*a=(int*)pV;a[10]=2,可以使用,没有释放这・页MEMORYSTATUSmemStatusVirtual3;GlobalMemoryStatus(&memStatusVirtual3);cout〈ぐ物理内存释放:"«endl;cout<〈"增加物理内存=,,«memStatusVirtual3.dwAvailPhys-memStatusVirtual2.dwAvailPhys«endl;cout<<"增加可用页文件="«memStatusVirtual3.dwAvailPageFile-memStatusVirtual2.dwAvailPageFile«endl;cout<<"增加可用进程空间メ«memStatusVirtual3.dwAvailVirtual-memStatusVirtual2.dwAvailVirtual«endl«endl;结果如下:9543029=5间.•=4件空放存文程釋受进存理用用可可理加加加可以看见,只释放物理内存,没有释放进程的空间。释放整个保留的空间:VirtualFree(LPVOID开始地址,SIZE.T大小,DWORD类型)“开始地址”•定是该区域的基地址;“大小’‘必须是〇,因为只能释放整个保留的空间;“类型”是MEM—RELEASE。C++程序: 19VirtualFree(pV,O,MEM_RELEASE);〃a[10]=2;不能使用了,进程空间也释放了MEMORYSTATUSmemStatusVirtual4;GlobalMemoryStatus(&memStatusVirtual4);cout<<"虚拟内存释放:“< 20当在boot.ini上加上/3GB选项时,应用程序的进程空间增加了1G,也就是说,你写程序时,可以分配的空间又增大了1G,而不管物理内存是多少,反正有虚拟内存的页文件,大不了慢点。PAE当在boot.ini上加上/PAE选项时,操作系统可以支持大于4G的物理内存,否则,你加再多内存操作系统也是不认的,因为管理这么大的内存需要特殊处理。所以,你内存小于4G是没有必要加这个选项的。注意,当要支持大于!6G的物理内存时,不能使用/3G选项,因为,只有1G的系统空间是不能管理超过16G的内存的。AWE当在boot.ini上加上/AWE选项时,应用程序可以为自己保留物理内存,直接的使用物理内存而不通过页文件,也不会被页文件交换出去。当内存大于3G时,就显得特别有用。因为可以充分利用物理内存。当物理内存大于4G时,需要/PAE的支持。以下是ー个boot.ini的实例图,是我机器上的: 21rboot.ini-Notepad■□:jXFileEditFormatViewHelp[bootloader]timeout=30default二multi(0)disk(O)rdisk(O)partition(1)\windows[operatingsystems]multi(0)disk(O)rdisk(O)partition(1)\wiNDOWS="MicrosoftwindowsxpProfessional"/noexecute=optin/fastdetect/3gb/pae/awe要使用AWE»需要用户具有LockPagesinMemory权限,这个在控制面板中的本地计算机政策中设置。第一,分配进程虚拟空间:VirtualAlloc(PVOID开始地址,SIZE_T大小,DWORD类型,DWORD保护属性)“开始地址”可以是NULL,由系统分配进程空间;“类型”是MEM_RESERVEIMEM_PHYSICAL;“保护属性’,只能是PAGE_READWRITEoMEM_PHYSICAL指的是区域将受物理存储器的支持。第二,你要计算出分配的页面数目PageCount:利用本文第二节的GetSystemlnfo可以计算出来。第三,分配物理内存页面:AllocateUserPhysicalPages(HANDLE进程句柄,SIZE_T页数,ULONG_PTR页面指针数组)进程句柄可以用GetCurrentProcess。获得;页数是刚计算出来的页数PageCount;页面数组指针unsignedlong*ArrayfPageCount]〇系统会将分配结果存进这个数组。第四,将物理内存与虚拟空间进行映射:MapUserPhysicalPages(PVOID开始地址,SIZE_T页数,ULONG_PTR页面指针数组) 22“开始地址’’是第一步分配的空间;这样的话,虚拟地址就可以使用了。如果“页面指针数组"是NULL,则取消映射。第五,释放物理页面FreeUserPhysicalPages(HANDLE进程句柄,SIZE_T页数,ULONG_PTR页面指针数组)这个除了释放物理页面外,还会取消物理页面的映射。第六,释放进程空间VirtualFree(PVOID开始地址,〇,MEM_RELEASE)C++程序:首先,在登录用户有了LockPagesinMemory权限以后,还需要调用WindowsAPI激活这个权限。BOOLVirtualMem::LoggedSetLockPagesPrivilege(HANDLEhProcess,BOOLbEnable)(struct{DWORDCount;〃数组的个数LUID_AND_ATTRIBUTESPrivilege[1];}Info;HANDLEToken;〃打开本进程的权限句柄BOOLResult=OpenProcessToken(hProcess,TOKEN_ADJUST_PRIVILEGES,&Token);If(Result!=TRUE)printf("Cannotopenprocesstoken./n");returnFALSE;〃我们只改变・个属性Info.Count=1;〃准备激活if(bEnable)Info.Privilege[0].Attributes=SE_PRIVILEGE_ENABLED;elseInfo.Privilege[0].Attributes=0; 23〃根据权限名字找到LGUIDResult=LookupPrivilegeValue(NULL,SE_LOCK_MEMORY_NAME,&(Info.Privilege[0].Luid));if(Result!=TRUE){printf("Cannotgetprivilegefor%s./n",SE_LOCK_MEMORY_NAME);returnFALSE;)〃激活LockPagesinMemory权限Result=AdjustTokenPrivileges(Token,FALSE,(PTOKEN_PRIVILEGES)&lnfo,0,NULL,NULL);if(Result!=TRUE){printf("Cannotadjusttokenprivileges(%u)/n",GetLastError());returnFALSE;}else(if(GetLastErrorO!=ERROR_SUCCESS)Iprintf("CannotenabletheSE_LOCK_MEMORY_NAMEprivilege;");printf("pleasecheckthelocalpolicy./n");returnFALSE; 24CloseHandle(Token);returnTRUE;}分配100M虚拟空间:PVOIDpVirtual=VirtualAlloc(NULL,100*1024*1024,MEM_RESERVEIMEM_PHYSICAL,PAGE_READWRITE);if(pVirtual==NULL)cout<ぐ没有那么大连续进程空间!"< 25〃如果没激活权限,是不能调用这个方法的,可以调用,但是返回FALSEBOOLflag=AllocateUserPhysicalPages(GetCurrentProcess(),&pages,frameArray);if(flag==FALSE)coutvv”分配物理内存失败ジvvendl;MEMORYSTATUSmemStatusVirtual6;GlobalMemoryStatus(&memStatusVirtual6);coutvv"物理内存分配:,,«endl;coutvぐ减少物理内存=n«memStatusVirtual5.dwAvailPhys-memStatusVirtual6.dwAvailPhys«endlcoutvv”减少可用页文件=,,«memStatusVirtual5.dwAvailPageFile-memStatusVirtual6.dwAvailPageFile«endl;coutvv”减少可用进程空间=n«memStatusVirtual5.dwAvailVirtual-memStatusVirtual6.dwAvailVirtual«endl«endl;结果如下:物理R存分配:憾少物理内存=105037824懈少可用员文件=104976384幟少可用逬程空间=1048576分配了物理内存,可能分配时需要进程空间管理。物理内存映射进程空间:int*pVInt=(int*)pVirtual;//pVInt[01=!0;这时候访问会出错flag=MapUserPhysicalPages(pVirtual,l,frameArray);if(flag==FALSE)cout<ぐ映射物理内存失败!”< 26coutvぐ减少物理内存=n«memStatusVirtual6.dwAvailPhys-memStatusVirtual7.dwAvailPhys«endlcout<〈"减少可用页文件=n«memStatusVirtual6.dwAvailPageFile-memStatusVirtual7.dwAvailPageFile«endl;coutvv"减少司用进程空间="«memStatusVirtual6.dwAvailVirtual-memStatusVirtual7.dwAvailVirtual«endl< 27•使用场合它有三个主要用途:系统加载EXE和DLL文件操作系统就是用它来加载exe和dll文件建立进程,运行exeo这样可以节省页文件和启动时间。访问大数据文件如果文件太大,比如超过了进程用户区2G,用fopen是不能对文件进行操作的。这时,可用内存映射文件。对于大数据文件可以不必对文件执行!/O操作,不必对所有文件内容进行缓存。进程共享机制内存映射文件是多个进程共享数据的一种较高性能的有效方式,它也是操作系统进程通信机制的底层实现方法。RPC、COM、OLE、DDE、窗口消息、剪贴板、管道、Socket等都是使用内存映射文件实现的。・系统加载EXE和DLL文件ノEXE文件格式每个EXE和DLL文件由许多节(Section)组成,每个节都有保护属性:READ,WRITE,EXECUTE和SHARED(可以被多个进程共享,关闭页面的COPY-ON-WRITE属性)。以下是常见的节和作用:节名作用.text.exe和.dll文件的代码.data已经初始化的数据.bss未初始化的数据.reloc重定位表(装载进程的进程地址空间).rdata运行期只读数据.CRTC运行期只读数据.debug调试信息.xdata异常处理表.tls线程的本地化存储.idata输入文件名表.edata输出文件名表.rsrc资源表.didata延迟输入文件名表ノ加载过程 281.系统根据exe文件名建立进程内核对象、页目和页表,也就是建立了进程的虚拟空间。2.读取exe文件的大小,在默认基地址0x00400000上保留适当大小的区域。可以在链接程序时用/BASE选项更改基地址(在VC工程属性/链接器/高级上设置)。提交时,操作系统会管理页目和页表,将硬盘上的文件映射到进程空间中,页表中保存的地址是exe文件的页偏移。3.读取exe文件的.idata节,此节列出exe所用到的所有dll文件。然后和exe文件一样,将dll文件映射到进程空间中。如果无法映射到基地址,系统会重新定位。4.映射成功后,系统会把第・页代码加载到内存,然后更新页目和页表。将第一条指令的地址交给线程指令指针。当系统执行时,发现代码没有在内存中,会将exe文件中的代码加载到内存中。,第二次加载时(运行多个进程实例)1.建立进程、映射进程空间都跟前面一样,只是当系统发现这个exe已经建立了内存映射文件对象时,它就直接映射到进程空间了;只是当系统分配物理页面时,根据节的保护属性赋予页面保护属性,对于代码节赋予READ属性,全周变量节赋予COPY-ON-WRITE属性。2.不同的实例共享代码节和其他的节,当实例需要改变页面内容时,会拷贝页面内容到新页面,更新页目和页表。3.对于不同进程实例需要共享的变量,exe文件有一个默认的节,给这个节赋予SHARED属性。4.你也可以创建自己的SHARED节#pragmadata_seg("节名”)LonginstCount;#pragmadata_seg()然后,你需要在链接程序时告诉编译器节的默认属性。/SECTION:节名,RWS或者,在程序里用以下表达式:#pragmacomment(linker,"/SECTION:节名,RWS")这样的话编译器会创建.drective节来保存上述命令,然后链接时会用它改变节属性。 29注意,共享变量有可能有安全隐患,因为它可以读到其他进程的数据。C++程序:多个进程共享变量举例*.cpp开始处:#pragmadata_seg(n.share'*)longshareCount=0;#pragmadata_seg()#pragmacomment(linker,"/SECTION:.share,RWS'*)ShareCount++;注意,同一个exe文件产生的进程会共享shareCount,必须是处于同一个位置上的exe〇・访问大数据文件ノ创建文件内核对象使用CreateFile(文件名,访问属性,共享模式,…)API可以创建。其中,访问属性有:0不能读写(用它可以访问文件属性)GENERIC_READGENERIC.WRITEGENERIC_READIGENERIC_WRITE;共享模式:〇独享文件,其他应用程序无法打开FILE_SHARE_WRITEFILE_SHARE_READIFILE_SHARE_WRITE这个属性依赖于访问属性,必须和访问属性不冲突。当仓リ建失败时,返回!NVALID_HANDLE_VALUEo 30C++程序如下:试图打开ー个!G的文件:MEMORYSTATUSmemStatus;GlobaIMemoryStatus(&memStatus);HANDLEhn=CreateFile(L"D://lG.rmvb",GENERlC_READIGENERIC_WRITE,FILE_SHARE_READIFILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);if(hn==INVALID_HANDLE_VALUE)cout<ぐ打开文件失败!”< 31HANDLECreateFileMapping(Handle文件,PSECURITY_ATTRIBUTES安全属性,DWORD保护属性,DWORD文件大小高32位,DWORD文件大小低32位,PCTSTR映射名称)“文件’’是上面创建的句柄;“安全属性''是内核对象需要的,NULL表示使用系统默认的安全属性;“保护属性''是当将存储器提交给进程空间时,需要的页面属性;PAGE_READONLY,PAGE_READWRITE和PAGE_WRITECOPYo这个属性不能和文件对象的访问属性冲突。除了这三个外,还有两个属性可以和它们连接使用(I)。当更新文件內容时,不提供缓存,直接写入文件,可用SEC_NOCACHE;当文件是可执行文件时,系统会根据节赋予不同的页面属性,可用SEC_IMAGE。另外,SEC_RESERVE和SEC_COMMIT用于稀疏提交的文件映射,详细介绍请参考下文。“文件大小高32位”和“文件大小低32位”联合起来告诉系统,这个映射所能支持的文件大小(操作系统支持2"^文件大小);当这个值大于实际的文件大小时,系统会扩大文件到这个值,因为系统需要保证进程空间能完全被映射。值为〇默认为文件的大小,这时候如果文件大小为〇,创建失败。“映射名称''是给用户标识此内核对象,供各进程共享,如果为NULL,则不能共享。对象创建失败时返回NULLo创建成功后,系统仍未为文件保留进程空间。C++程序:MEMORYSTATUSmemStatus2;GlobalMemoryStatus(&memStatus2);HANDLEhmap=CreateFileMapping(hn,NULL,PAGE_READWRITE,O,O,L"Yeming-Map");if(hmap==NULL)cout<ぐ建立内存映射对象失败!"«endl;MEMORYSTATUSmemStatus3;GlobalMemoryStatus(&memStatus3);cout〈ぐ建立内存映射文件后的空间:"«endl;cout<<"减少物理内存=M«memStatus2.dwAvailPhys-memStatus3.dwAvailPhys«endl;cout<〈"减少可用页文件="«memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile«endl;coutくぐ减少可用进程空间メ«memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual«endl«endl;结果如下: 32建立内存映射文件后的空间:减少可用反文件减少可用进程空间S默认内存映射的大小是1G文件。没有损失内存和进程空间。它所做的是建立内核对象,收集一些属性。,文件映射内核对象映射到进程空间API如下:PVOIDMAPViewOtFile(HANDLE映射对象,DWORD访问属性,DWORD偏移量高32位,DWORD偏移量低32位,SIZE_T字节数)“映射对象”是前面建立的对象;“访问属性’’可以是下面的值:FILE_MAP_WRITE(读和写)、FILE_MAP_READ、FILE_MAP_ALL_ACCESS(读和写)、FILE_MAP_COPYo当使用FILE_MAP_COPY时,系统分配虚拟页文件,当有写操作时,系统会拷贝数据到这些页面,并赋予PAGE_READWRITE属性。可以看到,每一步都需要设置这类属性,是为了可以多点控制,试想,如果在这ー•步想有多种不同的属性操作文件的不同部分,就比较有用。“偏移高32位”和“偏移低32位”联合起来标识映射的开始字节(地址是分配粒度的倍数):“字节数”指映射的字节数,默认0为到文件尾。当你需要指定映射到哪里时,你可以使用: 33PVOIDMAPViewOfFile(HANDLE映射对象,DWORD访问属性,DWORD偏移量高32位,DWORD偏移量低32位,SIZE_T字节数,PVOID基地址)“基地址’’是映射到进程空间的首地址,必须是分配粒度的倍数。C++程序:MEMORYSTATUSmemStatus3;GlobalMemoryStatus(&memStatus3);LPVOIDpMAP=MapViewOfFile(hmap,FILE_MAP_WRITE,0,0,0);coutくぐ映射内存映射文件后的空间:"«endl;if(pMAP==NULL)coutくぐ映射进程空间失败!"< 34另・个进程空间的数据也会跟着改变;不保证不同映射内核对象的多次映射的一致性。所以,使用文件映射时,最好在CreateFile时将共享模型设置为。独享,当然,对于只读文件没这个必要。C++程序:使用1G的文件MEMORYSTATUSmemStatus4;GlobalMemoryStatus(&memStatus4);coutvv”读取1G文件前:“vvendl;cout«n可用物理内^=n«memStatus4.dwAvailPhys«endl;coutvv”可用页文件二"v 35但有时候损失不过十几兆,可能跟系统当时的状态有关。不管怎样,这样你完全看不到!/〇操作,就像访问普通数据结构•样方便。イ保存文件修改为了提高速度,更改文件时可能只更改到了系统缓存,这时,需要强制保存更改到硬盘,特别是撤销映射前。BOOLFlushViewOfFile(PVOID进程空间地址,SIZE.T字节数)“进程空间地址”指的是需要更改的第一个字节地址,系统会变成页面的地址;“字节数’’,系统会变成页面大小的倍数。写入磁盘后,函数返回,对于网络硬盘,如果希望写入网络硬盘后オ返回的话,需要将FILE_FLAG_WRITE_THROUGH参数传给CreateFile。当使用FILE_MAP_COPY建立映射时,由于对数据的更改只是对虚拟页文件的修改而不是硬盘文件的修改,当撤销映射时,会丢失所做的修改。如果要保存,怎么办?你可以用FILE_MAP_WRITE建立另外•个映射,它映射到进程的另外一段空间;扫描第一一个映射的PAGE_READWRITE页面(因为属性被更改),如果页面改变,用MoveMemory或其他拷贝函数将页面内容拷贝到第二次映射的空间里,然后再调用FlushViewOfFile。当然,你要记录哪个页面被更改。ノ撤销映射用以下API可以撤销映射;BOOLUnmapViewOfFile(PVOIDpvBaseAddress)这个地址必须与MapViewOfFile返回值相同。,关闭内核对象在不需要内核对象时,尽早将其释放,防止内存泄露。由于它们是内核对象,调用CloseHandle(HANDLE)就可以了。在CreateFileMapping后马上关闭文件句柄;在M叩ViewOfFile后马上关闭内存映射句柄;最后再撤销映射。«进程共享机制,基于硬盘文件的内存映射如果进程需耍共享文件,只要按照前面的方式建立内存映射对象,然后按照 36名字来共享,那么进程就可以映射这个对象到自己的进程空间中。C++程序如下:HANDLEmapYeming=OpenFileMapping(FILE_MAP_WRrTE,true,L"Yeming-Map")if(mapYeming==NULL)coutvv”找不至リ内存映射对象:Yeming・Mapドvvendl;MEMORYSTATUSmemStatus3;GlobalMemoryStatus(&memStatus3);LPVOIDpMAP=MapViewOfFile(mapYeming,FILE_MAP_WRITE,0,0,100000000);coutvv”建立内存映射文件后的空间:n«endl;if(pMAP==NULL)coutvv”映射进程空间失败!”vvendl;elseprintf(“首地址=%x/n”,pMAP);MEMORYSTATUSmemStatus4;GlobalMemoryStatus(&memStatus4);coutくぐ减少物理内存=M«memStatus3.dwAvailPhys-memStatus4.dwAvailPhys«endl;coutvv”减少可用页文件=M«memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile«endl;coutvv”减少可用进程空间=n«memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual«endl«endint*pInt=(int*)pMAP; 37cout«plnt[1OO]«endl;结果如下:=2间=4件空射00存文程映20身进存=4理用用内址物可可立地少少少建首减减减在2.exe中打开之前l.exe创建的内存映射对象(当然,l.exe得处于运行状态),然后映射进自己的进程空间,当l.exe改变文件的值时,2.exe的文件对应值也跟着改变,Windows保证同一个内存映射对象映射出来的数据是一致的。可以看见,l.exe将值从90改为91,2.exe也跟着改变,因为它们有共同的缓冲页。,基于页文件的内存映射如果只想共享内存数据时,没有必要创建硬盘文件,再建立映射。可以直接建立映射对象:只要传给CreateFileMapping个文件句柄INVALID_HANDLE_VALUE就行了。所以,CreateFile时,・定要检查返回值,否则会建立一,个基于页文件的内存映射对象。接下来就是映射到进程空间了,这时,系统会分配页文件给它。C++程序如下:HANDLEhPageMap=CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,100000000,L"Yeming-Map-Page");if(hPageMap==NULL)cout<<"建立基于页文件的内存映射对象失败!"< 38cout<<"减少可用进程空间="< 39if(hPageMap==NULL)cout<("建立基于页文件的稀疏内存映射对象失败!”《endl;MEMORYSTATUSmemStatus8;GlobalMemoryStatus(&memStatus8);coutくぐ建立基于页文件的稀疏内存映射文件后的空间:"«endl;cout<〈"减少物理内存="«memStatus7.dwAvailPhys-memStatus8.dwAvailPhys«endl;cout<〈"减少可用页文件="«memStatus7.dwAvailPageFile-memStatus8.dwAvaiiPageFile«endl;cout<〈"减少可用进程空间="«memStatus7.dwAvailVirtual-memStatus8.dwAvailVirtua!«endl«end1;LPVOIDpVirtualMAP=MapViewOfFile(hVirtualMap,FILE_MAP_WRITE,0,0,0);cout<<"内存映射进程后的空间:"«endl;if(pVirtualMAP==NULL)cout<ぐ映射进程空间失败ジ<<endl;elseprintf("首地址=%x/n",pVirtualMAP);MEMORYSTATUSmemStatus9;GlobalMemoryStatus(&memStatus9); 40cout<〈"减少物理内存="«memStatus8.dwAvailPhys-memStatus9.dwAvailPhys«endl;cout<<"减少可用页文件="«memStatus8.dwAvailPageFile-memStatus9.dwAvailPageFile«endl;cout<<"减少可用进程空间="< 41boolresult=VirtualFree(pP,100000000,MEM_DECOMMIT);if(!result)coutvv”释放失败ジvvendl;result=VirtualFree(pP,l00000000,MEM.RELEASE);if(!result)coutvv”释放失败ジvvendl;CloseHandle(hVirtualMap);MEMORYSTATUSmemStatusll;GlobalMemoryStatus(&memStatus11);coutvv”增加物理内存=n«memStatuslLdwAvailPhys-memStatuslO.dwAvailPhys«endl;coutくぐ增加可用页文件=f,«memStatus1LdwAvailPageFile-memStatus10.dwAvailPageFile«endl;coutvぐ增加可用进程空间="«memStatusll.dwAvailVirtual-memStatuslO.dwAvailVirtual«endl«endl;result=Unm叩ViewOfFile(pVirtualMAP);if(!result)coutvv”撤销映射失败ジvvendl;MEMORYSTATUSmemStatus12;GlobalMemoryStatus(&memStatus12);coutvv”增加物理内存=n«memStatus12.dwAvailPhys-memStatus1l.dwAvailPhys«endl;coutvv”增加可用页文件=n«memStatus12.dwAvailPageFile-memStatus1l.dwAvailPageFile«endl; 42cout<<"增加可用进程空间メ«memStatusl2.dwAvailVirtual-memStatusll.dwAvailVirtual«endl«endl;结果如下:减少物理内存=204800激少可用贯文件=99983360減少可用进程空间=。增加物理内存=12288増加可用页文件=8192增加可用进程空间=。物可可・一r~rI-鴻冨冨印trtxIIKM进理用用可以看见,用Virtua圧ree是不能够释放这个稀疏映射的;最后用UnmapViewOfFile得以释放进程空间和物理内存。5.内存管理机制ー堆(Heap)•使用场合堆是进程创建时在进程空间建立的区域,由堆管理器来管理。•个进程可以有很多个堆。进程有一个默认堆为1M,可以动态的扩大。当程序需要管理很多小对象时,适合用堆;当需要的空间大于1M时,最好用虚拟内存来管理。堆的优点是,有堆管理器来替它管理,不需管理具体的事情如页面边界和分配粒度等问题,你可以从调用函数看的出来,比VirtualAlloc的参数少了不少。堆的缺点是分配和释放的速度比前儿种机制要慢,所以最好不要超过1M;不像虚拟内存那样随时提交和释放,因为它是由堆管理器决定的。如果用堆分配1G的空间,需要1分种,而用虚拟内存,则感觉不到任何延迟。•默认堆进程默认堆是供所有线程使用的,每当线程需要从堆中分配释放内存区时,系统会同步堆,所以访问速度较慢。它的默认大小是1M,同样的,你可以通过以下链接命令改变其大小:#pragmacomment(linker,7HEAP:102400000,1024000")第一个值是堆的保留空间,第二个值是堆开始时提交的物理内存大小。本文将堆改变为100M。当你在程序中扩大了堆提交的物理内存时,进程运行时,物理内存将减少扩大的数量。但是,默认堆总是可以扩大的,不能限制它的最大值。当你在程序中扩大了堆保留的空间时,进程运行时,可用进程空间将会减少扩 43大的数量。每次你用New操作符分配内存时,进程空间会相应的减少,物理内存也会相应的减少。ー个重要的提示,本文经过测试,如果你需要的内存块大部分都超过512K,那么,建堆时给它的初始大小不应该很大,因为,如果你所需内存块大于512K的话,它不是从堆中分配的,也就是说不用堆中默认的空间,但其仍然属于堆管理。默认堆的ー个用处是系统函数需要利用它运行。比如,Windows2000的字符集是UNICODE的,如果调用ANS!版本的函数,系统需要利用堆来从ANS!到UNICODE的转换,调用UNICODE版本的函数。•自建堆,使用场合保护数据结构:将不同的数据结构存在不同的堆中,可以防止不同的结构之间由于指针误操作而破坏了它们。消除内存碎片:将大小不同的结构保存在ー个堆中,会导致碎片的产生,比如释放ー个小结构时,大结构也不能利用它。独享堆的快速:如果用默认堆的话,线程之间是同步访问,速度慢;如果创建独享堆,则系统可以不需同步,比较快。第二个快速体现在释放的快速,默认堆中,你只能释放某个内存块,而不能释放整个堆;而独享堆可以一次释放堆,也就是释放了所有的内存块。ノ开始使用建立堆:使用以下APIHANDLEHeapCreate(DWORD选项,SIZE_T初始大小,SIZE_T最大值)“选项"取值为0,不是以下任意ー个HEAP_NO_SERIALIZE,系统无需同步堆HEAP_GENERATE_EXCEPTIONS,当创建失败或分配失败时产生异常。“初始大小'’是堆的大小,系统会规整到页面的整数倍,如0〜4096的任何数都 44为4096;但是,进程空间至少要64K。“最大值''是堆允许的最大值;为0则无限。使用HEAP_NO_SERIALIZE需确定只有单线程访问这个堆,否则有可能破坏堆;或程序有同步代码来同步堆。C++程序如下:pHeap=(char*)GetProcessHeap();printf("默认堆地址=%x/n”,pHeap);MEMORYSTATUSmemStatus2;GlobalMemoryStatus(&memStatus2);HANDLEhHeap=HeapCreate(HEAP_NO_SERIALIZEIHEAP_GENERATE_EXCEPTIONS,1024*1024*50,0);char*pHeap=(char*)hHeap;printf("新建堆1地址=%x/n",pHeap);if(hHeap==NULL){coutvv”创建堆失败!n«endl;)MEMORYSTATUSmemStatus3;GlobalMemoryStatus(&memStatus3);coutvv”建立堆后:"«endl;coutvv”减少物理内存=n«memStatus2.dwAvailPhys-memStatus3.dwAvailPhys«endl;coutvv”减少可用页文件=n«memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile«endl;cout<<"减少可用进程空间="«memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual«endl«endl;HANDLEhHeap2=HeapCreate(HEAP_NO_SERIALIZEIHEAP_GENERATE_EXCEPTION 45S,1024*1024*10,0);char*pHeap2=(char*)hHeap2;printf("新建堆2地址=%x/n",pHeap2);结果如下:默认堆地址=150000新建堆1地址=19c0000建立堆后:减少物理内存=52707328峰少可用页文件=52584448眄少可用进程空间=52432896師建堆2地址=4bc0000当建立堆1时,它分配了50M的物理内存给堆使用;当建立堆2时,堆2的地址是0x04bc0000=0x019c0000+50*1024*1024.分配内存:使用以下APIPVOIDHeapAlloc(HANDLE堆句柄,DWORD选项,SIZE_T字节数)“选项”可以是,HEAP_ZERO_MEMORY,所有字节初始化为0HEAP_NO_SERIALIZE,堆这个内存区独享HEAP_GENERATE_EXCEPTIONS,产生异常。如果创建堆有了它就不用再设了。异常可能为:STATUS_NO_MEMOR(无足够内存)和STATUS_ACCESS_VIOLATION(堆被破坏,分配失败)。C++程序如下:GlobalMemoryStatus(&memStatus3);PVOIDpV=HeapAlloc(hHeap,HEAP_ZERO_MEMORYIHEAP_NO_SERIALIZEIHEAP_GENERATE_EXCEPTIONS,1024*507);if(pV==NULL)(coutくぐ,分配堆内存失败[,< 46MEMORYSTATUSmemStatus4;GlobalMemoryStatus(&memStatus4);coutくぐ第一次堆分配后:"< 47coutvv”减少物理内存=H«memStatus4.dwAvailPhys-memStatus5.dwAvailPhys«endl;cout<<"减少可用页文件="«memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile«endl;cout<<"减少可用进程空间="«memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual«endl«endl;for(inti=0;i<200*1024;i++)pC2[i>9;MEMORYSTATUSmemStatus10;GlobalMemoryStatus(&memStatuslO);cout<<"第二次堆使用一半后:"< 48PVOIDHeapReAlloc(HANDLE堆句柄,DWORD选项,PVOID旧内存块地址,SIZE_T新内存块大小)“选项”除了以上三个外,还有HEAP_REALLOC_IN_PLACE_ONLY,指定不能移动原有内存块的地址。C++程序如下;GlobalMemoryStatus(&memStatus4);PVOIDpV2New=HeapReAlloc(hHeap,0,pV2,1024*1024*2);if(pV2New!=NULL)(char*pC2New=(char*)pV2New;printf("改变分西己地址二%x/n”,pC2New);cout«pC2New[0]«endl;//cout«pC2[0]«endl;i±!现访问违规SIZE_TlenNew=HeapSize(hHe叩,〇,pV2New);coutvv”改变后大小="vvlenNewvvendl;)GlobalMemoryStatus(&memStatus5);coutvv”改变分配后:“vvendl;coutvv”减少物理内存=n«memStatus4.dwAvailPhys-memStatus5.dwAvailPhys«endl;coutくぐ减少可用页文件=n«memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile«endl;coutvv”减少可用进程空间メ«memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual«endl«endl;结果如下:改变分配地址=5640020改变后大小=2097152除少物理向る=323584憾少可甫贯支件=1581056帳少可用逬程空间=1576960可以看出,新内存块紧接着原来内存块结束的地方开始创建,大小为2M;原来 49的内存块的内容被销毁和释放,所以新内存块只减少了增加的内存量。•个缺点就是,新内存块居然不保留原来内存的内容!另外,如果采用HEAP_REALLOC_IN_PLACE_ONLY的话,出现NotEnoughQuote异常。也就是说,当前内存的状况是,必须移动才可以扩大此内存块。査询内存:可以查询堆中一个内存块的大小。SIZE_THeapSize(HANDLE堆句柄,DWORD选项,LPVOID内存块地址)“选项"可为〇或HEAP_NO_SERIALIZEo参考以上例子。释放内存块:BOOLHeapFree(HANDLE堆句柄,DWORD选项,PVOID内存块地址)“选项"可为〇或HEAP_NO_SERIALIZEoC++程序如下:GlobalMemoryStatus(&memStatus5);He叩Free(hHe叩,〇,pV2New);MEMORYSTATUSmemStatus6;GlobalMemoryStatus(&memStatus6);coutvv”第二次堆分配释放后:n«endl;cout<ぐ,增加物理内存=n«memStatus6.dwAvailPhys-memStatus5.dwAvailPhys«endl;cout<<"增加可用页文件=n«memStatus6.dwAvailPageFile-memStatus5.dwAvailPageFile«endl;cout<<"增加可用进程空间=n«memStatus6.dwAvailVirtual-memStatus5.dwAvailVirtual«endl«endl;结果如下: 50カカカ.由日由冨日内存空间释放了原来的2M空间。释放堆:BOOLHeapDestroy(HANDLE堆句柄)不能用它释放默认堆,系统忽略它的处理。这一次,我们先在堆1中分配了70M的内存,由于它很大,所以,堆在堆外给它分配了内存,所以,堆1ー共有50M+70M=120M。释放程序如下:PVOIDpV4=HeapAlloc(hHeap,HEAP_ZERO_MEMORYIHEAP_NO_SERIALIZEIHEAP_GENERATE_EXCEPTIONS),1024*1024*70);if(pV4==NULL)(cout<ぐ,分配堆内存失败!"< 51boolre=HeapDestroy(hHeap);if(re==false)cout<<"释放堆失败!"< 52cout«,,Jt,,«i+l«,,=,,«handles[i]«endl;结果如下:=00150000堆2=00250000雁3=00260000=00030000堆5=00390000堆6=003A0000堆7=0i9c0000堆8=04BC0000堆9=00000000堆10=00000000可以看见,ー共有8个堆,堆1是默认堆,堆7和堆8是本文建立的堆。另外5个不知来源。验证堆:BOOLHeapValidate(HANDLE堆句柄,DWORD选项,LPVOID内存块地址)“选项”可为0或HEAP_NO_SERIALIZE;“内存块地址’’为NULL时,验证所有内存块。C++程序如下:HANDLEhandles1101;memset(handles,0,sizeof(hand!es));GetProcessHeaps(10,handles);for(inti=0;i<10;i++)(cout«,,jt,,«i+l«n="«handles[i]«n”;if(HeapValidate(handles[i],O,NULL))coutvv”验证堆成功!”vvendl;elsecout«endl; 53结果如下:佥佥佥佥佥佥佥佥功功功功功功功功成成成成成成成成坤注卄ゆけ坤ゆ段证证证证证证UI证igi=00150000堆2=00250000堆3=00260000堆4=00030000if5=00390000區=003A0000ii7=01900000土隹8=04BC0000堆9=00000000堆10=00000000合并内存块:UINTHeapCompact(HANDLE堆句柄,DWORD选项)“选项"可为〇或HEAP_NO_SERIALIZE;此函数可以合并空闲内存块。其他函数:HeapLock和HeapUnlock通常是系统使用的;HeapWalk可以遍历堆内存,需要以上两个函数。•C++内存函数Malloc和Free这是C语言使用的函数,只能从默认堆中分配内存,并且只是分配内存,不能调用构造函数,且只是按字节分配,不能按类型分配。New和Delete这是C++语言使用的函数,默认情况下从默认堆中分配内存,但是也可以通过重载New函数,从自建堆中按类型分配;同时可以执行构造函数和析构函数。它底层是通过HeapAlloc和HeapFree实现的。依赖于编译器的实现。GlobalAlloc和GlobalFree这是比HeapAlloc和HeapFree更慢的函数,但是也没有比它们更好的优点,只能在默认堆中分配;16位操作系统下利用它们分配内存。LocalAlloc和LocaiFree在WindowsNT内核里,和GlobalAlloc,GlobalFree是ー样的。 54•ー个例子默认情况下,New关键字是利用HeapAlloc在默认堆上建立对象。本文重载了类的New方法,使得类在自己的堆中存放,这样可以与外面的对象隔离,以免重要的数据结构被意外破坏。由于类中的成员变量是在堆中存放,因此不局限于线程堆栈的1M空间。C++程序如下:classAllocatelnOtherHeap(public:AllocatelnOtherHeap(void);-AllocatelnOtherHeap(void);void*operatornew(size_tsize);staticHANDLEheap;public:〃类对象唯一所需的空间intiArray[1024*1024*10];AllocatelnOtherHeap::AllocatelnOtherHeap(void){cout«"AllocateInOtherHeap()"«endl;〃如果New函数没有分配够空间,那么此处会出现访问违规memset(iArray,0,sizeof(AllocatelnOtherHeap));iArray[10241=8;)void*AllocatelnOtherHeap::operatornew(size_tsize){if(heap==NULL)heap=HeapCreate(HEAP_NO_SERIALIZEIHEAP_GENERATE_EXCEPTIONS,l024*1024*10,0);〃分配足够这个类对象的空间void*p=HeapAlloc(heap,0,sizeof(AllocateInOtherHeap));cout<<”堆的大小="< 55printf("A11ocatelnOtherHeap堆地址=%x/n",heap);printf('*AllocatelnOtherHeap返回地址=%x/n”,p);returnp;AllocatelnOtherHeap::-AllocatelnOtherHeap(void)(cout«n-AllocateInOtherHeapn«endl;}voidA1locatelnOtherHeap::operatordelete(void*p)(HeapFree(heap,O,p);HeapDestroy(heap);cout«,,delete(),,«endl;));结果如下:降的大小=41943040AllocatelnOtherHeap堆地址=ea0000AllocateInOtherHeapig[5]iteilE=18a0020new<>AllocateInOthei*Heap<>AllocatelnOtherHeap的密例大个=41943040AllocatelnOtherHeap的対象大小=41943040^Allocate1nOtherHeapdelete0可见,new函数先分配够空间,然后才能初始化对象变量;而delete函数得先做析构,才能释放空间。对象保存在堆外,因为大于512K;对象大小刚好是iArray变量的大小。注意,如果没有分配足够的空间,虽然你可以得到对象指针,但是你访问数据时可能会出现访问违规,如果没出现,那更惨,意味着你读写了别人的数据。6.内存管理机制ーー堆栈(Stack)•使用场合操作系统为每个线程都建立一个默认堆栈,大小为1M。这个堆栈是供函数调用时使用,线程内函数里的各种静态变量都是从这个默认堆栈里分配的。•堆栈结构默认1M的线程堆栈空间的结构举例如下,其中,基地址为0x00040000,刚开始时,CPU的堆栈指针寄存器保存的是栈顶的第一个页面地址0x0013F000。第二页面为保护页面。这两页是已经分配物理存储器的可用页面。随着函数的调用,系统将需要更多的页面,假设需要另外5页,则给这5页提交内存,删除原来页面的保护页面属性,最后•・页赋予保护页面属性。 56当分配倒数第二页0x00041000时,系统不再将保护属性赋予它,相反,它会产生堆栈溢出异常STATUS_STACK_OVERFLOW,如果程序没有处理它,则线程将退出。最后ー页始终处于保留状态,也就是说可用堆栈数是没有1M的,之所以不用,是防止线程破坏栈底下面的内存(通过违规访问异常达到目的)。0x0013F0000X00C410000x00040000当程序的函数里分配了临时变量时,编译器把堆栈指针递减相应的页数目,堆栈指针始终都是ー个页面的整数倍。所以,当编译器发现堆栈指针位于保护页面之下时,会插入堆栈检査函数,改变堆栈指针及保护页面。这样,当程序运行时,就会分配物理内存,而不会出现访问违规。•使用例子改变堆栈默认大小:有两个方法,•是在CreateThread〇时传,个参数进去改变;二是通过链接命令:#pragmacomment(linker,"/STACK:102400000,1024000")第・个值是堆栈的保留空间,第二个值是堆栈开始时提交的物理内存大小。本文将堆栈改变为100M。堆栈溢出处理:如果出现堆栈异常不处理,则导致线程终止;如果你只做了一般处理,内存结构已经处于破坏状态,因为已经没有保护页面,系统没有办法再抛出堆栈溢出异常,这样的话,当再次出现溢出时,会出现访问违规操作STATUS_ACCESS_VIOLATION,这是线程将被系统终止。解决办法是,恢复堆栈的保护页面。请看以下例子:C++程序如下: 57boolhandle=true;staticMEMORY_BASIC_INFORMATIONmi;LPBYTEIpPage;〃得到堆栈指针寄存器里的值_asmmovIpPage,esp;〃得到当前堆栈的ー些信息VirtualQuery(lpPage,&mi,sizeof(mi));〃输出堆栈指针printf("堆栈指针=%x/n",lpPage);〃这里是堆栈的提交大小printf("已用堆栈大小=%d/n",mi.RegionSize);printf("堆栈基址=%x/n",mi.AIlocationBase);for(inti=0;i<2;i++){_try{_try{_try{cout«H**************************”//end卜〃如果是这样静态分配导致的堆栈异常,系统默认不抛出异常,捕获不到//chara[1024*1024];〃动态分配栈空间,有系统调用Alloca实现,自动释放Add(lOOO);〃系统可以捕获违规访问int*p=(int*)OxCOOOOOOOO; 58*p=3;cout<<"执行结束"< 59/Z得到堆栈指针对应的下ー页基址IpPage=(LPBYTE)(mi.BaseAddress)-si.dwPageSize;primf("已用堆栈大小二%d/ガ,mi.RegionSize);printf("坏堆栈基址=%x/n”,mi.AHocationBase);〃释放准保护页面的下面所有内存if(!VirtualFree(mi.AllocationBase,(LPBYTE)lpPage-(LPBYTE)mi.AllocationBase,MEM_DECOMMIT))(exit(l);}/Z改页面为保护页面if(!VirtualProtect(lpPage,si.dwPageSize,PAGE_GUARDIPAGE_READWRITE,&dwOidProtect))(exit(l);)}printf("Exceptionhandler%IX/n",_exception_code());})_except(EXCEPTION_EXECUTE_HANDLER)Icout«"Defaulthandler"«endl;)1cout<<"正常执行"< 60printf("c[O]=%x/n",c);printf("c[1024*800]=%x/n",&c[1024*800-1]);}voidThreadStack::Add(unsignedlonga){〃深递归,耗堆栈charb[1000];if(a==0)return;Add(a-l);程序运行结果如下:qntsd-zd:\crash0d1c^2008-04-1715-07-48-2170cf8.dmpウK:\NTSDTraining\SimpleApplicatio^^Q@gChildEBPRetAddr0012FF44004010aa0012f£600040130d***ERROR:Symbolel32.dll-0012ffc077e523e5ArgstoChild00000000004020ac0000000100362438filecouldnotbe00000000BugApplication?IsPrefix*0x3e00362bl0BugApp1ication?nain*0x2afound.Defaultedtoexportsymbolsforkern00000000000000007ffdf000BugApplication?nainCRTStartup*0xl43“ARNING:Stackunwindinformationnotavailable.Followingframesmaybewrong.0012fff000000000004011ca0000000078746341kerne132?IsProcessorFeaturePresent♦0x9e0:000>可以看见,在执行递归前,堆栈已被用了800多K,这些是在编译时就静态决定了。它们不再占用进程空间,因为堆栈占用了默认的1M进程空间。分配是从栈顶到栈底的顺序。当第一次递归调用后,系统捕获到了它的溢出异常,然后堆栈指针自动恢复到原来的指针值,并且在异常处理里,更改了保护页面,确保第二次递归调用时不会出现访问违规而退出线程,但是,它仍然会导致堆栈溢出,需要动态的增加堆栈大小,本文没有对这个进行研究,但是试图通过分配另外内存区,改变堆栈指针,但是没有奏效。注意:在ー个线程里,全局变量加上任何一个函数里的临时变量,如果超过堆栈大小,当调用这个函数时,都会出现堆栈溢出,这种溢出系统不会抛出堆栈溢出异常,而直接导致线程退出。对于函数1调用函数2,而函数n-1又调用函数n的嵌套调用,每层调用不算临时变量将损失240字节,所以默认线程最多有1024*(1024-2)/240=4360次调用。加上函数本身有变量,这个数目会大大减少。1.求下面函数的返回值(微软)1.intfunc(x) 611.{2.intcountx=0;3.while(x)4.{5.countx++;6.x=x&(x-l);7.}8.returncountx;9.)复制代码假定x=9999.答案:8思路:将X转化为2进制,看含有的1的个数。2.什么是“引用”?申明和使用“引用”要注意哪些问题?答:引用就是某个目标变量的“别名’‘(alias),对应用的操作与对变量直接操作效果完全相同。中明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的ー个别名,它本身不是ー种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。3.将“引用”作为函数参数有哪些特点?(1)传递引用给函数与传递指针的效果是ー样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的ー个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用”・指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。4.在什么时候需要使用“常引用''?如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const类型标识符&引用名=目标变量名:例11.inta;2.constint&ra=a; 621.ra=l;〃错误2.a=1;〃正确复制代码例21.stringfoo();2.voidbar(string&s);复制代码那么下面的表达式将是非法的:1.bar(foo());2.bar(nhelloworld");复制代码原因在于foo()和"helloworld"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const.5.将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?格式:类型标识符&函数名(形参列表及类型说明){〃函数体}好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtimeerror!注意事项:(1)不能返回局部变量的引用。这条可以参照EffectiveC++[l]的Item31.主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。(2)不能返回函数内部new分配的内存的引用。这条可以参照EffectiveC++。]的Item31.虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memoryleak.(3)可以返回类成员的引用,但最好是const.这条原则可以参照EffectiveC++"]的Item30.主要原因是当对象的属性是与某种业务规则(businessrule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在ー个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏、业务规则的完整性。(4)流操作符重载返回值申明为“引用”的作用:流操作符<<和>>,这两个操作符常常希望被连续使用,Win:cout«"hello"«endl:因此这两个操作符的返回值应该是ー个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造ー个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回ー个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟ー选择。这个唯ー选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。赋值操作符=.这个操作符象流操作符ー样,是可以连续使用的,例如:x=j=10;或者(x=10)=100:赋值操作符的返回值必须是ー个左值,以便可以被继续赋值。因此引用成了这个操作符的惟ー返回值选择。 63例31.#include<iostream.h>2.int&put(intn);3.intvals[10];4.interror="l;5.voidmain()6.{7.put(0)=10;〃以put(O)函数值作为左值,等价于vals⑼=10;8.put⑼=20;〃以put(9)函数值作为左值,等价于vals[9]=20;9.cout«vals[0];10.cout«vals[9];H.)12.int&put(intn)13.{14.if(n>=0&&n<=9)returnvals[n];15.else{cout«Hsubscripterror";returnerror;}16.复制代码(5)在另外的一些操作符中,却千万不能返回引用:+-*Z四则运算符。它们不能返回引用,EffectiveC++[l]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有sideeffect,因此,它们必须构造ー个对象作为返回值,可选的方案包括:返回一个对象、返回ー个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b)==(c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。6.“引用”与多态的关系?引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。例41.ClassA;2.ClassB:ClassA{...};3.Bb;4.A&ref=b;复制代码7.“引用”与指针的区别是什么?指针通过某个指针变量指向ー个对象后,对它所指向的变量间接操作。程序中使用指 64针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传ref和pointer的区别。6.什么时候需要“引用”?流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。以上2-8参考:hup:〃blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx7.结构与联合有和区别?1.结构和联合都是由多个不同的数据类型成员组成,但在任何同一时刻,联合中只存放了一个被选中的成员(所有成员共用ー块地址空间),而结构的所有成员都存在(不同成员的存放地址不同)。2.对于联合的不同成员赋值,将会对其它成员重写,原来成员的值就不存在了,而对于结构的不同成员赋值是互不影响的。10.下面关于“联合”的题目的输出?a)1.#include 651.inti;2.struct{/・在联合中定义ー个结构*/3.charfirst;4.charsecond;5.Jhalf;6.}number;7.number.i=0x4241;/*联合成员赋值・/8.printf("%c%c 66M,number.half.first,mumber.half.second);9.number.half.first=,a,;/・联合中结构成员赋值・/10.number.half.second=,b,;11.printf("%x 67H,number.i);12.getch();13.)复制代码答案:AB(0x41对应A,是低位;0x42对应B,是高位)6261(number.i和number.half共用ー块地址空间)11.已知strcpy的函数原型:char*strcpy(char*strDest,constchar*strSrc)其中strDest是目的字符串,strSrc是源字符串。不调用C++/C的字符串库函数,请编写函数strcpy.答案:1.char*strcpy(char*strDest,constchar*strSrc)2.{3.if(strDest==NULLIIstrSrc==NULL)4.returnNULL;5.if(strDest=strSrc)6.returnstrDest;7.char*tempptr=strDest;8.while((*strDest++=*strSrc++)!='、〇');9.returntempptr;10.)复制代码12.已知String类定义如下:1.classString2.{ 681.public:2.String(constchar*str=NULL);//通用构造函数3.String(constString&another);/Z拷贝构造函数4.〜StringO;/Z析构函数5.String&operater=(constString&rhs);/Z赋值函数6.private:7.char*m_data;/Z用于保存字符串8.);复制代码尝试写出类的成员函数实现。答案:1.String::String(constchar*str)2.(3.if(str==NULL)//strlen在参数为NULL时会抛异常オ会有这步判断4.{5.m_data=newchar[1];6.m_data[O]=・0’;7.}8.else9.{10.m_data=newchar[strlen(str)+1];11.strcpy(m_data,str);12.}13.)14.String::String(constString&another)15.{16.m_data=newchar[strlen(another.m_data)+1];17.strcpy(m_data,other.m_data);18.)19.String&String::operator=(constString&rhs)20.{21.if(this==&rhs)22.return*this; 691.deleteロm_data;〃删除原来的数据,新开ー块内存2.m_data=newchar[strlen(rhs.m_data)+1];3.strcpy(m_data,rhs.m_data);4.return*this;5.}6.String:>String()7.{8.delete[]m_data;9.} 7013..h头文件中的ifndef/define/endif的作用?答:防止该头文件被重复引用。14.#include 71复制代码实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!加extern"C"声明后的编译和连接方式加extern"C"声明后,模块A的头文件变为:1./Z模块A头文件moduleA.h2.#ifndefMODULE.A_H3.#defineMODULE.A_H4.externHCMintfoo(intx,inty);5.#endif复制代码在模块B的实现文件中仍然调用foo(2,3),其结果是:(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;(2)连接器在为模块B的目标代码寻找fo。(2,3)调用时,寻找的是未经修改的符号名—foo.如果在模块A中函数声明了foo为extern"C"类型,而模块B中包含的是externintfoo(intx,inty),则模块B找不到模块A中的函数;反之亦然。所以,可以用一句话概括extern"C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问ー问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。明白了C++中extern"C"的设立动机,我们下面来具体分析extern"C"通常的使用技巧:extern"。的惯用法(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:1.extern"C"2.(3.#includeHcExample.hn4.)复制代码 72而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern"C"声明,在。c文件中包含了extern"C"时会出现编译语法错误。C++引用C函数例子工程中包含的三个文件的源代码如下:1./*c语言头文件:cExample.h*/2.#ifndefC_EXAMPLE_H3.#defineC_EXAMPLE_H4.externintadd(intx,inty);5.#endif6./*c语言实现文件:cExample.c*/7.#include"cExample.h"8.intadd(intx,inty)9.{10.returnx+y;H.)12.〃C++实现文件,调用add:cppFile.cpp13.extern"C"14.{15.#include"cExample.h"16.}17.intmain(intargc,char*argv[])18.{19.add(2,3);20.return0;21.}复制代码如果C++调用一个C语言编写的。DLL时,当包括。DLL的头文件或声明接口函数时,应加extern"C"{}.(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern"C",但是在C语言中不能直接引用声明了extern"C"的该头文件,应该仅将C文件中将C++中定义的extern"C"函数声明为extern类型。C引用C++函数例子工程中包含的三个文件的源代码如下: 732.#ifndefCPP_EXAMPLE_H3.#defineCPP_EXAMPLE_H4.extern"C"intadd(intx,inty);5.#endif6.〃C++实现文件cppExample.cpp7.#include"cppExample.h"8.intadd(intx,inty)9.{10.returnx+y;11.)12./*C实现文件cFile.c13./・这样会编译出错:#include"cExample.h"*/14.externintadd(intx,inty);15.intmain(intargc,char*argvf])16.(17.add(2,3);18.return0;19.)复制代码15题目的解答请参考《C++中extern"C”含义深层探索》注解:16.关联、聚合(Aggregation)以及组合(Composition)的区别?涉及到UML中的ー些概念:关联是表示两个类的一般性联系,比如“学生”和“老师”就是ー种关联关系;聚合表示has-a的关系,是ー种相对松散的关系,聚合类不需要对被聚合类负责,如下图所示,用空的菱形表示聚合关系:从实现的角度讲,聚合可以表示为:classA{...}classB{A*a;…}而组合表示contains-a的关系,关联性强于聚合:组合类与被组合类有相同的生命周期,组合类要对被组合类负责,采用实心的菱形表示组合关系:实现的形式是:classA{...}classB{Aa;...} 74参考文章:htlp:〃blog.csdn.net/wfwd/archive/2006/05/30/763753.aspxhttD://blog.csdn.net/wfwd/archive/20()6/05/30/763760.asr>x16.面向对象的三个基本特征,并简单叙述之?1.封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private,protected,public)2.继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。3.多态:是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。18.重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?常考的题目。从定义上来说:重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。重写:是指子类重新定义复类虚函数的方法。从实现原理上来说:重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:functionfunc(p:integer):integer!和functionfunc(p:string):integer:〇为B么编译器做过修饰后的函数名称可能是这样的:int_func、str_func.对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。19.多态的作用?主要是两个:1.隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;2.接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某ー属性时的正确调用。20.Ado与Ado.net的相同与不同?除了“能够让应用程序处理存储于DBMS中的数据”这一基本相似点外,两者没有太多共同之处。但是Ado使用OLEDB接口并基于微软的COM技术,而ADO.NET拥有自己的ADO.NET接口并且基于微软的。NET体系架构。众所周知。NET体系不同于COM体系,ADO.NET接口也就完全不同于ADO和OLEDB接口,这也就是说ADO.NET和ADO是两种数据访问方式。ADO.net提供对XML的支持。 7518.Newdelete与mallocfree的联系与区别?答案:都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new会自动调用对象的构造函数。delete会调用对象的destructor,而free不会调用对象的destructor.22.#defineDOUBLE(x)x+x,i=5*D0UBLE(5);i是多少?答案:i为30.23.有哪几种情况只能用intializationlist而不能用assignment?答案:当类中含有const、reference成员变量:基类的构造函数都需要初始化表。24.C++是不是类型安全的?答案:不是。两个不同类型的指针之间可以强制转换(用reinterpretcast)。C#是类型安全的。25.main函数执行以前,还会执行什么代码?答案:全局对象的构造函数会在main函数之前执行。26.描述内存分配方式以及它们的区别?1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。27.struct和class的区别答案:struct的成员默认是公有的,而类的成员默认是私有的。struct和class在其他方面是功能相当的。从感情上讲,大多数的开发者感到类和结构有很大的差别。感觉上结构仅仅象一堆缺乏封装和功能的开放的内存位,而类就象活的并且可靠的社会成员,它有智能服务,有牢固的封装屏障和一个良好定义的接口。既然大多数人都这么认为,那么只有在你的类有很少的方法并且有公有数据(这种事情在良好设计的系统中是存在的!)时,你也许应该使用struct关键字,否则,你应该使用class关键字。28.当ー个类A中没有生命任何成员变量与成员函数,这时sizeof(A)的值是多少,如果不是零,请解释ー下编译器为什么没有让它为零。(Autodesk)答案:肯定不是零。举个反例,如果是零的话,声明一个classA[10]对象数组,而每ー个对象占用的空间是零,这时就没办法区分A[0],Aロ]…了。 7628.在8086汇编下,逻辑地址和物理地址是怎样转换的?(Intel)答案:通用寄存器给出的地址,是段内偏移地址,相应段寄存器地址・10H+通用寄存器内地址,就得到了真正要访问的地址。29.比较C++中的4种类型转换方式?请参考:http:〃blog.csdn.net/wfwd/archive/2006/05/30/763785.aspx,重点是static_cast,dynamic_cast和reinterpret_cast的区别和应用30.分别写出BOOL,int,float,指针类型的变量a与“零”的比较语句。答案:BOOL:if(!a)orif(a)int:if(a==0)float:constEXPRESSIONEXP=0.000001if(a 771.char*p=a;2.cout«sizeof(a)«endl;//12字节3.cout«sizeof(p)«endl;//4字节复制代码计算数组和指针的内存容量1.voidFunc(chara[100])2.{3.cout«sizeof(a)«endl;//4字节而不是100字节4.}复制代码34.类成员函数的重载、覆盖和隐藏区别?答案:a.成员函数被重载的特征:(1)相同的范围(在同一个类中);(2)函数名字相同;(3)参数不同;(4)virtual关键字可有可无。b.覆盖是指派生类函数覆盖基类函数,特征是:(1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)基类函数必须有virtual关键字。c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual 78关键字,基类的函数将被隐藏(注意别与重载混淆)。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)35.Therearetwointvariables:aandb,don'tuse"if\"?:","switch"orotherjudgementstatements,findoutthebiggestoneofthetwonumbers.答案:((a+b)+abs(a-b))/236,如何打印出当前源文件的文件名以及源文件的当前行号?答案:cout«_FILE_;cout«_LINE_;_FILE_和_LINE_是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。37.main主函数执行完毕后,是否可能会再执行一段代码,给出说明?答案:可以,可以用_onexit注册ー个函数,它会在main之后执行intfnl(void),fn2(void),fn3(void),fn4(void);1.voidmain(void)2.(3.Stringstrf'zhangliiT);4._onexit(fnl);5._onexit(fn2);6._onexit(fn3);7._onexit(fn4);8.printf("ThisisexecutedfirstAn");9.)10.intfnl()11.{12.printf("nextAn");13.return0;14.) 7917.printf("executedM);18.return0;19.)20.intfn3()21.{22.printf("isM);23.return0;24.)25.intfn4()26.)27.printf("This");28.return0;29.)复制代码The_onexitfunctionispassedtheaddressofafunction(func)tobecalledwhentheprogramterminatesnormally.Successivecallsto_onexitcreatearegisteroffunctionsthatareexecutedinLIFO(last-in-first-out)order.Thefunctionspassedto_onexitcannottakeparameters.38.如何判断一段程序是由C编译程序还是由C++编译程序编译的?答案:1.#ifdef_cplusplus2.cout«"c++";3.#else4.cout«"c";5.#endif复制代码39.文件中有一组整数,要求排序后输出到另一个文件中答案:1.#include 806.inttag=false;//设置是否需要继续冒泡的标志位7.for(inti=0;i 8137.data.push_back(temp);38.)39.in.close();〃关闭输入文件流40.Order(data);41.ofstreamout("c:\\result.txt,');42.if(!out)43.{44.cout«nfileerror!'1;45.exit(l);46.)47.for(i=0;i 821.pl->next=NULL; 8311.p2->next=pl;12.pl=p2;13.p2=p3;14.p3=p3->next;15.)16.p2->next=pl;17.head=p2;18.returnhead;19.)复制代码(2)已知两个链表headl和head2各自有序,请把它们合并成一个链表依然有序。(保留所有结点,即便大小相同)1.Node*Merge(Node*headl,Node*head2)2.{3.if(headl==NULL)4.returnhead2;5.if(head2==NULL)6.returnhead1;7.Node*head=NULL;8.Node*pl=NULL;9.Node*p2=NULL;10.if(head1->data 8421.)22.Node*pcurrent=head;23.while(pl!=NULL&&p2;=NULL)24.(25.if(pl->data<=p2->data)26.(27.pcurrent->next=pl;28.pcurrent=pl;29.pl=pl->next;30.)31.else32.(33.pcurrent->next=p2;34.pcurrent=p2;35.p2=p2->next;36.}37.}38.if(pl!=NULL)39.pcurrent->next=pl;40.if(p2!=NULL)41.pcurrent->next=p2;42.returnhead;43.}复制代码(3)已知两个链表headl和head2各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。(Autodesk)答案:1.Node*MergeRecursive(Node*headl,Node*head2)2.(3.if(headl==NULL)4.returnhead2; 856.returnhead1;7.Node*head=NULL;8.if(head1->data 8617.);18.BPlay(Bb)19.(20.returnb;21.)复制代码(1)results:intmain(intargc,char*argv[])constructedbyparameter5{destructedB(5)形参析构Btl=Play(5);Bt2=Play(tl);destructedtl形参析构return0;destructedt2注意顺序!}destructedtl(2)results:intmain(intargc,char*argv[])constructedbyparameter5{destructedB(5)形参析构Btl=Play(5);Bt2=Play(10);constructedbyparameter10return0;destructedB(10)形参析构}destructedt2注意顺序!destructedtl42.写ー个函数找出ー个整数数组中,第二大的数(microsoft)答案:1.constintMINNUMBER=-32767;2.intfind_sec_max(intdata[],intcount)3.(4.intmaxnumber=data[0];5.intsec.max=MINNUMBER;6.for(inti=1;i 8715.if(data>sec_max)16.sec_max=data;17.)18.)19.returnsec_max;20.}43.写一个在ー个字符串(n)中寻找ー个子串(m)第一个位置的函数。KMP算法效率最好,时间复杂度是0(n+m)。44.多重继承的内存分配问题:比如有classA:publicclassB,publicclassC{}3P么A的内存结构大致是怎么样的?这个是compiler-dependent的,不同的实现其细节可能不同。如果不考虑有虚函数、虚继承的话就相当简单;否则的话,相当复杂。可以参考《深入探索C++对象模型》,或者:htlp://blog.csdn.net/wfwd/archive/2006/05/30/763797.aspx45.如何判断ー个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针)structnode{charval;node*next;}boolcheck(constnode*head){}//returnfalse:无环;true:有环ー种0(n)的办法就是(搞两个指针,ー个每次递增ー步,ー个每次递增两步,如果有环的话两者必然重合,反之亦然):1.boolcheck(constnode*head)2.{3.if(head==NULL)returnfalse;4.node*low=head,*fast=head->next;5.while(fast!=NULL&&fast->next!=NULL)6.(7.low=low->next;8.fast=fast->next->next; 8811.returnfalse;
此文档下载收益归作者所有