栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言——内存问题

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言——内存问题

你经常遇到哪些内存问题?

c语言内存模块会出现很多问题,本文总结了c语言可能出现的内存问题、问题示例以及避免内存问题的方法。何助文档。

C 语言内存问题中内存主要指(静态)数据区、堆区和栈区空间:

  • 数据区内存在程序编译时分配,数据区内存的生存期为程序的整个运行期间,如全局变量和 static 关键字所声明的静态变量。
  • 栈区内存指函数执行时在栈上开辟局部自动变量的储存空间,执行结束时自动释放栈区内存。堆区内存由程序在运行时调用 maloc / caloc / realoc 等库函数请,并由使用者调用 free 库函数释放。
  • 堆内存比栈内存分配容量更大,生存期由使用者决定,所以非常灵活。但是堆内存使用时很容易出现内存泄露、内存越 和重复释放等严重问题。

文章目录
  • 你经常遇到哪些内存问题?
  • 文章框架图
  • 一、C语言 堆区和栈区 可能会出现哪些内存问题?
    • 1.越界访问
    • 2.堆/栈溢出
    • 3.内存泄漏
    • 4.内存碎片
    • 5.野指针
    • 6.悬空指针
    • 7.未初始化的指针变量
    • 8.引用空指针
    • 9.二次释放
  • 二、C语言 (静态)数据区 可能会出现哪些内存问题?
    • 1.多重定义
    • 2.内存越界

文章框架图

一、C语言 堆区和栈区 可能会出现哪些内存问题?
1.越界访问

(1)越界访问场景:
①读越界:读了不属于自己的数据或读了随机数。

例1:
int arr[5]={0,1,2,3,4};
printf(“%d”,arr[5]);//越界,数组下标范围为[0,4]

②写越界(缓冲区溢出):写入的数据是随机的

例2:
void function(char *str,int n)
{
char *p=NULL;//定义一个指针并初始化
p=(char *)malloc(sizeof(char)*n);//给p分配一个堆区的空间,空间大小为sizeof(char)*n
memset(p,-1,sizeof(char)*n*n);//给堆区空间p,初始化,sizeof(char)*n*n个字节全部赋初值为-1;(越界,只有n个字节空间,你却非要给n*n个字节空间赋值)
}
例3:
int main()
{
int arr[5]={0,1,2,3,4}//所申请的数组长下标范围为[0,4]
int arr[5]=10;//越界
int arr[10]=10;//越界(使用了不属于你的内存地址&arr[10])
return 0;
}

(2)如何避免越界访问
① 搞清楚数据类型、数组边界、指针边界等;
② 仔细检查外部传入的参数;


2.堆/栈溢出

(1)堆栈溢出 描述: 分配的空间不够用,例如:需要1M , 你申请的(或系统分配的)空间只有500KB。
(2)发生 堆/栈溢出 的场景

  例4:
   #include  
  int main ( ) 
  { 
  char name[8]; 
  printf("Please type your name: "); 
  gets(name); //【注意】:如果此处gets获得的字符串长度超过定义的长度8,则会发生下标溢出。
  printf("Hello, %s!", name); 
  return 0; 
  }
 

(3)导致堆栈溢出的原因:

  • 函数调用层次太深 :函数递归调用层次太深时,可能导致递归无法返回,也可能导致栈无法容纳这些递归调用后每个层次返回的值或地址从而造成堆溢出
  • 动态申请空间(例如malloc)后没有释放(free)空间: 会造成内存泄漏,一次泄漏影响不大,多次内存泄漏会造成堆栈溢出(内存溢出)。
  • 数组访问越界
  • 指针非法访问

3.内存泄漏

(1)内存泄漏 描述: : 申请的空间,使用之后没有释放掉,该现象即为内存泄漏。内存泄漏(因)最终会导致内存溢出(果)。

(2)发生 内存泄漏 的场景

例5:
char *stack1 = malloc(10);//申请一个内存空间,名字为stack1,内存大小为:10
char *stack2 = malloc(10);//申请一个内存空间,名字为stack2,内存大小为:10
stack1=stack2;//将stack2指针(地址)赋值给stack1,导致stack1最开始申请的stack1空间被闲置,没有指针指向该空间,程序结束时,要释放空间(free)时,找不到该空间所指向的指针,导致该空间没有被释放,该内存被泄露

