📄 第2章 变量和数据存储.txt
字号:
C语言编程常见问题解答
发表日期:2003年9月21日 作者:C语言之家整理 已经有7602位读者读过此文
第2章 变量和数据存储
C语言的强大功能之一是可以灵活地定义数据的存储方式。C语言从两个方面控制变量的性质:作用域(scope)和生存期(lifetime)。作用域是指可以存取变量的代码范围,生存期是指可以存取变量的时间范围。
作用域有三种:
1. extern(外部的) 这是在函数外部定义的变量的缺省存储方式。extern变量的作用域是整个程序。
2.static(静态的) 在函数外部说明为static的变量的作用域为从定义点到该文件尾部;在函数内部说明为static的变量的作用域为从定义点到该局部程序块尾部。
3.auto(自动的) 这是在函数内部说明的变量的缺省存储方式。auto变量的作用域为从定义点到该局部程序块尾部。
变量的生存期也有三种,但它们不象作用域那样有预定义的关键字名称。第一种是extern和static变量的生存期,它从main()函数被调用之前开始,到程序退出时为止。第二种是函数参数和auto变量的生存期,它从函数调用时开始,到函数返回时为止。第三种是动态分配的数据的生存期,它从程序调用malloc()或calloc()为数据分配存储空间时开始,到程序调用free()或程序退出时为止。
2.1 变量存储在内存(memory)中的什么地方?
变量可以存储在内存中的不同地方,这依赖于它们的生存期。在函数外部定义的变量(全局变量或静态外部变量)和在函数内部定义的static变量,其生存期就是程序运行的全过程,这些变量被存储在数据段(datasegment)中。数据段是在内存中为这些变量留出的一段大小固定的空间,它分为两部分,一部分用来存放初始化变量,另一部分用来存放未初始化变量。
在函数内部定义的auto变量(没有用关键字static定义的变量)的生存期从程序开始执行其所在的程序块代码时开始,到程序离开该程序块时为止。作为函数参数的变量只在调用该函数期间存在。这些变量被存储在栈(stack)中。栈是内存中的一段空间,开始很小,以后逐渐自动增大,直到达到某个预定义的界限。在象DOS这样的没有虚拟内存(virtual memory)的系统中,这个界限由系统决定,并且通常非常大,因此程序员不必担心用尽栈空间。关于虚拟内存 的讨论,请参见2.3。
第三种(也是最后一种)内存空间实际上并不存储变量,但是可以用来存储变量所指向的数据。如果把调用malloc()函数的结果赋给一个指针变量,那么这个指针变量将包含一块动态分配的内存的地址,这块内存位于一段名为“堆(heap)”的内存空间中。堆开始时也很小,但当程序员调用malloc()或calloc()等内存分配函数时它就会增大。堆可以和数据段或栈共用一个内存段(memorysegment),也可以有它自己的内存段,这完全取决于编译选项和操作系统。
与栈相似,堆也有一个增长界限,并且决定这个界限的规则与栈相同。
请参见:
1.1 什么是局部程序块(10calblock)?
2.2 变量必须初始化吗?
2.3 什么是页抖动(pagethrashing)?
7.20 什么是栈(stack)?
7.21 什么是堆(heap)7 .
2.2 变量必须初始化吗?
不。使用变量之前应该给变量一个值,一个好的编译程序将帮助你发现那些还没有被给定一个值就被使用的变量。不过,变量不一定需要初始化。在函数外部定义的变量或者在函数内部用static关键字定义的变量(被定义在数据段中的那些变量,见2.1)在没有明确地被程序初始化之前都已被系统初始化为0了。在函数内部或程序块内部定义的不带static关键字的变量都是自动变量,如果你没有明确地初始化这些变量,它们就会具有未定义值。如果你没有初始化一个自动变量,在使用它之前你就必须保证先给它赋值。
调用malloc()函数从堆中分配到的空间也包含未定义的数据,因此在使用它之前必须先进行初始化,但调用calloc()函数分配到的空间在分配时就已经被初始化为0了。
请参见:
1.1 什么是局部程序块(10calblock)?
7.20 什么是栈(stack)?
7.21 什么是堆(heap)?
2.3 什么是页抖动(pagethrashing)?
有些操作系统(如UNIX和增强模式下的Windows)使用虚拟内存,这是一种使机器的作业地址空间大于实际内存的技术,它是通过用磁盘空间模拟RAM(random—access memory)来实现的。
在80386和更高级的Intel CPU芯片中,在现有的大多数其它微处理器(如Motorola 68030,sparc和Power PC)中,都有一个被称为内存管理单元(Memory Management Unit,缩写为MMU)的器件。MMU把内存看作是由一系列“页(page)”组成的来处理。一页内存是指一个具有一定大小的连续的内存块,通常为4096或8192字节。操作系统为每个正在运行的程序建立并维护一张被称为进程内存映射(Process Memory Map,缩与为PMM)的表,表中记录了程序可以存取的所有内存页以及它们的实际位置。
每当程序存取一块内存时,它会把相应的地址(虚拟地址,virtualaddress)传送给MMU,MMU会在PMM中查找这块内存的实际位置(物理地址,physical address),物理地址可以是由操作系统指定的在内存中或磁盘上的任何位置。如果程序要存取的位置在磁盘上,就必须把包含该地址的页从磁盘上读到内存中,并且必须更新PMM以反映这个变化(这被称为pagefault,即页错)。
希望你继续读下去,因为下面就要介绍其中的难点了。存取磁盘比存取RAM要慢得多,所以操作系统会试图在RAM中保持尽量多的虚拟内存。如果你在运行一个非常大的程序(或者同时运行几个小程序),那么可能没有足够的RAM来承担程序要使用的全部内存,因此必须把一些页从RAM中移到磁盘上(这被为pagingout,即页出)。
操作系统会试图去判断哪些页可能暂时不会被使用(通常基于过去使用内存的情况),如果它判断错了,或者程序正在很多地方存取很多内存,那么为了读入已调出的页,就会产生大量页错动作。因为RAM已被全部使用,所以为了调入要存取的一页,必须调出另一页,而这将导致更多的页错动作,因为此时不同的一页已被移到磁盘上。在短时间内出现大量页错动作的情形被称为页抖动,它将大大降低系统的执行效率。
频繁存取内存中大量散布的位置的程序更容易在系统中造成页抖动。如果同时运行许多小程序,而实际上已经不再使用这些程序,也很容易造成页抖动。为了减少页抖动,你应该减少同时运行的程序的数目。对于大的程序,你应该改变它的工作方式,以尽量使操作系统能准确地判断出哪些页不再需要。为此,你可以使用高速缓冲存储技术,或者改变用于大型数据结构的查找算法,或者使用效率更高的malloc()函数。当然,你也可以考虑增加系统的RAM,以减少页出动作。
请参见:
7.17 怎样说明一个大于640KB的数组?
7.21 什么是堆(heap)?
18.14 怎样才能使DOS程序获得超过64KB的可用内存?
21.31 Windows是怎样组织内存的?
2.4 什么是const指针?
如果希望一个变量在被初始化后其值不会被修改,程序员就会通过cons,修饰符和编译程序达成默契。编译程序会努力去保证这种默契——它将禁止程序中出现对说明为const的变量进行修改的代码。
const指针的准确提法应该是指向const数据的指针,即它所指向的数据不能被修改。只要在指针说明的开头加入const修饰符,就可说明一个cosnt指针。尽管const指针所指向的数据不能被修改,但cosnt指针本身是可以修改的。下面给出了const指针的一些合法和非法的用法例子:
const char *str="hello";
char c=*str; /*legal*/
str++; /*legal*/
*str='a'; /* illegal */
str[1]='b'; /*illegal*/
前两条语句是合法的,因为它们没有修改str所指向的数据;后两条语句是非法的,因为它们要修改str所指向的数据。
在说明函数参数时,常常要使用const指针。例如,一个计算字符串长度的函数不必改变字符串内容,它可以写成这样:
my_strlen(const char *str)
{
int count=0;
while ( * str++)
{
count ++;
}
return count;
}
注意,如果有必要,一个非const指针可以被隐式地转换为const指针,但一个const指针不能被转换成非const指针。这就是说,在调用my_strlen()时,它的参数既可以是一个const指针,也可以是一个非const指针。
请参见:
2.7 一个变量可以同时被说明为const和volatile吗?
2.8 什么时候应该使用const修饰符?
2.14 什么时候不应该使用类型强制转换(type cast)?
2. 18 用const说明常量有什么好处?
2.5 什么时候应该使用register修饰符?它真的有用吗?
register修饰符暗示编译程序相应的变量将被频繁使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存取速度。但是,使用register修饰符有几点限制。
首先,register变量必须是能被CPU寄存器所接受的类型。这通常意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。但是,有些机器的寄存器也能存放浮点数。
其次,因为register变量可能不存放在内存中,所以不能用取址运算符“&”来获取register变量的地址。如果你试图这样做,编译程序就会报告这是一个错误。
register修饰符的用处有多大还受其它一些规则的影响。因为寄存器的数量是有限的,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此,真正能起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
在某些情况下,把变量保存在寄存器中反而会降低运行速度,因为被占用的寄存器不能再用于其它目的,或—者变量被使用的次数不够多,不足以抵消装入和存储变量所带来的额外开销。
那么,什么时候应该使用register修饰符呢?回答是,对现有的大多数编译程序来说,永远不要使用register修饰符。早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定哪些变量应该被存到寄存器中时,现在的C编译程序能比程序员作出更好的决定。
实际上,许多C编译程序会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。
在极罕见的情况下,程序运行速度很慢,而你也知道这是因为有一个变量被存储在内存中,也许你最后会试图在该变量前面加上register修饰符,但是,如果这并没有加快程序的运行速度,你也不要感到奇怪。
请参见:
2.6 什么时候应该使用volatile修饰符?
2. 6 什么时候应该使用volatile修饰符?
volatile修饰符告诉编译程序不要对该变量所参与的操作进行某些优化。在两种特殊的情况下需要使用volatile修饰符:第一种情况涉及到内存映射硬件(memory-mapped hardware,如图形适配器,这类设备对计算机来说就好象是内存的一部分一样),第二种情况涉及到共享内存(shared memory,即被两个以上同时运行的程序所使用的内存)。
大多数计算机拥有一系列寄存器,其存取速度比计算机主存更快。好的编译程序能进行一种被称为“冗余装入和存储的删去”(redundant load and store removal)的优化,即编译程序会·在程序中寻找并删去这样两类代码:一类是可以删去的从内存装入数据的指令,因为相应的数据已经被存放在寄存器中;另一种是可以删去的将数据存入内存的指令,因为相应的数据在再次被改变之前可以一直保留在寄存器中。
如果一个指针变量指向普通内存以外的位置,如指向一个外围设备的内存映射端口,那么冗余装入和存储的优化对它来说可能是有害的。例如,为了调整某个操作的时间,可能会用到下述函数:
time_t time_addition(volatile const struct timer * t, int a),
{
int n
int x
time_t then
x=O;
then= t->value
for (n=O; n<1O00; n++)
{
x=x+a ;
}
return t->value - then;
}
在上述函数中,变量t->value实际上是一个硬件计数器,其值随时间增加。该函数执行1000次把a值加到x上的操作,然后返回t->value在这1000次加法的执行期间所增加的值。
如果不使用volatile修饰符,一个聪明的编译程序可能就会认为t->value在该函数执行期间不会改变,因为该函数内没有明确地改变t->value的语句。这样,编译程序就会认为没有必要再次从内存中读入t->value并将其减去then,因为答案永远是0。因此,编译程序可能会对该函数进行“优化”,结果使得该函数的返回值永远是0。
如果一个指针变量指向共享内存中的数据,那么冗余装入和存储的优化对它来说可能也是有害的,共享内存通常用来实现两个程序之间的互相通讯,即让一个程序把数据存到共享的那块内存中,而让另一个程序从这块内存中读数据。如果从共享内存装入数据或把数据存入共享内存的代码被编译程序优化掉了,程序之间的通讯就会受到影响。
请参见:
2.7 一个变量可以同时被说明为const和volatile吗?
2.14 什么时候不应该使用类型强制转换(typecast)?
2.7 一个变量可以同时被说明为const和volatile吗?
可以。const修饰符的含义是变量的值不能被使用了const修饰符的那段代码修改,但这并不意味着它不能被这段代码以外的其它手段修改。例如,在2.6的例子中,通过一个volatile const指针t来存取timer结构。函数time_addition()本身并不修改t->value的值,因此t->value被说明为const。不过,计算机的硬件会修改这个值,因此t->value又被说明为volatile。如果同时用const和volatile来说明一个变量,那么这两个修饰符随便哪个在先都行,
请参见:
2.6什么时候应该使用volatile修饰符?
2.8什么时候应该使用const修饰符?
2.14什么时候不应该使用类型强制转换(typecast)?
2.8 什么时候应该使用const修饰符?
使用const修饰符有几个原因,第一个原因是这样能使编译程序找出程序中不小心改变变量值的错误。请看下例:
while ( * str=0) / * programmer meant to write * str! =0 * /
{
/ * some code here * /
strq++;
}
其中的“=”符号是输入错误。如果在说明str时没有使用const修饰符,那么相应的程序能通过编译但不能被正确执行。
第二个原因是效率。如果编译程序知道某个变量不会被修改,那么它可能会对生成的代码进行某些优化。
如果一个函数参数是一个指针,并且你不希望它所指向的数据被该函数或该函数所调用的函数修改,那么你应该把该参数说明为const指针。如果一个函数参数通过值(而不是通过指针)被传递给函数,并且你不希望其值被该函数所调用的函数修改,那么你应该把该参数说明为const。然而,在实际编程中,只有在编译程序通过指针存取这些数据的效率比拷贝这些数据更高时,才把这些参数说明为const。
请参见:
2.7 一个变量可以同时被说明为const和volatile吗?
2.14 什么时候不应该使用类型强制转换(typecast)?
2.18用const说明常量有什么好处?
2.9 浮点数比较(floating-point comparisons)的可靠性如何?
浮点数是计算机编程中的“魔法(black art)”,原因之一是没有一种理想的方式可以表示一个任意的数字。电子电气工程协会(IEEE)已经制定出浮点数的表示标准,但你不能保证所使用的每台机器都遵循这一标准。
即使你使用的机器遵循这一标准,还存在更深的问题。从数学意义上讲,两个不同的数字之间有无穷个实数。计算机只能区分至少有一位(bit)不同的两个数字。如果要表示那些无穷无尽的各不相同的数字,就要使用无穷数目的位。计算机只能用较少的位(通常是32位或64位)来表示一个很大的范围内的数字,因此它只能近似地表示大多数数字。
由于浮点数是如此难对付,因此比较一个浮点数和某个值是否相等或不等通常是不好的编程习惯。但是,判断一个浮点数是否大于或小于某个值就安全多了。例如,如果你想以较小的步长依次使用一个范围内的数字,你可能会编写这样一个程序:
#include <stdio.h>
const float first = O.O;
const float last = 70.0
const float small= O.007
main ( )
{
float f;
for (f=first; f !=last && f<last+1.O; f +=small)
printf("f is now %g\n", f);
}
然而,舍入误差(rounding error)和变量small的表示误差可能导致f永远不等于last(f可能会从稍小于last的一个数增加到一个稍大于last的数),这样,循环会跳过last。加入不等式"f<last+1.0"就是为了防止在这种情况发生后程序继续运行很长时间。如果运行该程序并且被打印出来的f值是71或更大的数值,就说明已经发生了这种情况。
一种较安全的方法是用不等式"f<last"作为条件来终止循环,例如:
float f;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -