C/C++开发,无可避免的内存管理(篇一)-内存那些事

news2025/7/24 4:00:26

一、内存管理机制

        任何编程语言在访问和操作内存时都会涉及大量的计算工作。但相对其他语言,c/c++开发者必须自行采取措施确保所访问的内存是有效的,并且与实际物理存储相对应,以确保正在执行的任务不会访问不应该访问的内存位置。C/C++语言及编译器为了灵活性、为了兼容、为了速度,也将内存管理很大一部分工作交给使用者来决策与实现。

       1.1  内存存储

        内存”这个术语“,通常指的是主板上专用芯片提供的数据存储。这些芯片提供的存储通常称为随机存取存储器(RAM,Random Access Memory)、主存储器(main memory)和基础存储器(primary storage)。它是电子设备可以有效运行的核心。这些芯片提供的存储是易失性的,也就是说,当电源关闭时,芯片中的数据会丢失。

        各种类型的RAM:

        1)DRAM(Dynamic RAM,动态RAM),每秒充电数千次(刷新)

        2)SDRAM(Synchronous DRAM,同步动态DRAM),以最有效时钟运行速度刷新;

        3)SRAM(Static RAM,静态RAM),不需要向DRAM那样刷新,更快也更昂贵;

        4)VRAM(Video,视频RAM),视频硬件使用的内存;

        5)DDR SDRAM(Double Data Rate SDRAM,双倍数据速率SDRAM),在系统时钟周期的上升和下降时刷新,基本上是正常可用带宽的两倍;

        6)RDRAM(Rambus DRAM,Rambus公司的DRAM)高性能、芯片对芯片接口技术的新一代存储产品。

        7)ESDRAM(Enhanced SDRAM,增强型SDRAM),更便宜的SDRAM代替SRAM的型号。

        RAM芯片中是由一个个基本单元集成,每个单元的基本结构是晶体管和电容器的特定配置组成。每个单元都是一个数字开关,可以打开或关闭(即1或0),在逻辑实现上由单个二进制数字(即1或0)-位来标识。这些单元被分组为8位(8bits,目前的事实标准)单元调用字节。字节是测量存储设备提供的内存量的基本单位。

        内存计算单位如下:

1 byte          = 8 bits
1 word          = 2 bytes
1 double word   = 4 bytes
1 quad word     = 8 bytes
1 octal word    = 8 bytes
1 paragraph     = 16 bytes
1 kilobyte (KB) = 1,024 bytes
1 megabyte (MB) = 1,024KB = 1,048,576 bytes
1 gigabyte (GB) = 1,024MB = 1,073,741,824 bytes
1 terabyte (TB) = 1,024GB = 1,099,511,627,776 bytes
1 petabyte (PB) = 1,024TB = 1,125,899,906,842,624 bytes

        另外需要特别指出的是,CPU本身也有自己内存-寄存器以及L1、L2、L3等缓存,寄存器其实就是位于处理器(CPU)内部的小存储空间,是一块一块小的存储空间组成。寄存器是处理器最喜欢的工作区。处理器的大部分日常工作都是对寄存器中的数据进行的。将数据从一个寄存器移动到另一个寄存器是移动数据的最方便的方式。寄存器就像是CPU与RAM交互的中转站,并且它离 CPU 很近,其存取速度要比内存快得多。关键字register 修饰符定义的数据就是用CPU寄存器存放在的。

        高速缓存L*提供了比DRAM更快访问的临时存储。通过将程序的计算密集型部分放置在缓存中,处理器可以避免必须连续访问DRAM的开销。节省的资金可能是巨大的。有不同类型的缓存。一级缓存是位于处理器本身上的存储空间。二级缓存通常是处理器外部的SRAM芯片,有些计算设备还提供三级缓存空间。

        另外,磁盘空间可以用于创建虚拟内存。虚拟内存是通过使用磁盘空间模拟的内存。通常存储在DRAM中的部分内存被写入磁盘,因此处理器可以访问的内存量大于实际的物理内存量。例如,如果您有100MB的DRAM,并且使用200MB的磁盘空间来模拟内存,则处理器可以访问300MB的虚拟内存。

        使用虚拟内存是一种迫不得已的选择,需要软件程序对数据存储作出优化,即那些数据适合使用虚拟内存。因为磁盘虽然带来的很多额外内存,但在性能方面你会付出巨大的代价。磁盘I/O涉及一系列强制操作,其中一些是机械操作。PS:一个基本的事实就是,如果您将所有可能的东西都缓存在内存中,只有在绝对没有其他选择的情况下才移动到磁盘存储,这样呈现出来的处理效率是最高的。

        1.2 内存地址

        DRAM中的每个字节都分配了一个称为地址的唯一数字标识符,地址是一个整数值,就像街道上的房子门牌号一样。内存中的第一个字节分配了一个零地址。地址零附近的内存区域被称为内存底部或低内存。最后一个字节附近的内存区域称为高内存。处理器能够寻址的物理(即DRAM)字节数称为处理器的物理地址空间。

        处理器的物理地址空间指定可寻址的潜在字节数,而不是实际存在的物理字节数。处理器的物理地址空间由其拥有的地址线的数量决定。地址线是将处理器连接到DRAM芯片的一组电线。每个地址行指定给定字节地址中的单个位。例如,有32条地址线。这意味着每个字节都分配了一个32位地址,因此其地址空间由2^32个可寻址字节(4GB)组成。

        为了访问和更新物理内存,处理器使用控制总线和数据总线。总线是将处理器连接到硬件子系统的相关导线的集合。

        控制总线用于指示处理器是要从存储器读取还是要向存储器写入。数据总线用于在处理器和存储器之间来回传送数据。即:

        当处理器读取内存时,通过这些步骤:1)处理器将要读取的字节的地址放在地址线上;2)处理器在控制总线上发送读取信号;3)DRAM芯片返回数据总线上指定的字节。
        当处理器写入内存时,通过这些步骤:1)处理器将要写入的字节的地址放置在地址线上;2)处理器在控制总线上发送写信号;3)处理器通过数据总线发送要写入存储器的字节。

        上述内容只是简单阐述了处理器如何将字节读取和写入内存,但更高级的做法是,处理器还支持两种高级内存管理机制:分段和分页。

        分段是通过将计算机的地址空间划分为特定区域(称为分段)来实现的。在没有分段时,内存的换入换出都是以整个进程内存空间为单位,非常的耗时并且对内存的利用率也不高。内存分段下,程序是由若干个逻辑分段组成的,如可由代码段、数据段、栈段、堆段组成,不同的段是有不同的属性的,使用分段是一种隔离内存区域的方法,分段提供了所谓的内存保护,这样程序就不会相互干扰。内存分段后,采用虚拟地址访问内存空间,虚拟地址到物理地址的映射采用段表来实现,虚拟地址由段地址(段选择器索引)和段偏移量组成来确定物理地址。

        段地址表示特定的存储器段,并且总是存储在16位段寄存器之一中。具体地,段地址指定存储器段的基地址(最低地址)。每个段寄存器都有特定用途:

