关于内存问题和使用内存建议

 

内存的问题是C++的重要问题,解决C++使用过程中的内存问题,就可以解决使用内存异常(bug)和内存泄露(无法抵制压力测试)。内存问题难,关键在于我们对于内存有太多的未知数。规范的意义,在于把程序运行时内存的不确定性变为可知的、可确定性,这样就可以达到有效避免错误。

所有的数据(数据段)和执行指令(程序段)都放在内存中。下面解释一下,我理解的程序的运行过程(仅作参考,因为我还不熟悉VC的编译和微软操作系统的管理机制,正确性未知),以帮助大家了解内存问题。

编译过程:编译过程是将程序员写的C++程序生成计算机可识别的二进制代码段,它是一段代码,而且还是逻辑的代码段。因为,程序里的地址都是相对的,是以0作为首地址的。在装入内存的时候,计算机开辟出跟代码段一样大的一块内存,存放代码,这段内存叫做栈,并且OS记录内存段的首地址。用于计算数据和指令的实际内存地址。其结构可用下面的逻辑图来表示:

程序段

 

函数:main

int  a (逻辑地址为0x000032,物理地址就是这个逻辑地址+程序段开头);

 
0x0002568

 

 

 

 

      在这个代码段中,存放着类定义,函数定义和实现,全局变量等。

类定义说明了类中数据成员、类型和函数列表,寒暑列表指向了一个个的成员函数。可理解为下图

 

 

 

 

 


函数定义就是一个可执行的代码段,结构简单:用汇编来表示如下

:push 记录当前运行状态的IP等

: push

: push

:move:向函数里传递参数

:move

…..执行代码

Pop:取出函数执行前的运行状态,然后跳过去

Pop:

接着往下执行。

下面说明变量的定义,这个很简单,关键是为变量分配内存而已。

比如:

int a;

d

40个字节

 

 

 

 

b

8个字节

 

 

a

4个字节

 
float b,

int d[10];

装到内存后,就是

四个字节:a

八个字节:b

10*4个字节 d:

我发现数据在内存中都是倒着的。

由于静态数组的分配是在程序装载时进行的,所以程序数组的个数不能定义为变量,因为变量是在运行时,才知道其值的;因此,大家在定义变量的静态数组时,是无法编译通过的。

 

运行过程:

一个程序要想得到执行,首先要由OS装入内存,然后就是OS系统找到程序(Exe)的入口函数运行地址Main,这个函数在C里很直观,在VC中发生了变异,且被隐藏了起来。从管理角度,这个函数,实现了管理员的功能,它把一个个的类实现的任务,组织起来,完成用户的要求。那就些具体类就是我们这些开发人员,各司其职,完成自己能力可以完成的任务。

如果说Exe的执行主要是由OS负责的,那么DLL的执行则是由Exe负责的。应用程序通过装入DLL(包括类定义等),执行DLL中的全局变量构造,然后就把DLL准备好了,供Exe调用,我们调用的输出函数,实际上就相当于Exe中的main,只不过exe只有一个入口,而dll可以有多个入口而已。

 

由上面可以看到,程序在内存中大小是固定的。那么动态分配又是怎么一回事呢?以上的程序段,只有定义和静态数据的分配,他们都是在栈中进行的。程序运行起来之后,比如要分配一个类对象,那么操作系统就会到上面的代码中查找类定义,有什么函数,有什么成员变量,然后根据成员变量的大小在堆里分配内存,并将其内存其实地址的值赋值为函数列表地址。

栈:                堆:

 

 

 

 

 

 

 

 


由上图可以看出:堆只起了存放数据的左右,堆得地址是由栈中的p来保存的,且函数都在栈里。

以上的说明,基本描述了程序的运行过程。下面专门描述内存错误出现的时机并对其原因进行解释。

1:在模块之间传递类的指针时,如果两个类不完全一致,如果刚好在A中使用了A中具有但B中没有的函数,这个指针又是由B传给A的,那么就会出现错误。由于两个类位于不同的模块中,并当作两个不同的类定义,当用A的规则来对B解码时,如果定义不一致,肯定会出现问题,而编译器却无法发现这个问题,从而留下了问题的隐患。这个我们制定长报文传输服务标准是一脉相承的,目的是编码端和解码端按照统一的规则解码。

2.由于内存对所有的程序都是公开的,属于全民所有制的社会主义体制。不同人编写的代码,函数往往存在一定的差异,这是不可消除的。也许这就是自描述语言XML产生的一个理由吧,它通过自描述的方式,消除了格式化语言的弊端。鉴于这种情况,我们对于内存的情况就处于一种未知状态,比如一个内存分配后,是否是空,函数的执行结果是否对内存进行了赋值操作。等等着一些,我们都是不知道的。但是,我们清楚我们自己的行为,我们知道自己要干什么。我们不能假定,别人如何干,我们必须自己动手来这样干。

A:在分配内存之前对指针进行初始化:这主要是因为我们在调用分配后,当分配不成功时,是不是会为我们=NULL;如果没有为我们赋空,而此时指针又指向一个无效的内存,那么问题立刻就出来了。这一点特别应用在通过函数返回指针之前。

B:在调用分配之后,立刻对指针进行判断,然后才可以使用,这一点比较简单,因为我们不知道内存分配是否成功,我们不能假定一定能成功。