代码解释:

  • 将stack2指针(地址)赋值给stack1,导致stack1最开始申请的stack1空间被闲置,没有指针指向该空间,程序结束时,要释放空间(free)时,找不到该空间所指向的指针,导致该空间没有被释放,该内存被泄露。
  • 如果内存泄漏次数较多,即多至到达内存最大值,即装不下了(不够用),会导致内存溢出。

(3)导致内存泄漏的原因: malloc内存分配一个空间以后,没有对其进行free(释放)
(4)如何发现该问题 :
实际上不同的系统都带有内存监视工具,我们可以从监视工具收集一段时间内的堆栈内存信息,观测增长趋势,来确定是否有内存泄漏。

①静态分析法:

  • 手动检测(人工查看):浏览源代码,查找c语言中malloc和free以及c++中的new和delete是否平衡,是否存在malloc过的内存而没有free掉(C语言中),或是否存在new过的内存而没有delete掉(c++中);
  • 使用静态扫描和分析工具查漏: 例如:splint、PC-LINT、BEAM等,BEAM 可以检测四类问题: 没有初始化的变量;废弃的空指针;内存泄漏;冗余计算。而且支持的平台比较多,例如:BEAM 支持以下平台:Linux x86 (glibc 2.2.4)、Linux s390/s390x (glibc 2.3.3 or higher)、Linux (PowerPC, USS) (glibc 2.3.2 or higher)、AIX (4.3.2+)、Window2000 以上;
  • 使用内嵌程序自动监测: 可以重载内存分配和释放函数 new 和 delete,然后编写程序定期统计内存的分配和释放,从中找出可能的内存泄漏。或者调用系统函数(linux中getrusage函数)定期监视程序堆的大小,关键要确定堆的增长是泄漏而不是合理的内存使用。

②动态运行检测法:

  • 使用Valgrind工具: Valgrind是帮助程序员找程序里面bug和改进程序性能的工具,Valgrind是在Linux系统下开发应用程序时调试内存问题的工具。Valgrind 现在提供多个工具,其中最重要的是 Memcheck,Cachegrind,Massif 和 Callgrind,其中的memecheck 工具可以用来寻找 c、c++ 程序中内存管理的错误。可以检查出下列几种内存操作上的错误:读写已经释放的内存、读写内存块越界(从前或者从后)、使用还未初始化的变量、将无意义的参数传递给系统调用、内存泄漏;
  • 使用Rational purify工具: Rational Purify 主要针对软件开发过程中难于发现的内存错误、运行时错误。在软件开发过程中自动地发现错误,准确地定位错误,提供完备的错误信息,从而减少了调试时间。同时也是市场上唯一支持多种平台的类似工具,并且可以和很多主流开发工具集成。Purify 可以检查应用的每一个模块,甚至可以查出复杂的多线程或进程应用中的错误。另外可以检查C/C++、 Java 或 .NET 中的内存泄漏问题给出报告。在 Linux 系统中,使用 Purify 需要重新编译程序。通常的做法是修改 Makefile 中的编译器变量。

(5)如何解决 :

  • 尽量避免使用malloc分配内存空间和memset初始化内存空间;
  • 程序员要养成良好习惯,保证malloc/new和free/delete匹配,每一个malloc都要有一个对应的free;
  • 一遍又一遍的看代码,希望能看出内存泄露的bug ;
  • 检查malloc/new和free/delete是否匹配,一些工具也就是这个原理。要做到这点,就是利用宏或者钩子,在用户程序与运行库之间加了一层,用于记录内存分配情况。
  • 当要给指针赋值前,应确保没有内存位置被闲置或孤立,比如开辟一个暂时的指针去存放将要被闲置的指针,保证该空间有指针指向该空间,以便最终释放时可以找到该空间;
  • 使用了大量的内存村泄露检测工具,结合各种自动测试软件,采取疯狂加变态的测试方法,试图找出所有可能的内存泄露bug
  • 代码规模超过千万行,内存从一个模块被传递到另一个或多个,谁知道传递给哪个模块,最后在那里释放的了,反正我保证那个指针指向的数据是好的就行了。由于代码的规模,穷举式的测试根本不可能,只有一遍一遍的看代码,祈祷自己的代码没有内存泄露的问题。

4.内存碎片