CS, Segment address of code currently being executed
SS, Segment address of stack
DS, Data segment address
ES, Extra segment address (usually data)
FS, Extra segment address (usually data)
GS, Extra segment address (usually data)

        偏移地址可以存储在通用寄存器中,大小为多少位表示。假定偏移地址为16位,这将每个段的大小限制为64KB(2^16bit)。

        分页是重新组织物理或虚拟内存实现虚拟管理内存的一种方法。分页会把整个虚拟和物理内存空间切成一段段固定尺寸的大小,这样一个连续并且尺寸固定的内存空间,叫页(Page)。如果启用了分页,处理器能够寻址的总字节数称为其虚拟地址空间。这个虚拟地址空间中的字节地址不再与处理器放置在地址总线上的地址相同。这意味着必须建立转换数据结构和代码,以便将虚拟地址空间中的字节映射到物理字节(无论该字节是在DRAM中还是在磁盘上),虚拟地址与物理地址之间通过页表来映射,内存管理单元 (MMU)就可以依据页表做将虚拟内存地址转换成物理地址的工作。

        当引用内存位置时,在内存地址解析到其物理位置的同时,处理器执行一系列检查。因为这些检查是在地址解析周期同时执行的,所以不会影响性能。如果处理器的各种检查中有一项发现保护违规,处理器将生成异常。异常是处理器产生的信号。使用处理器中断处理功能来捕获并处理异常。通常,处理器将使用特殊的数据结构,如中断描述符表(IDT),将异常切换到操作系统,然后由操作系统决定要做什么。操作系统实际上负责在处理器启动时代表处理器建立和设置IDT等。这允许操作系统自由地向IDT注册特殊处理程序,以便在发生内存冲突时可以调用适当的例程。

#include<stdio.h>

int main(int argc, char* argv[])
{
    int array[4]={0};
    int i;
	printf("sizeof(array)=%d\n",sizeof(array)/sizeof(int));
    for(i=0;i<10;i++)
    {
        array[i]=i;
        printf("set array[%d]=%d\n",i,array[i]);
    }
	getchar();
    return 0;
}

         编译上述代码,g++ main.cpp -o test.exe,运行测试,如下:

         是否觉得很奇怪呢,命名数组越界了呀,怎么还能继续执行呢,再看看系统日志是怎样的,操作系统是明确知道栈堆存储故障的,但是程序使用者在这个程序使用上,他认为程序是正确的,使用者是不会去查看源码或系统日志而知道有内存错误的,所有c/c++开发,内存使用不当带来很多潜在危害而不自知。

         还好,由于程序本身的内存空间是独立的,不会影响到操作系统和其他程序,操作系统只是把它作为异常处理了并报告了异常事件。但是如果我们不行指定了内存地址做了非法越界操作呢,下面代码或许无意中覆盖位于内存底部的中断向量表,但是绝对是引起系统崩溃的灾难。PS:这段代码别拿去测试。这段代码别拿去测试。这段代码别拿去测试。

