内存处理的常见问题小结

1、内存泄露

在堆上对内存进行申请的时候,一定要提高警觉程度。 动态内存的申请与释放必须配对,程序中malloc 与free 、new与delete的一定要成对。千万防止光申请不释放的代码出现。

如果发生这种错误,函数每被调用一次就丢失一块内存。

我们来看一下如何防止:

我们将指针放进对象的内部并且让对象管理指针是最简单的防止内存泄露的方法。应把new返回的指针存储在对象的成员变量里,当需要时该对象的析构函数便能够释放掉该指针。

这种方法的优点就是将指针完全交给对象去管理,不需要担心在操作指针时出现内存泄露等问题。 但是指针仅有初步创建和释放的语义,通过把指针放进对象里可以保证该析构函数的执行及正确释放分配的内存。

C/C++中,内存管理器不会自动回收不再使用的内存,如果忘记释放不再使用的内存,这些内存就允许被重用,此时就会造成内存泄露。

 

2、内存越界

何谓内存访问越界,简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。

读越界:读了不属于自己的数据。 如果读的内存地址是无效的就会崩溃。

写越界:也叫做 缓冲区溢出, 在写入的数据对别人来说是随机的,这样也会发生错误。

解决:遇到这种问题,首先你得找到哪里有内存访问越界,而一个比较麻烦的问题在于,出现错误的地方往往不是真正内存越界的地方。对于内存访问越界,往往需要进行仔细得代码走查、单步跟踪并观察变量以及在调试环境得帮助下对变量进行写入跟踪

char b[16]="abcd";
memset(b, 0,32);//越界

 

3、野指针

使用free或delete释放了内存后,没有将指针设置为NULL,也就是指向不可用内存区域的指针。

野指针”不是NULL指针,是指向“垃圾”内存的指针,即是指向不可用内存区域的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。野指针的成因主要有三种:

 

1、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

char *p =NULL;  
char *str = (char *)malloc(100);

2、指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

3、指针操作超越了变量的作用范围。这种情况让人防不胜防。

class A    

{     

public:     

 void Func(void)         

 {           

       cout << "Func of class A" << endl;         

 }    

};            

   void Test(void)     

  {              

           A *p;            

       {              

             A a;                  

            p = &a;         //    注意    a    的生命期         

        }       

    p->Func();             //    p是“野指针”    

 } 

 在Test执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了野指针。

4.返回指向临时变量的指针或引用

栈里面的变量是临时的,当前函数执行完成时,相关的临时变量和参数都被清除了。不允许把只想这些临时变量的指针返回给调用者,因为这样的指针变成了野指针,指向的内存是不可用的内存区域的指针,导致指向的数据是随机的,会给程序带来严重后果。

 

 

4、分配不成功,使用该内存

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

 

5、分配成功,但未初始化

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。 内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。

 

6、试图修改常量

在函数参数前加上const修饰符,只是给编译器做类型检查用的。编译器禁止修改这样的变量,但是并不强制,我们完全可以使用强制类型转换来处理,一般不会出错。然后,我们的全局常量和字符串使用强制类型转换来处理在运行时仍然会出错。因为他们是放在“rodata”里面的,而“rodata”内存页面时不允许修改的。

 

7、对同一指针删除两次

 A* a = new A();

A* b = a;

delete a;

delete b;上面的代码就出现了两次删除同一个指针的问题了。 我们第一次delete会安全的释放掉*a,并且由a所指向的内存会被安全地返回到堆上。 而我们并没有返回该指针new的操作就把相同的指针第二次传递到delete()函数中,而且把之前*a中的对象传递给析构函数,然后把由a指向的内存第二次传递给该堆,这样的操作是灾难性的,因为这可能会破坏该堆及其自由内存表。当我们不再使用指针并试图把该指针删除时,一定要慎重地考虑如何删除,而且一个指针只能删除一次。

 

8、p指向数组,调用delete p

当指针p指向数组时,不允许执行delete p操作。这是因为当p指向某种内置类型的数组时不能省略delete p[]中的[]。在delete p中,底层解除分配原语是operator delete (void*),而delete [] p底层解除分配原语是operator delete[] (void*)。

 

9.栈溢出。

我们在前面关于堆栈的一节讲过,在PC上,普通线程的栈空间也有十几M,通常够用了,定义大一点的临时变量不会有什么问题。而在一些嵌入式中,线程的栈空间可能只5K大小,甚至小到只有256个字节。在这样的平台中,栈溢出是最常用的错误之一。在编程时应该清楚自己平台的限制,避免栈溢出的可能。

 

10.误用sizeof。

在传递数组参数时,程序会默认将数组转换为指向数组首元素的指针传入函数中,在函数里用sizeof是无法取得数组的大小的,得到永远是指针的大小(4字节)。从下面这个例子可以看出:

void test(char str[20])

   printf("%s:size=%d/n", __func__, sizeof(str));

}  

 int main(int argc, char* argv[])

{    char str[20]  = {0};   

     test(str);    

     printf("%s:size=%d/n", __func__, sizeof(str));

    return 0;

 

11.多线程共享变量没有用valotile修饰。

我们讲了valotile的作用,它告诉编译器,不要把变量优化到寄存器中。在开发多线程并发的软件时,如果这些线程共享一些全局变量,这些全局变量最好用valotile修饰。这样可以避免因为编译器优化而引起的错误,这样的错误非常难查。 


12.深拷贝

如果类中有指向动态分配内存的指针成员变量的话,复制对象时,拷贝指针拷贝的不是指针值,而拷贝的是那个动态分配的内存上的内容。


13.指针赋值

指针赋值的时候,先判断原来指针是否有值,如果有且指向是动态分配的内存时,先释放原来的内存。再被赋值。对于赋值,要考虑是拷贝地址值还是拷贝内存上的内容。


14.指针容器

std::vector<CType>这个错误也是同志们经常犯的,其实很多时候若是简单结构、简单类,你直接用std::vector<CType>就好了,能不用std::vector<CType*>就尽量不用,因为确实很容易忘了vector中原来还存放了要释放的内存的指针,而且在clear或是删除一个元素时都得记起来释放指针指向的内容。

这个很像小时候家里收邮包,邮递员不将邮包送到家里来,也许因为太沉了吧,只是给张包裹单要自己取领。今天忙,往抽屉里一扔,然后就忘了,下次又来一包裹单,又往抽屉里一扔又忘了(指针压入vector),若你不小心将包裹单(指针)弄丢了,你自己都不知道有这么回事(忘了释放内存)。但现在就好多了,快递公司包裹直接送到你手上。


15.扫尾函数

有些类型对象如CDialog,CWindow,CFile,CImage等需要在Delete前做Close、Release、Destroy等操作的,Delete时检查是否已经调用了相应的扫尾函数。
这个要具体情况具体分析了,比如CDialog的子类销毁时往往需要先调用OnDestroy或是DestroyWindow,不然就可能会存在资源泄漏的问题。

16.公共模块/第三方库

公共模块一般有init()、open()和release()、terminate()、close()两种类型的函数,不要忘记扫尾类型函数的调用。
在我们这个软件项目中就有用到一个第三方的Av.dll,主要是进行视频编解码方面的库,这个库需要进行初始化才能用,同时也提供了使用完关闭的方法。当时一位同志就忘了调用扫尾函数导致了大量的内存泄漏。这个就要求我们使用第三方库时一定要看仔细使用说明,不要一味冒进。

17.虚析构函数
一个类的指针被向上引用,作为基类的指针来使用的时候,把析构函数写成虚函数。这样做是为了当用一个基类的指针类型来删除一个派生类的对象时,派生类的析构函数会被调用。


代码示例:
struct ST_Info
{
      int iWeight;
      char strName[128]
}
class CFruit
{

public:

  virtual ~CFruit();
};
 
class CApple:public CFruit
{
public:
      std::vector< ST_Info> m_vecInfo;
}
 
CFruit * GetApple()
{
   CApple *ptrApple = new CApple();
   ST_Info st_Info = {9, “Apple1”};
   ptrApple->m_vecInfo.push_back(st_Info);
 
   return ptrApple; 
}
void main(int argc, char**argv)
{
   CFruit *ptrFruit = GetApple();
  
   delete ptrFruit;
   ptrFruit = NULL;
}
 
上面的代码如果CFruit的析构函数不定义成虚析构函数就会产生内存泄漏了, ptrApple->m_vecInfo中存放的内存将全部泄漏掉,一个能为delete时认为这是一个CFruit *的指针,不会去释放ptrApple->m_vecInfo中元素对应的内存。

 
18.线程的安全退出,user-interface thread安全退出

和窗口关联的user-interface thread 必须处理WM_DESTROY消息,建议定义一个OnDestroy()函数,该函数调用PostQuitMessage(0)的方法让user-interface thread安全退出,防止线程不安全退出导致内存泄漏。
线程进行安全退出,防止非正常退出的内存泄漏问题。
例子:
LRESULT CMsgReflect::OnDestroy(HWND hWindow, UINT uiMessage, WPARAM uiParam, LPARAM ulParam)
{
         PostQuitMessage(0);
         return 0;
}


19.内存动态分配后,在各个分支路径均要考虑是否要释放掉
下面的代码就没有考虑到执行到continue时的情况会产生内存泄漏。
                for (std::vector<TeamInfo>::iterator it = e.teamlist.begin(); it != e.teamlist.end(); it++)
                {
                        FriendGroupData *pGroup=new FriendGroupData;
                        if(it->unTeamID==DEFAULT_FRIEND_GROUP_ID)
                            continue;
                       ….
                       delete pGroup;
}

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值