||
一、全局变量和局部变量
全局变量和局部变量的区别在于作用域的不同。此外还有静态全局变量和静态局部变量。
全局变量作用域为全局,在一个源文件中定义,其他的源文件也可以应用。在其他的源文件中使用extern加以声明;
静态全局变量作用域为该源文件,只作用在声明它的源文件中,通过static声明,这样即使在其他的源文件中有相同名称的变量也不相同;
局部变量作用域为函数内部。当函数被调用时,为其分配空间,函数调用完成后收回内存,销毁变量;
局部静态变量作用域为局部,只被初始化一次,但是在后面它一直存在。即使函数调用完成后也一直存在。
全局变量和局部变量都是要分配内存的,它们的区别只是在于作用域不同。
在C中,全局变量、静态全局变量及静态局部变量都在静态存储区,由于这些变量的生存周期较长,占有较多的内存但是由于已经分配好了内存,因此速度较快,而局部变量则在栈中分配空间。在KeilC51中这是不同的。
二、C51中的全局变量和局部变量
在51中程序ROM和数据RAM是严格分开的,特殊功能寄存器与片内数据存储器统一编址。这与其他一般的微机不同。51中内部的RAM有256字节,外部可寻址64KB,对于256字节,其中前128字节(00-7FH)又分为三部分:通用寄存器组、可位寻址区、用户RAM区;高128字节(7F-FF)为SFR。上电复位后堆栈指针指向07H,在通用寄存器区,此时对战区占用1,2,3组寄存器,但是用户可自行将sp设置在30-7F。
C51编译器通过将变量定义为不同的类型,来区分不同的存储区,常用的变量类型有:
data:片内RAM的低128字节
bdata:可位寻址的片内RAM
以上两种类型可以快速的存取数据,常用来放临时性的传递变量或使用频率较高的变量。
idata:整个片内RAM。
xdata:片外存储区(64KB),由于在对片外存储区操作时,需要先将数据移到片内,进行处理后再存储到片外,因此常用来存放不常用的变量,或收集待处理的数据,或存放要被发往另一台计算机的数据。
pdata:属于xdata类型,由于它的高字节保存在P2口中,只能寻址256字节。
code:ROM内,数据不会丢失。
此外,C51还有三种存储模式:SMALL, COMPACT, LARGE
SMALL模式下,如果不做特别说明,参数及局部变量默认为data型,放在片内RAM128字节内,访问迅速。由于内部的RAM有限,如果变量过多,会导致频繁的使用寄存器,而使代码变的冗长。此时栈也在片内的RAM,栈长很关键,因为栈长依赖于不同函数的嵌套层数。
COMPACT:不做特别说明,参数及局部变量默认为pdata,栈空间在内部RAM。
LARGE:参数及局部变量默认为xdata,使用DPTR来寻址。访问效率低,此外这种数据指针不能对称操作。
全局变量会根据定义的类型或者存储的模式分配在相应的存储区内,有固定的地址,如果全局变量过多则会导致占用太多内存,处理速度变慢。
三、共享和覆盖
由于51的存储区有限,因此变有了覆盖和共享的概念。
共享:有共享变量和共享函数,共享是针对全局变量或静态变量而言的,对全局变量定义后就对其分配了内存,在任何函数或者程序中都可以共享该变量的内存,在其他的文件中也可以通过声明extern来实现共享。共享函数也是类同。
覆盖:如果一个程序不再被调用,也不由其他的程序调用,在其他的程序运行之前程序也不在运行,那么这个程序的变量可以放在与其他的程序完全相同的RAM空间,这就是覆盖。
对于函数之间的覆盖在Keil中有一段描述:
The system the linker uses to determine which arguments (or parameters) and variables may be overlaid is quite sophisticated. It begins when the compiler generates the object code for a .
The compiler stores all parameters and local variables in overlayable bit, data, pdata, or xdata segments. The segment names generated by the compiler for Parameters and Local Variables are well-defined. They are used by the compiler to access parameters and local variables.
As the linker resolves references between s, it builds a call tree based on where those references appear. For instance, if _a calls _b, the compiler inserts a reference to _b in the object code generated for _a. When the linker resolves this reference, it inserts the address of _b and adds a call from _a to _b in the call tree.
The local variables and parameters of _a are overlaid with the variables and parameters of _b only under the following conditions:
在函数中定义的动态局部变量可以被覆盖,一个函数说明的变量在下一次进入函数时不同,即函数调用时会发生变化(函数调用时才为局部变量分配空间,因此每次调用分配的地址可能不同)。有一些编译器通常把局部变量放在堆栈上,这样运行起来位置不固定,栈操作不方便,而在51的编译器中则监视函数调用的嵌套顺序,把几个函数的变量放在同样固定的位置。在51编译器中连接器会搜索所有函数中局部变量占用存储区间最多的函数,然后已这个函数的局部变量的占用的空间的多少开辟一片空间,其他函数的局部变量也放在该空间中,同时实现了变量的覆盖(无相互调用)与地址的共享。例如函数A占10个字节,函数B占20个字节,函数C占15个字节,如果它们之间没有相互调用则仅需20个字节就可以满足45个字节的变量需要。
C51中根据变量定义是的数据类型在相应的存储区内为变量分配内存,在内部整个RAM区中,先为定义在该端的全局变量分配地址,然后是程序中所有函数的参数和局部变量的覆盖区(内部RAM存取迅速)。以上两部分内存都是上面所说的C中的静态存储区(段)。接下来就是系统的堆栈,栈底有?STACK自动生成,栈顶在0xFF。
正是由于所有函数的参数和局部变量的共享一个覆盖区,函数没有相互的调用时,在执行一个函数时,会将另一个函数的变量的存储区覆盖。如果函数有调用,那么不会覆盖原来函数的局部变量的区间,但如果函数的嵌套(递归)层数太多,所有的变量的内存大于了覆盖区时,一个函数的内部的变量可能会被新调用的函数冲掉,再返回该函数时,无法找到相应变量的内存,也就无法找到该变量的值。通过声明为可重入函数,让参数和变量放在堆栈中。
对于覆盖,大多人认为是为了节省内存,但网上有另一种说法,个人觉得很有道理,引用如下:
一般的C编译器(或者更确切点地说:基于一般的处理器上的C编译器),其函数的局部变量是存放于堆栈中的,而C51是存放于一个可覆盖的(数据)段中的.
至于C51这样做的原因,不是象有些人说的那样,为了节约内存.事实上,这样做根本节约不了内存.理由如下:
1) 如果一个函数func1调用另一个函数func2,那么func1,func2的局部变量根本就不能是同一块内存.C51还是要为他们分配不同的RAM.这跟使用堆栈相比,节约不了内存.
2) 如果func1,func2不是在一个调用链上,那么C51可以通过覆盖分析,让它们的局部变量共享相同的内存地址.但这样也不会比使用堆栈节约内存.因为既然它们是在不同的调用链上,那么当其中一个函数运行时,那么另外一个函数必然不在其生命期内,它所占用的堆栈也已释放,归还给系统.
真实的原因(C51使用覆盖段作为局部变量的存放地的原因)是:
51的指令系统没有一个有效的相对寻址(变址寻址)的指令,这使得使用堆栈作为变量的代价太过昂贵.
使用堆栈存放变量的一般做法是:
进入函数时,保留一段堆栈空间,作为变量的存放空间,用一个可作为基址寻址的寄存器指向这个空间,通过加上一个偏移量,就可以访问不同的变量了.
例如: MOV EAX, [EBP + 14];X86指令
LDR R0, [R12, #14];ARM指令
都可以很好的解决这个问题.
但51缺少这样的指令.
*其实,51中还是有2个可变址寻址的指令的,但不适合访问堆栈的局部变量这样的场合.
MOVC A, @A+DPTR
MOVC A, @A+PC
所以,C51有个特别的关键字: reentrant 用来解决函数重入的问题。