int main(int argc, char* argv[])
{
    unsigned char *ptr;
    ptr = (unsigned char *)0x0;    //这将是一个灾难
    for(int i=0;i<64;i++)
    {
        ptr[i]=0x0;
    }
    return 0;
}

        如果程序在段或页面中引用的字节不在内存中或非法修改了内容,则会生成段或页面错误。当这种情况发生时,处理器将产生段或页面错误,并使用异常处理数据结构将其移交给本地操作系统。操作系统一旦接收到故障信号,操作系统进行异常报告及相关处理,如果是危害系统的事务,就可能造成系统崩溃重启等现象。

        1.3 系统内存管理

        高级内存管理功能由四个系统寄存器(GDTR-Global Descriptor Table Registers、LDTR-Local Descriptor Table Registers、IDTR-Interrupt Descriptor Table Registers 、TR-Task Registers )和五个模式控制寄存器(CR0、CR1、CR2、CR3、CR4)实现。

  •  GDTR,48位寄存器,全局描述符表寄存器,用于存放全局描述符表GDT的32位的线性基地址和16位的表限长值。
  • LDTR,16位寄存器,局部描述符表寄存器,保存16位的选择符。16位的选择符从GDT中选择一个LDT描述符,送入描述符高速缓冲寄存器(64位)中,以确定当前局部描述符LDT所在的位置的基地址和界限值。
  • IDTR,48位寄存器,中断描述符表寄存器,用于存放中断描述符表IDT的32位线性基地址和16位表长度值。
  • TR,16位寄存器,保存任务状态段TSS的16位段选择符。
  • CR0用于控制处理器模式和处理器状态。
  • CR1是保留的。
  • CR2寄存器用于存储导致页面错误的线性地址。
  • CR3在物理地址的解析中起着核心作用,保存页目录的基地址。
  • CR4寄存器用于启用一些高级机制。

        通用计算机(如PC)通电时,BIOS(烧录到ROM中)会立即启动并寻找可引导设备。BIOS将磁盘的引导扇区加载到地址0000[0]:7C00(即,物理地址0x07C00)的DRAM中。一旦BIOS加载了引导扇区,它就会将机器的控制权移交给现在位于0000[0]:7C00的机器指令。然后按指令集引导完完成后面的自检、中断使能、参数配置等一系列设备启动操作。

        而程序加载到内存,是由系统内存管理单元在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。这是一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行,这就是前面所示的分段。当系统内存管理单元发现没有一段连续内存空间分配给程序时,就启用Swap内存交换模块去重新置换出满足程序运行需要的内存。另外内存是昂贵的,因此程序执行时,一般不会按程序实际需要内存大小全加载,而是先加载一小块或者几小块,等真用到了再去真实的物理地址取一块,这样一块一块的就是分页。而分页是把整个物理内存空间切成一段段固定尺寸的大小,也很利于内存交换。

        通过虚拟内存(非磁盘虚拟化内存,而是无论内存虚拟化管理)、内存交换和内存分段分页的技术组合,便能让程序运行加载不需要考虑实际的物理内存地址、大小和当前分配空间,而把这些都交给了系统内存管理来处理。

        1.4 应用程序内存

        用户应用程序通常将其地址空间分段设计,划分为四部分,实际上,在Linux内核里,对分段管理用得极少,而DS、SS的指向基本上也从来没有做过区分:

  • code section,代码部分,存放当前正在运行的程序代码所在段的段基址,标识当前使用的指令代码可从该段寄存器指定的存储器段中取得,相应的偏移量由IP提供。
  • data section,数据部分,当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
  • stack/heap section,堆栈部分
  • Extra section,附加部分

        作为c/c++开发者的角度,我们主要关注内存管理的三个部分:静态区,栈,堆。

  •  静态区:保存自动全局变量和 static 变量(包括 static 全局和局部变量)。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候分配。
  •  栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限。
  •  堆:由 malloc 系列函数或 new 操作符分配的内存。其生命周期由 free 或 delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错。

        其中,容易混淆的是栈(stack)和堆(heap)的区分,stack空间有限,其空间由操作系统自动分配/释放,heap有较大内存区,其空间通过malloc 系列函数(c)或 new系列操作符(c++)手动分配/释放的。下面这些代码,来区分一下那些是堆,那些是栈或区分那些是全局变量、那些是局部变量。

static int j;	//静态区
int k = 0;		//静态区
void fun(void)
{
	static int i = 0;	//静态区
	i++;
	j++;
	k=i;
};