(1)内存碎片 描述: :内存碎片是描述一个系统中所有的不可用的空闲内存。
这些资源之所以仍然未被使用,是因为负责分配内存的分配器使这些内存无法使用。例如:malloc/new分配的是连续性空间,当你需要一个20字节的空间时,用malloc向内存空间申请20字节内存块后,分配器会给你一块连续的内存块,而大小小于20的内存块被闲置,导致小内存无法被分配,从而造成内存碎片。

(2)发生 内存碎片 的场景

例6:
char net(char **p , int num)
{
	p= (char )malloc(num);
}

int main()
{
	char *str = NULL;
	net( &str , 100);//malloc内存分配长度为100
	strcop(str,"hello");//把字符串“hello” 复制到str空间中
	free(str);//释放str空间
	str=NULL;//释放指针
	if(str !=NULL){//对str空间判空
	strcop(str,"world");
	printf("%s",str);
}

(3)导致内存碎片的原因:: 原因在与空闲内存以小而不连续的方式出现在不同的位置(内存分配较小,并且分配的这些小的内存生存周期又较长,反复申请后将产生内存碎片的出现)。 内存分配程序浪费内存的基本方式有两种:即内部碎片和外部碎片。

①内部碎片的产生: 内存分配程序需要存储一些描述其分配状态的数据。这些存储的信息包括任何一个空闲内存块的位置、大小和所有权,以及其它内部状态详情。一般来说,一个运行时间分配程序存放这些额外信息最好的地方是它管理的内存。内存分配程序需要遵循一些基本的内存分配规则。例如,所有的内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址。内存分配程序把仅仅预定大小的内存块分配给客户,可能还有其它原因。当某个客户请求一个 43 字节的内存块时,它可能会获得 44字节、48字节 甚至更多的字节。由所需大小四舍五入而产生的多余空间就叫内部碎片。

②外部碎片的产生: 外部碎片的产生是当已分配内存块之间出现未被使用的差额时,就会产生外部碎片。
例如:
-假设有一块一共有100个单位的连续空闲内存空间,范围是[0,99]。
-如果你从中申请一块内存,如10个单位,那么申请出来的内存块就是:[0,9]区间。
-这时候你继续申请一块内存,比如说5个单位大,第二块得到的内存块就应该为[10,14]区间。
-如果现在,你把第一块内存块释放(及释放掉[0,9]区间),然后再申请一块大于10个单位的内存块,比如说20个单位。因为刚被释放的内存块[0,9]不能满足新的请求,所以只能从15开始分配出20个单位的内存块[15,34]。
-现在整个内存空间的状态是[0,9]空闲,[10,14]被占用,[15,34]被占用,[35,99]空闲。其中[0,9]就是一个内存碎片了。如果[10,14]一直被占用,而以后申请的空间都大于10个单位,那么0~9就永远用不上了,变成外部碎片。

【注意】: 虽然额外开销和内部碎片会浪费内存,因此是不可取的,但外部碎片才是嵌入系统开发人员真正的敌人,造成系统失效的正是分配问题。

(4)如何发现该问题 : 应用层内存是通过内存伙伴系统进行管理,可以通过命令查看各种大小的内存块数量进行分析。

  • 通过sysrq-trigger接口查看内存信息,找到内存碎片;
  • 通过buddyinfo接口查看内存信息,找到内存碎片;

(4)如何避免内存碎片 :

  • 少用动态内存分配的函数(尽量使用栈空间);
  • 尽可能少地申请空间;
  • 尽量少使用堆上的内存空间;
  • 分配内存和释放的内存尽量在同一个函数中;
  • 尽量一次性申请较大的内存2的指数次幂大小的内存空间,而不要反复申请小内存(少进行内存的分割);
  • 使用内存池来减少使用堆内存引起的内存碎片。做内存池,也就是自己一次申请一块足够大的空间,然后自己来管理,用于大量频繁地new/delete操作。

5.野指针

(1)野指针 描述: :访问了已经释放的地址,或访问了不属于自己的空间;
(2)发生 野指针 的场景

例:
char net(char *str)
{
	char *s=NULL;//在栈区定义了一个指针,指针初值为NULL
	strcpy(s,str);//把主函数中传过来的数组str复制给s
	return s;//程序结束后,s指针被释放
}

int main()
{
	char str[100]={0};
	char *p=net(str);
	printf(“%s”,*p);//此时net函数调用结束已经结束,使用野指针
}

代码解释:

(3)导致野指针的原因:

(栈区)主调函数接收了被调函数中局部数组或局部变量的地址:: 指针指向了不可访问的地址。 主调函数接收的被调函数中定义的局部变量数组的地址,当被调函数调用结束,被调函数会释放栈区空间并出栈,所返回地址不能使用,一旦使用,则变成野指针;

②(堆区)指针被释放时没有被置空: 我们在用malloc开辟内存空间时,要检查返回值是否为空,如果为空,则开辟失败(malloc判空);如果不为空,则指针指向的是开辟的内存空间的首地址。指针指向的内存空间在用free或者delete(delete只是一个操作符,free()是一个函数)释放后,如果程序员没有对其置空或者其他的赋值操作,当后面再次使用该指针,就会使其成为一个野指针。

③指针操作超越变量作用域: 指针指向了一个不属于自己的的地址,该地址可能正在被其他程序使用,使得该指针变量保存了非法地址。即使用或引用返回指向栈内存的指针,而栈内存在函数结束的时候该会被释放,当有其他申请程序空间时,该空间会被分配给其他程序使用,而计算机中的地址是唯一的,因此指针可能指向了不属于自己空间内存,引用时造成野指针;

(4)如何发现该问题 :

  • 指针判空;

(5)如何解决 :

  • 定义指针时,养成习惯给指针初始化为NULL;
  • 在指针解引用之前,先去判断这个指针是不是NuLL;
  • 指针使用完之后,将其赋值为NULL;
  • 在指针使用之前,将其赋值绑定给一个可用地址空间;
  • 任何变量(或指针)在定义时一定记得初始化

6.悬空指针

(1)悬空指针 描述: 悬空指针是已经的指向对象已经被删除、被释放、或该指针没有指向一个明确的地址,那么该指针就变成了了悬空指针;引用悬空指针就会造成野指针。
(2)发生 悬空指针 的场景

例:
int main()
{
	int *p=NULL;
	char c;
	p=&c;//p为悬空指针,因为c没有初始化,地址和值均为随机分配,所以p指向也不明确
}

(3)导致悬空指针的原因:
c语言中的指针可以指向一块内存,如果这块内存稍后被操作系统回收(被释放),但是指针仍然指向这块内存,那么,就会造成 “悬空指针”。
(4)如何解决 :

  • 在释放一块内存时,将指向这块内存的指针变量设置为NULL;
  • 访问指针变量前,先判断是否为NULL;
  • 在堆区分配空间,使用结束free以后,要将指针赋值为NULL(规避悬空指针);
  • 当有多个指针变量都指向同一块内存时,释放这块内存时,需要将所有指针变量的值都置为NULL。但是这种方式开销大,所以通常很少使用。使用频率不是非常高的对象,可以在使用前先根据id等索引查找,如果找不到,则不要使用。如果有使用者时,不能释放这块内存,我们可以使用引用计数,只有当引用计数为0时,才真正释放内存,否则,只是引用计数减1。

7.未初始化的指针变量

(1)未初始化的指针变量 描述:
当使用未初始化的内存指针时,会导致程序无法进行,因为指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用函数时,会使用乱码所指的内存,指针根本就无权访问,导致出错。
(2)发生 未初始化的指针变量 的场景

例:
#include
void getvalue(float x, float y, float* sum); //计算两个浮点数的和
int main()
{
 float a, b;
 float* sum;//定义了一个浮点型的指针变量sum,但未初始化
 scanf_s("%f%f", &a, &b);
 getvalue(a, b, sum);//将未初始化的指针变量穿给被调函数
 return 0;
}
void getvalue(float x, float y, float* sum)//接收了sum的地址,而该地址指向不明确(没有初始化)
{
 float sum1;
 sum1 = x + y;
 sum = &sum1;
 printf("两者之和是%.2fn ", *sum);
}

(3)如何发现该问题 :
检查代码,看是否存在定义了但没有初始化的指针;
(4)如何解决 :

  • 养成良好的编程相习惯,在定义指针或任何变量时,应该对其初始化。
    变量可以初始化为:0,指针可以初始化为:NULL,结构体可以初始化为:{0};

8.引用空指针

(1)空指针 描述: 空指针 是一个特殊的指针值。
空指针 是指可以确保没有向任何一个对象的指针,通常使用宏定义 NULL 来表示空指针常量值。

(2)发生 空指针 的场景

例:

int main()
{
	int *pi=NULL;
	printf("pi的值:%d",pi);
	*pi=5;//对pi进行赋值
	printf("pi的值:%d",pi);
}

(3)如何发现该问题 :

  • 检查代码,查看是否存在指向不明确的指针;

(4)如何解决 :

  • 加一个if语句,对指针进行判空;

9.二次释放

(1)二次释放 描述:
对已经释放过的内存再次或多次重复释放时:
①空指针可以二次释放;
②非空指针不可以二次释放,二次释放会造成野指针;
原因:二次释放会导致野指针,因为第一次free以后,p已经被释放,第二次释放时,会先找到p(即访问它)再释放,此时p已经不在了,继续访问(释放)会造成野指针。

(2)发生二次释放 的场景

void function(char *str,int n)
{
char *p=NULL;//定义一个指针并初始化
p=(char *)malloc(sizeof(char)*n);//给p分配一个堆区的空间,空间大小为sizeof(char)*n
memset(p,-1,sizeof(char)*n*n);//给堆区空间p,初始化,sizeof(char)*n*n个字节全部赋初值为-1;(越界,只有n个字节空间,你却非要给n*n个字节空间赋值)
}

int main()
{
	int n=0;
	char s[100]={0};
	char *q=NULL;
	q=function(s,n);
	free(p);
	//p=NULL;
	free(p);//二次释放
	return 0;
}

代码解释:

  • 当把p=NULL注释以后,程序编译时会通过,但是运行时会报错;
  • 当不把p=NULL注释时,编译好运行,都不报错;

(3)如何解决 :

  • 养成良好的编程习惯,当对堆区申请的空间free之后,应该及时将指针赋值为NULL,避免二次释放带来的影响;
  • malloc和free个数应该保持平衡;

二、C语言 (静态)数据区 可能会出现哪些内存问题? 1.多重定义

函数和定义时已初始化的全局变量是强符号;未初始化的全局变量是弱符号。多重定义的符号只允许最多一个强符号。

Unix 链接器使用以下规则来处理多重定义的符号:
1.不允许有多个强符号。在被多个源文件包含的头文件内定义的全局变量会被定义多次(预处理阶段会将头文件内容开在源文 中),若在定义时显式地赋值(初始化),则会违反此
规则。
2.若存在一个强符号和多个弱符号,则选择强符号。
3.若存在多个弱符号,则从这些弱符号中任选一个。

当不同文件内定义同名(即便类型和含义不同)的全局变量时,该变量共享同一块内存(地址相同)。若变量定义时均初始化,则会产生重定义( multiple definition )的链接错误;若某处变量定义时未初始化,则无链接错误,仅在因类型不同而大小不同时可能产生符号大小变化( size of symbol ` XXX ’ changed )的编译警告。在最坏情况下,编译链接正常,但不同文件对同名全局变量读写时相互影响,引发非常诡异的问题。这种风险在使用无法接触源码的第三方库时尤为突出。

// test . C 
 int gdwCount =0;
 int GetCount ( void )
 return gdvCount ;
// main . C 
 extern int GetCount ( void );
 int gdwCount ;
 int main ( void )
 gdwCount =10;
 printf (" GetCount =% d  n ", GetCountO ); return 0;

编者期望函数 GetCount 的返回值打印出来是0,但其实是10。若将 main . c 中的 int gdwCount 语句改为 int gdwCount =0,编译链接时就会报告 multiple definition of ' gdwCount 的错误。因此尽量不要依赖和假设这种符号规则。
所以在编码过程中尽量避免使用全局变量。若确有必要,应采用静态全局变量无强弱之分,
且不会和其他全局符号产生冲突),并封装访问函数供外部文件调用

2.内存越界

内存越界访问分为读越界和写越界。

  1. 读越界 表示读取不属于自己的数据,如读取的字节数多于分配给目标变量的字节数。若所读的内存地址无效,则程序立即崩溃;若所读的内存地址有效,则可读到随机的数据,导致不可预料的后果。
  2. 写越界: 亦称“缓冲区溢出”,所写入的数据对目标地址而言也是随机的,因此同样导致不可预料的后果。内存越界访问会严重影响程序的稳定性,其危险在于后果和症状的随机性。这种随机性使得故障现象和本源看似无关,给排障带来极大的困难。数据区内存越界主要指读写某一数据区内存(如全局或静态变量、数组或结构体等)时,超出该内存区域的合法范围。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/869021.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号