C:所谓的异常,是由于使用了无效的内存,那么有效的内存一直得不到释放,就形成了内存的泄漏,内存的泄漏可能不会立刻影响你程序的运行,但是它经不起压力的测试,有人说:window操作系统支持虚拟内存,就是把硬盘的一部分当作内存使用,因而永远都不会出现内存不够用,但是,我发现好像不完全是这样,虚拟内存的大小也是受硬盘所限的,即使这些不考虑,硬盘的访问速度和内存的访问速度差别有多大,不言而喻。

泄漏出现的可能性很大:

1.1         是由于根本没有delete

1.2         是由于逻辑的问题,没有保证在所有的逻辑路径下都能执行到delete

1.3         没有加[],如果分配的是一个数组,如下

double *p=NULL;

p = new double [10];

那么删除的时候,也要以数组的条件删除;

如下:

delete [] p;//正确

如果

delete p;

那么就只删除了p的第一个地址,泄漏了9*sizeof(double)个字节。

对于二维,甚至更高维要循环删除。

1.  4为避免野指针(指向无效内存的指针),我们在调用释放后,应立即=NULL。

D:内存分为两种,一种是没有管理器的,一种是有管理器的(一般内部通过链表来管理数据),

没有管理器的俗称没人管单身,占有大多数,一般情况下是这样的,这些内存的释放需要自己动手来完成。

有管理器的,占很少的一部分,一般情况下,你分配的内存都属于一类,而且内存快数量多,为了管理方便,就提供了管理器,由管理器来统一管理。由于他们属于同一类,如ITEM,管理器是知道内存的结构的,因此它有管理的能力。BCG里有好多类都实现了管理。对于大家采用的CarrAY<*,*〉CLIst<*,*>这些情况下,你还是自己来管理吧,因为Carray不知道指针的内存结构,它没有能力合理的删除内存呀。正所谓巧妇难为无米之璀;如果大家实在不知道是否要删除,那么你可以删除以下,如果出错了,那肯定是有人帮你删了。恭喜你:)

E:bug本来是调试(debug)版本软件针对使用逻辑错误的提示信息。通过bug查找使用逻辑错误,一般认为,逻辑正确之后,就不会出现“传入参数=NULL,无效,不是窗口等等”,Assert也就弹不出bug了。这种bug,在程序的内部,进行了Assert而没有进行throw所以我们的try根本抓不住他,只有那种,不是逻辑错误的,我们无法通过调试来发现的错误,才会throw,那样,我们才能抓住他try ..catch…

鉴于,逻辑错误调试的困难,而且,我们的软件也不能成为release.那么我们没有办法,我们只能将Assert检查的内容拿到函数的外面,让我们先进行检查,以逃过Assert 的追剿。

F:对于某些函数和类中定义的操作符,一般都有ASSERT.有时,我们不知道里面的ASSERT检查了什么内容,那么就请你来调试看看吧,这样,你就知道该检查什么了!!只要你做够熟练,你直接可以通过ASSERT对话框来判断错误的来源。那么你就是调试高手了。Congretulation On your successs!

G:请记住我的一句话,栈里面的内存是自动消亡的,堆里面的内存是永远不会自己消亡的。所以请大家要学会了解那些内存是在堆里面的,如果没有管理器来替你维护,那么你就要自己操作了。

H:还有一种错误,也可以归类到内存错误里面,就是函数返回只在函数里有效地内存数据,这些内存在函数返回之后,就会被释放,那么外部的变量,就无法使用这段内存了,为什么呢,因为返回的内存是在栈中,它在函数返回后,就无效了,所以要想使函数返回后内存有效,这段内存必须是在堆里的。这就是函数返回内存的原则。

I:当我们都知道释放内存的时候,我们还需要保证,我们是否释放干净了内存,这也是很重要的一面,经验证明,我们经常会犯这种错误。

J:为了合理管理内存,一个简单的原则就是谁分配,谁释放,尤其在多个类的情况下,那各类分配的,就有哪个类来负责释放,正是各司所职的道理,用内存传递除外。

K:有时候,我们采用消息传递内存数据:如果我们传递的是指针(内存),那么就要通过SendMessage保证接收消息方能够读取完内存后,发送方才可以释放内存,如果不是内存指针,可以使用PostMessage.其实,有一个消息,为我们考虑了这些那就是WM_COPYDATA 它会在剪切版分配一个临时内存,用来保证数据传输的正确性。

L:线程管理:线程管理造成的内存泄露往往会被大家忽视,因为大家以为线程运行完毕后,就会自己释放资源,由于内存泄露的隐蔽性,我也一直这样认为,直到前不久关注多核时代到来引起并行开发热潮时才发现这个问题。线程运行完成后,但是线程的资源还没有释放!

如果不释放,你看看等你开辟了很多线程后,程序运行有多慢:

这就要求我们在判断线程运行完毕后,要释放线程独有资源。

GetExitCodeThread( pHandle->m_ThreadHandle, &dwExitCode )
                          //获得线程pHandle的当前状态
                          if ( dwExitCode != STILL_ACTIVE )
                          //如果已完成
                          {
                                   //释放当前句柄
                                   CloseHandle( pHandle->m_ThreadHandle ) );
                                   delete pHandle;
                                   pHandle = (CHandle *)NULL;
                          }

 

综上所述:内存问题极其复杂,需要每个人去不断积累经验,共同学习,共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值