int *p1i = NULL;	//堆,c
int *p2i = new int; //堆,c++,int *p2i = new int();
int main(int argc, char* argv[])
{
	int n;	//栈
	for(n=0;n<10;n++)
	{
		fun();
	}
	std::cout << "j = " << j <<"\n";
	std::cout << "k = " << k <<"\n";
	std::cout << "n = " << n <<"\n";
	char *pc = new char('a');	//堆,c++
	int *p3i = (int*)malloc(2*sizeof(int));	//堆,c
	int *p4i = new int[2];		//堆,c++
	p1i = &n;
	*p3i = 11;
	*(p3i+1) = 12;
	memcpy(p4i,p3i,2*sizeof(int));
	if(NULL!=pc)
	std::cout << "*pc = " << *pc <<"\n";
	if(NULL!=p1i)
	std::cout << "*p1i = " << *p1i << "\n" ;
	if(NULL!=p2i)
	std::cout << "*p2i = " << *p2i << "\n" ;
	p2i = (p3i+1);
	if(NULL!=p2i)
	std::cout << "*p2i =" << *p2i << "\n" ;
	if(NULL!=p3i)
	std::cout << "(*p3i) = " << (*p3i) << "\n" ;
	if(NULL!=(p3i+1))
	std::cout << "(*p3i+1) = " << (*p3i+1) << "\n";
	if(NULL!=p4i)
	std::cout << "(*p4i) = " << (*p4i) << "\n" ;
	if(NULL!=(p4i+1))
	std::cout << "(*p4i+1) = " << (*p4i+1) << "\n";
	if(NULL!=pc){
		delete pc;
		pc = NULL;
	}
	if(NULL!=p3i){
		//free(p3i);
		delete[] p3i;
		p3i = NULL;
	}
	if(NULL!=p2i)
	{
		//free(p2i);
		delete p2i;    //全局指针,在某些系统下,调用该语句会出现无效指针告警
		p2i = NULL;
	}
	if(NULL!=p1i)
	{
		//free(p1i);
		delete p1i;    //全局指针,在某些系统下,调用该语句会出现无效指针告警
		p1i = NULL;
	}
	delete[] p4i;
    return 0;
}
//out log
j = 10
k = 10
n = 10
*pc = a
*p1i = 10
*p2i = 15117832    //这个随机
*p2i =12
(*p3i) = 11
(*p3i+1) = 12
(*p4i) = 11
(*p4i+1) = 12

        1.5 堆内存问题

        内存释放建议new和delete 、new[]和delete[] 、malloc和free配套使用。但像上面这段代码中,基本类型的定义编译器并不严格执行:

    int *p1i = NULL;	        //堆,c

	//free(p1i);                //OK
	delete pi; pi = NULL;       //OK,建议

        更令人疑惑的是,malloc分配0大小,居然指针不为null,也能给指针赋值,编译运行后,也无异常输出,系统日志也无事件告警。

