计算机程序运行时需要保存和处理数据,而所有的运行时使用的数据都是保存在内存当中的。让我们一起看一下嵌入式C程序在运行时是如何使用内存的。
一个嵌入式程序中使用的内存都是通过变量来访问的(可能是直接访问,也可能是间接访问),我们看看一个程序中都有哪些种类的变量,它决定着程序运行时的内存使用方式。
类型一:全局变量、静态变量、静态成员变量和静态局部变量
这些变量的特点是它们从程序运行的一开始就存在着,可以在程序运行过程中的任意时刻,从任意的位置进行访问,它们将一直生存到程序结束时为止。
这一类变量是在一块固定的全局内存区(或称为静态内存区)中分配的,这块空间由程序在开始运行时保留,保存在这一空间中的变量也在运行开始是创建并初始化。所有这些变量都一直保存在这块区域中,并在整个程序的运行过程中都是存在的。因此,程序可以随时访问。
类型二:局部变量
局部变量指那些在函数中声明和使用的变量,它们伴随着函数的执行而生,伴随着函数的返回而死,它们的生存历程就是所在函数的执行过程。它们只能在所在的函数中访问,在函数之外没有意义。它们的特点是,生存时间是完全确定的,生命周期由函数调用机制处理。
这类变量通常在一块称为栈(stack)的内存空间中分配。stack所需要的空间通常是在程序运行开始是预先分配的,但是,栈空间的使用并不是固定的。在我们调用一个函数时,程序会为我们在栈上面分配一块这个函数需要的内存空间,这块空间称为栈帧(stack frame),被调函数所需要的所有局部变量都在函数开始执行之前在这块空间上创建,在函数执行结束时,栈帧将被撤销,函数中的局部变量就不再存在了。(所以,如果函数返回一个执行局部变量的指针,该指针指向的内存在被访问时会出现非法内存访问错误)栈中的数据可以在当前函数和所有被当前函数调用的函数中使用,因为在函数调用其他函数时不会撤销自己的栈帧,而是在自身栈帧之上创建为被调函数创建一个新的栈帧。
要注意,嵌入式系统程序预先分配的栈空间是有限的(这个值可以在编译时确定),因此,程序的函数调用就不会是无止境的,如果函数调用过深,或者函数局部变量过多(比如:int a[100000000000000] :P)将会导致程序耗尽栈内空间而报错(stack overflow)。
类型三:动态分配的内存
动态分配的内存是指那些用malloc(realloc,...)和new、new[]分配的内存,它们在程序中是不能够直接访问的,必须通过指针,用*p或者p[10]再或者p->i的方式间接访问,而它们的释放必须由程序员通过free或delete、delete[]主动进行(除非你的语言的runtime可以向java那样自动进行内存回收),它们的生存周期可以由程序员随心所欲的控制,要长即长,说短就短。不过也可能由于程序员忘记回收而使无用的数据一直占据着内存。
这类变量在另一块内存中分配,这块内存称为堆(heap)。堆中的内存可以根据实际需要在程序的运行过程中分配(malloc/new)和释放(free/delete),在堆中分配的数据将一直存在下去,直到明确地被释放或者保存到程序运行结束时。堆中的内存被释放之前它可以在程序中任意访问,并被所有函数共享访问(注意和栈中变量的区别)。堆中内存的分配通常是由操作系统的API进行的(比如windows中的GlobalAlloc等),而C/C++中使用malloc和new来调用API进行内存分配。
动态内存分配永远是同指针联系在一起的,因为堆中分配的内存不能直接在程序中访问,必须通过指针变量进行间接访问。对堆中内存的分配操作将会返回分配好的内存的一个指针,我们通过这个值针对分配的空间进行操作。指针是一个变量,它保存着一个内存地址,使我们可以根据这个地址访问这个地址单元上所保存的数据。
程序对内存的动态分配能力是程序设计灵活性的一个象征(要知道,最早的高级语言入Fortan是不能动态分配内存的)。而对于是用指针通过手动内存管理的C/C++来说,灵活意味着风险。指针的使用可能会缠身很多错误,现在让我们一一道来。
1,非法访问
指针中保存一块内存单元的地址,可是它却不能保证这块地址一定是我们可以访问的。这块地址可能是一块不属于你的内存,如果,你强行访问那么轻则产生保护性错误,使你的程序崩溃;重则破坏系统的数据结构是操作系统崩溃。
2,访问越界
我们可以使用指针分配一整块内存供程序使用,可以程序并不会监督你的访问范围是不是超出了你分配的大小,一切都要你自己负责。比如:
char * s= new char[100];
*(s+100) = 'h'; //Boom!!!!!
3,内存泄漏
我们一旦在堆中分配了内存他就会一直存在,直到我们释放。而如果我们由于某种原因,在释放之前就失去了对所分配的内存的指针,那么这块内存将变成一只断了线的风筝,永远占据着内存空间。而如果这种丢失经常而且持续性的发生,我们的程序就会像漏水的桶一样,最终让无用数据耗光所有的内存空间。下面给出一个例子,看看大家能不能知道内存是怎样溜掉的。
bool m(){
try{
Something * s = new Something();
//do something...
delete s;
}
catch(int errCode){
return false;
}
return true;
}
4,错误释放
和内存泄漏相对,太过积极的释放内存也会产生错误。首先,不是任意的内存都是可以释放的,必须要保证指针指向的是由malloc/new分配的内存,而且指向的分配到的内存的开始位置,否则结果就和非法访问一样。同样,对已经是放过的内存再次调用free和delete也不会得到什么好下场。
由于,C++中规定delete 0不会产生任何错误,所以,在每次调用delete之后将指针设为0是一个不错的习惯,值得发扬。 |