//
    //n = 10;
	int *p5i = (int*)malloc(0);	//堆,c
	if(NULL!=p5i)    //非null
	{
		p5i = &n;    //OK
        //*p5i = n;    //也OK
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
    free(p5i);
    p5i = NULL;
// out log
*p5i = 10

        另外,对于全局指针变量,由于p2i是全局指针,在某些系统,尤其是linux下,能编译,但编译器编译出来的程序,在运行到该位置时,在释放时会报告invalid pointer无效,出现异常,这主要是操作系统内存管理机制的严格与否,毕竟遵循谁new,谁delete 的原则,不能局部内干掉全局变量。虽然程序结束时会自动释放(整个程序内存区域清除,全局指针同样在内),但建议最好不要创建全局指针变量,再重复一遍,最好不要创建全局指针变量

int *p1i = NULL;	//堆,c
int *p2i = new int; //堆,c++,int *p2i = new int();
int main(int argc, char* argv[])
{
...
    if(NULL!=p2i)
	{
		//free(p2i);    //无效指针告警
		//delete p2i;   //无效指针告警
		p2i = NULL;
	}
    if(NULL!=p1i)
	{
		//free(p1i);    //无效指针告警
		//delete p1i;   //无效指针告警
		p1i = NULL;
	}
...
}

        一般来说,大家在对于使用delete释放时,都会习惯将指针赋值为null,但是对于free释放时,就很多人没有这个习惯了,那么free释放是否会产生野指针呢,是否还需要null赋值呢。

//
    //n = 10
	int *p5i = (int*)malloc(0);	//堆,c
	if(NULL!=p5i)
	{
		//p5i = &n;
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
	free(p5i);
    //p5i = NULL;    //注释该项
	if(NULL!=p5i)    //居然也OK
	{
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
    free(p5i);    //还能继续释放,神奇吧
	p5i = NULL;   //
//out log
*p5i = 11
*p5i = 11

        看看,还能继续给该指针赋值,那是因为free仅是斩断指针变量与指向内存的关系。至于指针变量 p 本身保存的地址并没有改变,还是存在的,而非想象的null值。因此再次给起赋值是可以的,并又重新建立了指针变量与指向内存的关系,当然也可再次调用free释放,编译器不会报错。既然使用 free 函数之后指针变量 p 本身保存的地址并没有改变,那我们还是养成跟着重新把p 的值变为 NULL吧。

        那么,如果已经重新设置为NULL的指针再次调用free呢,又会怎样:

//
    //n=10
	int *p5i = (int*)malloc(0);	//堆,c
	if(NULL!=p5i)
	{
		//p5i = &n;
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
	free(p5i);
	//p5i = NULL;
	if(NULL!=p5i)
	{
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
	free(p5i);
	p5i = NULL;
	free(p5i);    //来个叛逆的测试
	getchar();

        嗯,编译时没问题的,但是程序运行时,本人在win10电脑下g++ 编译的程序卡死了,只能强制关闭运行该程序的命令窗口,如下图。所有还是遵循malloc和free建议的使用习惯,malloc和free内存分配及释放一一对应,并记得free后随即将指针赋值为NULL。

         1.6 指针作为形参引起内存问题探讨

        指针变量可以是全局变量,也可以是局部变量,尤其指针作为函数内的局部变量时,不少教程都告诉我们,函数结束会自动释放掉,使用它作为返回值往往不是我们期望的结果了,但真的就是这样吗。看下面这段代码曾经视为经典教程的代码:

char * GetMemory(char * p, int num)
{
	p = (char *)malloc(num*sizeof(char));
	return p;
}

int main(int argc, char* argv[])
{
	//
	char *str = NULL;
	str = GetMemory(str,10);
	if(NULL!=str){
		strcpy(str,"hello");
		printf("*str:%s\n",str);
		free(str);
		str = NULL;
	}
	//
	getchar();
    return 0;
}

//out log
*str:hello

        程序输出居然是“*str:hello”,神奇吧,str 居然不为空,还成功分配了空间,能给该字符指针赋值成功。再看看它的变种:

void GetMemory(char ** p, int num)
{
	*p = (char *)malloc(num*sizeof(char));
};

//
int main(int argc, char* argv[])
{
	char *str_ = NULL;
	GetMemory(&str_,10);
	if(NULL!=str_){
		strcpy(str_,"hello");
		printf("*str:%s\n",str_);
		free(str_);
        str_ = NULL;
	}
	//
	getchar();
    return 0;
}

//out log
*str:hello

        嘿,同样也成功了。不甘心啊,再次变更一下看看:

char * GetMemory(int num)
{
	char *p = (char *)malloc(num*sizeof(char));
	return p;
};

int main(int argc, char* argv[])
{
	//
	char *str_c = NULL;
	str_c = GetMemory(10);
	if(NULL!=str_c){
		strcpy(str_c,"hello");
		printf("*str:%s\n",str_c);
		free(str_c);
		str_c = NULL;
	}
	//
	getchar();
    return 0;
}
//out log
*str:hello

        直接在函数内部定义指针返回时也能分配到空间,三种方法都能得到同样效果,和想象的很不一样呀。

         难道是编译器的问题,切换到linux环境(centos7)下尝试,同样能得到正确结果:

         显然,采用指针做局部变量:如果申请了空间(用new等,赋值不算)又没有delete之前,那么这个空间在你程序运行结束之前不会释放,只要你知道这个空间的地址,就可以访问。但赋值不算,比如,你先定义一个数组,然后把数组名赋值指针。

        1.7 数值作为形参引起的内存问题

        既然说到数组了,我们再看看数组作为形参时,数组为局部变量转换为指针后,又会怎样。

char * GetMemory_G(char p[], int num)
{
	p = (char *)malloc(num*sizeof(char));
	return p;
};

void GetMemory_G(char * p[], int num)
{
	*p = (char *)malloc(num*sizeof(char));
};
//
int main(int argc, char* argv[])
{
	//
	char *str_g1 = NULL;
	str_g1 = GetMemory_G(str_g1,10);
	if(NULL!=str_g1){
		strcpy(str_g1,"hello");
		printf("*str:%s\n",str_g1);
		free(str_g1);
		str_g1 = NULL;
	}
	//
	//
	char *str_g2 = NULL;
	GetMemory(&str_g2,10);
	if(NULL!=str_g2){
		strcpy(str_g2,"hello");
		printf("*str:%s\n",str_g2);
		free(str_g2);
		str_g2 = NULL;
	}
	//
	getchar();
    return 0;
}
//out log,正确输出哦,无论是linux还是win
*str:hello
*str:hello

        再看数组在函数内定义,返回数组地址又会怎样:

char * GetMemory_G(int num)
{
	char p[num] = "hello";
	return p;
};

//编译输出
//WIN
D:\workForMy\workspace\mem_mgr>g++ main.cpp -o test.exe
main.cpp: In function 'char* GetMemory_G(int)':
main.cpp:50:7: warning: address of local variable 'p' returned [-Wreturn-local-addr]
  char p[num];
//linux
[pengyong@pyfree mem_mgr]$ g++ main.cpp -o test
main.cpp: 在函数‘char* GetMemory_G(int)’中:
main.cpp:50:7: 警告:返回了局部变量的‘p’的地址 [-Wreturn-local-addr]
  char p[num];

        显然,如果数组不是作为形参传入,在内部定义返回其指向地址会有局部变量返回异常告警,但是作为形参时已经转化为指针形式,那就和指针的性质是一样的,只要数组的地址没有被作出改变,只要知道这个数组空间的地址,就可以访问。

        c/c++编程中,内存管理这块绝对重灾区,很多常见的内存错误很多编程老手那么都经历过,还是会不知觉地又中招了。这些常见的内存错误介绍,待续。

三、本文测试代码参考

        main.cpp

#include <stdio.h>
#include <stdlib.h>
#include <memory>
#include <cstring>
#include <iostream>

static int j;	//静态区
int k = 0;		//静态区
void fun(void)
{
	static int i = 0;	//静态区
	i++;
	j++;
	k=i;
};

int *p1i = NULL;	//堆,c
int *p2i = new int; //堆,c++,int *p2i = new int();
//
char * GetMemory(char * p, int num)
{
	p = (char *)malloc(num*sizeof(char));
	return p;
};

void GetMemory(char ** p, int num)
{
	*p = (char *)malloc(num*sizeof(char));
};

char * GetMemory(int num)
{
	char *p = (char *)malloc(num*sizeof(char));
	return p;
};

char * GetMemory_G(char p[], int num)
{
	p = (char *)malloc(num*sizeof(char));
	return p;
};

void GetMemory_G(char * p[], int num)
{
	*p = (char *)malloc(num*sizeof(char));
};

char * GetMemory_G(int num)
{
	char p[num];
	return p;
};

int main(int argc, char* argv[])
{
	/*
    int array[5]={0};
    int i;
	printf("sizeof(array)=%d\n",sizeof(array)/sizeof(int));
    for(i=0;i<5;i++)
    {
        array[i]=i;
        printf("set array[%d]=%d\n",i,array[i]);
    }
	*/
	int n;	//栈
	for(n=0;n<10;n++)
	{
		fun();
	}
	std::cout << "j = " << j <<"\n";
	std::cout << "k = " << k <<"\n";
	std::cout << "n = " << n <<"\n";
	char *pc = new char('a');	//堆,c++
	int *p3i = (int*)malloc(2*sizeof(int));	//堆,c
	int *p4i = new int[2];		//堆,c++
	p1i = &n;
	*p3i = 11;
	*(p3i+1) = 12;
	memcpy(p4i,p3i,2*sizeof(int));
	if(NULL!=pc)
	std::cout << "*pc = " << *pc <<"\n";
	if(NULL!=p1i)
	std::cout << "*p1i = " << *p1i << "\n" ;
	if(NULL!=p2i)
	std::cout << "*p2i = " << *p2i << "\n" ;
	p2i = (p3i+1);
	if(NULL!=p2i)
	std::cout << "*p2i =" << *p2i << "\n" ;
	if(NULL!=p3i)
	std::cout << "(*p3i) = " << (*p3i) << "\n" ;
	if(NULL!=(p3i+1))
	std::cout << "(*p3i+1) = " << (*p3i+1) << "\n";
	if(NULL!=p4i)
	std::cout << "(*p4i) = " << (*p4i) << "\n" ;
	if(NULL!=(p4i+1))
	std::cout << "(*p4i+1) = " << (*p4i+1) << "\n";
	if(NULL!=pc){
		std::cout << "_1_\n";
		delete pc;
		pc = NULL;
	}
	if(NULL!=p3i){
		std::cout << "_2_\n";
		//free(p3i);
		delete[] p3i;
		p3i = NULL;
	}
	if(NULL!=p2i)
	{
		//free(p2i);
		//delete p2i;	//某些系统下运行会异常,无效指针
		p2i = NULL;		//某些系统下运行会异常,无效指针
	}
	if(NULL!=p1i)
	{
		//free(p1i);	//某些系统下运行会异常,无效指针
		//delete p1i;	//某些系统下运行会异常,无效指针
		p1i = NULL;
	}
	delete[] p4i;
	//
	/*
	int *p5i = (int*)malloc(0);	//堆,c
	if(NULL!=p5i)
	{
		//p5i = &n;
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
	free(p5i);
	//p5i = NULL;
	if(NULL!=p5i)
	{
		*p5i = 11;
		std::cout << "*p5i = " << *p5i << "\n" ;
	}
	free(p5i);
	p5i = NULL;
	//free(p5i);    //来个叛逆的测试
	*/
	//
	char *str = NULL;
	str = GetMemory(str,10);
	if(NULL!=str){
		strcpy(str,"hello");
		printf("*str:%s\n",str);
		free(str);
		str = NULL;
	}
	//
	char *str_ = NULL;
	GetMemory(&str_,10);
	if(NULL!=str_){
		strcpy(str_,"hello");
		printf("*str:%s\n",str_);
		free(str_);
		str_ = NULL;
	}
	//
	char *str_c = NULL;
	str_c = GetMemory(10);
	if(NULL!=str_c){
		strcpy(str_c,"hello");
		printf("*str:%s\n",str_c);
		free(str_c);
		str_c = NULL;
	}
	//
	//
	char *str_g1 = NULL;
	str_g1 = GetMemory_G(str_g1,10);
	if(NULL!=str_g1){
		strcpy(str_g1,"hello");
		printf("*str:%s\n",str_g1);
		free(str_g1);
		str_g1 = NULL;
	}
	//
	//
	char *str_g2 = NULL;
	GetMemory(&str_g2,10);
	if(NULL!=str_g2){
		strcpy(str_g2,"hello");
		printf("*str:%s\n",str_g2);
		free(str_g2);
		str_g2 = NULL;
	}
	//
	getchar();
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/368398.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java】volatile

一、volatile volatile是Java虚拟机提供的轻量级的同步机制&#xff0c;它有&#xff13;个特性&#xff1a; &#xff11;&#xff09;保证可见性 &#xff12;&#xff09;不保证原子性 &#xff13;&#xff09;禁止指令重排 当写一个volatile变量时&#xff0c;JMM会把该…

openEuler部署Ceph集群(块存储)

openEuler部署Ceph集群1 目标2 环境2.1 服务器信息2.2 软件信息3 部署流程3.1 获取系统镜像3.2 创建虚拟机3.3 配置虚拟机3.3.1 配置互信3.3.2 关闭防火墙3.3.3 配置免密登录3.3.4 配置NTP3.3.4.1 安装NTP服务3.3.4.2 配置NTP服务端3.3.4.3 配置NTP客户端3.3.4.4 启动NTP服务3.…

pyqt5通过CANoe COM Server来操作CANoe仿真工程

文章目录前言一、COM接口技术二、UI界面设计三、功能实现四、工程运行测试前言 继续学习《CANoe开发从入门到精通》。 今天在《CANoe仿真工程开发》的基础上&#xff0c;开发实现pyqt5应用程序来操控CANoe工程。 一、COM接口技术 COM&#xff08;Component Object Model&…

Linux基础命令-find搜索文件位置

文章目录 find 命令介绍 语法格式 命令基本参数 参考实例 1&#xff09;在root/data目录下搜索*.txt的文件名 2&#xff09;搜索一天以内最后修改时间的文件&#xff1b;并将文件删除 3&#xff09;搜索777权限的文件 4&#xff09;搜索一天之前变动的文件复制到test…

不懂什么是智慧工厂,看这篇文章就够了!

一、智慧工厂是什么&#xff1f; 一直以来&#xff0c;自动化在某种程度上始终是工厂的一部分&#xff0c;甚至高水平的自动化也非新生事物。然而&#xff0c;“自动化”一词通常表示单一且独立的任务或流程的执行。过去&#xff0c;机器自行“决策”的情况往往是以自动化为基…

【基础篇】9 # 排序:冒泡排序(Bubble Sort)、插入排序(Insertion Sort)、选择排序(Selection Sort)

说明 【数据结构与算法之美】专栏学习笔记 如何分析一个排序算法&#xff1f; 1、排序算法的执行效率 最好情况、最坏情况、平均情况时间复杂度时间复杂度的系数、常数 、低阶比较次数和交换&#xff08;或移动&#xff09;次数 2、排序算法的内存消耗 3、排序算法的稳定…

Fabric.js使用说明Part 2

目录一、Fabric.js使用说明Part 1Fabric.js简介 开始方法事件canvas常用属性对象属性图层层级操作复制和粘贴二、Fabric.js使用说明Part 2锁定拖拽和缩放画布分组动画图像滤镜渐变右键菜单删除三、Fabric.js使用说明Part 3自由绘画绘制背景图片绘制文本绘制线和路径一、锁定Fab…

传统豪华品牌引领?智能座舱进入「沉浸式娱乐体验」新周期

智能座舱正在进入硬件定型、软件&#xff08;功能&#xff09;升级以及多应用融合的新周期。 高工智能汽车研究院监测数据显示&#xff0c;2022年中国市场&#xff08;不含进出口&#xff09;乘用车搭载智能数字座舱&#xff08;大屏语音车联网OTA&#xff09;前装标配交付795…

【死磕数据库专栏启动】在CentOS7中安装 MySQL5.7版本实战

文章目录前言实验环境一. 安装MySQL1.1 配置yum源1.2 安装之前的环境检查1.3 下载MySQL的包1.4 开始使用yum安装1.5 启动并测试二. 设置新密码并重新启动2.1 设置新密码2.2 重新登录测试总结前言 学习MySQL是一件比较枯燥的事情&#xff0c;学习开始之前要先安装MySQL数据库&a…

【Linux修炼】14.磁盘结构/文件系统/软硬链接/动静态库

每一个不曾起舞的日子&#xff0c;都是对生命的辜负。 磁盘结构/文件系统/软硬链接/动静态库前言一.磁盘结构1.1 磁盘的物理结构1.2 磁盘的存储结构1.3 磁盘的逻辑结构二.理解文件系统2.1 对IO单位的优化2.2 磁盘分区与分组2.3 分组的管理方法2.4 文件操作三.软硬链接3.1理解硬…

测试4年裸辞失业,面试17k的测试岗被按在地上摩擦,结局让我崩溃大哭...

作为IT行业的大热岗位——软件测试&#xff0c;只要你付出了&#xff0c;就会有回报。说它作为IT热门岗位之一是完全不虚的。可能很多人回说软件测试是吃青春饭的&#xff0c;但放眼望去&#xff0c;哪个工作不是这样的呢&#xff1f;会有哪家公司愿意养一些闲人呢&#xff1f;…

「smardaten」上架钉钉应用中心!让进步再一次发生

使用钉钉的团队小伙伴们&#xff0c;smardaten给您送来福利啦~为了给更多团队提供更优质的应用开发体验&#xff0c;方便用户在线、快速使用无代码&#xff0c;数睿数据近期在【钉钉应用中心】发布smardaten在线版本。继与华为云、亚马逊云建立战略合作之后&#xff0c;smardat…

微信小程序实现分享到朋友圈的功能

分享朋友圈官方API&#xff1a;分享到朋友圈 1、分享到朋友圈接口设置事项 2、onShareTimeline()注意事项 3、分享朋友圈后&#xff0c;测试发现&#xff0c;没有数据请求。 用户在朋友圈打开分享的小程序页面&#xff0c;并不会真正打开小程序&#xff0c;而是进入一个“小程…

浏览器缓存策略

先走强缓存&#xff0c;再走协商缓存 强缓存 不发送请求&#xff0c;直接使用缓存的内容 状态码200 当前会话没有关闭的话就是走memory cache&#xff0c;否则就是disk cache 由响应头的 Pragma&#xff08;逐渐废弃&#xff0c;优先级最高&#xff09;&#xff0c;catch-…

LeetCode 817. 链表组件

LeetCode 817. 链表组件 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 给定链表头结点 headheadhead&#xff0c;该链表上的每个结点都有一个 唯一的整型值 。同时给定列表 numsnumsnums&#xff0c;该列表是上述链表中整型值的一个子集。 返回列表 numsnu…

自动驾驶仿真:ECU TEST 、VTD、VERISTAND连接配置

文章目录一、ECU TEST 连接配置简介二、TBC配置 test bench configuration三、TCF配置 test configuration提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、ECU TEST 连接配置简介 1、ECU TEST&#xff08;简称ET&#xff09;&#xff0c;用于HIL仿…

MySQL tinyint(1) 、int(32) 与 varchar(255) 长度含义不同

MySQL tinyint(1) 、int(32) 与 varchar(255) 长度含义不同 发现 tinyint(1)&#xff0c;int(32) 和 varchar(255) 这里面的数字的含义是不同的。 先说数字类型 tinyint 和 int 等 他们能存储的字节大小是与类型绑定的&#xff0c;即定义了 tinyint 或者 int 就确定了能存储…

【C++的OpenCV】第六课-OpenCV图像常用操作(三):OpenCV的图像的侵蚀和扩张

让我们继续一、图像的侵蚀和扩张1.1 侵蚀1.1.1 函数原型1.1.2 侵蚀的效果1.1.3 关于侵蚀的解释1.2 扩张1.2.1 函数原型1.2.2 扩张的效果二、实例一、图像的侵蚀和扩张 本章节中我们将会学习到&#xff1a; cv::erode() 函数详情cv::dilate() 函数详情 两个函数的基本使用方法…

java 接口 详解

目录 一、概述 1.介绍 : 2.定义 : 二、特点 1.接口成员变量的特点 : 2.接口成员方法的特点 : 3.接口构造方法的特点 : 4.接口创建对象的特点 : 5.接口继承关系的特点 : 三、应用 : 1.情景 : 2.多态 : ①多态的传递性 : ②关于接口的多态参数和多态…

Android ION 相关信息查看方法

目录 查看DMA buffer 信息 查看ion buffer 的总体分配情况 分配的ION buffer 都会设置为DMA buffer&#xff0c;以fd的形式交给使用方&#xff0c; 如app, camera等。 查看ion 的使用情况&#xff0c;可以查看ion 的各个heap分配情况&#xff0c; 也可以从DMA buffer 入手查…