Chinaunix首页 | 论坛 | 博客
  • 博客访问: 310633
  • 博文数量: 126
  • 博客积分: 7051
  • 博客等级: 少将
  • 技术积分: 1425
  • 用 户 组: 普通用户
  • 注册时间: 2008-04-20 13:21
文章分类

全部博文(126)

文章存档

2008年(126)

我的朋友

分类: C/C++

2008-05-03 13:58:37

C语言变量和数据存储

C语言的强大功能之一是可以灵活地定义数据的存储方式。C语言从两个方面控制变量的性质:作用域(scope)和生存期(lifetime)。作用域是指可以存取变量的代码范围,生存期是指可以存取变量的时间范围。
作用域有三种:
1. extern(外部的) 这是在函数外部定义的变量的缺省存储方式。extern变量的作用域是整个程序。
2.static(静态的) 在函数外部说明为static的变量的作用域为从定义点到该文件尾部;在函数内部说明为static的变量的作用域为从定义点到该局部程序块尾部。
3.auto(自动的) 这是在函数内部说明的变量的缺省存储方式。auto变量的作用域为从定义点到该局部程序块尾部。
变量的生存期也有三种,但它们不象作用域那样有预定义的关键字名称。第一种是extern和static变量的生存期,它从main()函数被调用之前开 始,到程序退出时为止。第二种是函数参数和auto变量的生存期,它从函数调用时开始,到函数返回时为止。第三种是动态分配的数据的生存期,它从程序调用 malloc()或calloc()为数据分配存储空间时开始,到程序调用free()或程序退出时为止。

变量可以存储在内存中的不同地方,这依赖于它们的生存期。在函数外部定义的变量(全局变量或静态外部变量)和在函数内部定义的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 .


不。使用变量之前应该给变量一个值,一个好的编译程序将帮助你发现那些还没有被给定一个值就被使用的变量。不过,变量不一定需要初始化。在函数外部定义的 变量或者在函数内部用static关键字定义的变量(被定义在数据段中的那些变量,见2.1)在没有明确地被程序初始化之前都已被系统初始化为0了。在函 数内部或程序块内部定义的不带static关键字的变量都是自动变量,如果你没有明确地初始化这些变量,它们就会具有未定义值。如果你没有初始化一个自动 变量,在使用它之前你就必须保证先给它赋值。
调用malloc()函数从堆中分配到的空间也包含未定义的数据,因此在使用它之前必须先进行初始化,但调用calloc()函数分配到的空间在分配时就已经被初始化为0了。

请参见:
1.1 什么是局部程序块(10calblock)?
7.20 什么是栈(stack)?
7.21 什么是堆(heap)?


有些操作系统(如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是怎样组织内存的?


如果希望一个变量在被初始化后其值不会被修改,程序员就会通过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说明常量有什么好处?


register修饰符暗示编译程序相应的变量将被频繁使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存取速度。但是,使用register修饰符有几点限制。
首先,register变量必须是能被CPU寄存器所接受的类型。这通常意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。但是,有些机器的寄存器也能存放浮点数。
其次,因为register变量可能不存放在内存中,所以不能用取址运算符“&”来获取register变量的地址。如果你试图这样做,编译程序就会报告这是一个错误。
register修饰符的用处有多大还受其它一些规则的影响。因为寄存器的数量是有限的,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因 此,真正能起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
在某些情况下,把变量保存在寄存器中反而会降低运行速度,因为被占用的寄存器不能再用于其它目的,或—者变量被使用的次数不够多,不足以抵消装入和存储变量所带来的额外开销。
那么,什么时候应该使用register修饰符呢?回答是,对现有的大多数编译程序来说,永远不要使用register修饰符。早期的C编译程序不会把变 量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定哪些变量应 该被存到寄存器中时,现在的C编译程序能比程序员作出更好的决定。
实际上,许多C编译程序会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。
在极罕见的情况下,程序运行速度很慢,而你也知道这是因为有一个变量被存储在内存中,也许你最后会试图在该变量前面加上register修饰符,但是,如果这并没有加快程序的运行速度,你也不要感到奇怪。

请参见:
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)?

可以。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)?

使用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说明常量有什么好处?


浮点数是计算机编程中的“魔法(black art)”,原因之一是没有一种理想的方式可以表示一个任意的数字。电子电气工程协会(IEEE)已经制定出浮点数的表示标准,但你不能保证所使用的每台机器都遵循这一标准。
即使你使用的机器遵循这一标准,还存在更深的问题。从数学意义上讲,两个不同的数字之间有无穷个实数。计算机只能区分至少有一位(bit)不同的两个数 字。如果要表示那些无穷无尽的各不相同的数字,就要使用无穷数目的位。计算机只能用较少的位(通常是32位或64位)来表示一个很大的范围内的数字,因此 它只能近似地表示大多数数字。
由于浮点数是如此难对付,因此比较一个浮点数和某个值是否相等或不等通常是不好的编程习惯。但是,判断一个浮点数是否大于或小于某个值就安全多了。例如,如果你想以较小的步长依次使用一个范围内的数字,你可能会编写这样一个程序:

#include
const float first = O.O;
const float last = 70.0
const float small= O.007
main ( )
{
float f;
for (f=first; f !=last && f printf("f is now %gn", f);
}

然而,舍入误差(rounding error)和变量small的表示误差可能导致f永远不等于last(f可能会从稍小于last的一个数增加到一个稍大于last的数),这样,循环会 跳过last。加入不等式"f 一种较安全的方法是用不等式"f float f;
for(f=first; f
你甚至可以预先算出循环次数,然后通过这个整数进行循环计数:
float f;
int count=(last-first)/small;
for(f=first;count-->0;f+=small)

请参见:
2.11 对不同类型的变量进行算术运算会有问题吗?


要判断某种特定类型可以容纳的最大值或最小值,一种简便的方法是使用ANSI标准头文件limits.h中的预定义值。该文件包含一些很有用的常量,它们定义了各种类型所能容纳的值,下表列出了这些常量:
----------------------------------------------------------------
常 量 描 述
----------------------------------------------------------------
CHAR—BIT char的位数(bit)
CHAR—MAX char的十进制整数最大值
CHAR—MIN char的十进制整数最小值
MB—LEN—MAX 多字节字符的最大字节(byte)数
INT—MAX int的十进制最大值
INT—MIN int的十进制最小值
LONG—MAX long的十进制最大值
LONG—MIN long的十进制最小值
SCHAR—MAX signedchar的十进制整数最大值
SCHAR—MIN signedchar的十进制整数最小值
SHRT—MIN short的十进制最小值
SHRT—MAX short的十进制最大值
UCHAR—MAX unsignedchar的十进制整数最大值
UINT—MAX unsignedint的十进制最大值
ULONG—MAX unsignedlongint的十进制最大值
USHRT—MAX unsignedshortint的十进制最大值
-----------------------------------------------------------------
对于整数类型,在使用2的补码运算的机器(你将使用的机器几乎都属此类)上,一个有符号类型可以容纳的数字范围为-2位数-1到(+2位数-1-1),一 个无符号类型可以容纳的数字范围为0到(+2位数-1)。例如,一个16位有符号整数可以容纳的数字范围为--215(即-32768)到(+215- 1)(即+32767)。

请参见:
10.1用什么方法存储标志(flag)效率最高?
10.2什么是“位屏幕(bitmasking)”?
10.6 16位和32位的数是怎样存储的?


C有三类固有的数据类型:指针类型、整数类型和浮点类型;
指针类型的运算限制最严,只限于以下两种运算:
- 两个指针相减,仅在两个指针指向同一数组中的元素时有效。运算结果与对应于两个指针的数组下标相减的结果相同。
+ 指针和整数类型相加。运算结果为一个指针,该指针与原指针之间相距n个元素,n就是与原指针相加的整数。
浮点类型包括float,double和longdouble这三种固有类型。整数类型包括char,unsigned char,short,unsigned short,int,unsigned int,long和unsigned long。对这些类型都可进行以下4种算术运算:
+ 加
- 减
* 乘
/ 除
对整数类型不仅可以进行上述4种运算,还可进行以下几种运算:
% 取模或求余
>> 右移
<< 左移
& 按位与
| 按位或
^ 按位异或
! 逻辑非
~ 取反
尽管C允许你使用“混合模式”的表达式(包含不同类型的算术表达式),但是,在进行运算之前,它会把不同的类型转换成同一类型(前面提到的指针运算除外)。这种自动转换类型的过程被称为“运算符升级(operator promotion)”。

请参见:
2.12什么是运算符升级(operatorpromotion)?


当两个不同类型的运算分量(operand)进行运算时,它们会被转换为能容纳它们的最小的类型,并且运算结果也是这种类型。下表列出了其中的规则,在应用这些规则时,你应该从表的顶端开始往下寻找,直到找到第一条适用的规则。
-------------------------------------------------------------
运算分量1 运算分量2 转换结果
-------------------------------------------------------------
long double 其它任何类型 long double
double 任何更小的类型 double
float 任何更小的类 float
unsigned long 任何整数类 unsigned long
long unsigned>LONG_MAX unsigned long
long 任何更小的类型 long
unsigned 任何有符号类型 unsigned
-------------------------------------------------------------
下面的程序中就有几个运算符升级的例子。变量n被赋值为3/4,因为3和4都是整数,所以先进行整数除法运算,结果为整数0。变量f2被赋值为3/4.0,因为4.0是一个float类型,所以整数3也被转换为float类型,结果为float类型0.75。
#include
main ()
{
float f1 = 3/4;
float f2 = 3/4.0
printf("3/4== %g or %g depending on the type used. n",f1, f2);
}

请参见:
2.11对不同类型的变量进行算术运算会有问题吗?
2.13什么时候应该使用类型强制转换(typecast)?


在两种情况下需要使用类型强制转换。第一种情况是改变运算分量的类型,从而使运算能正确地进行。下面的程序与2.12中的例子相似,但有不同之处。变量n 被赋值为整数i除以整数j的结果,因为是整数相除,所以结果为0。变量f2也被赋值为i除以j的结果,但本例通过(float)类型强制转换把i转换成一 个float类型,因此执行的是浮点数除法运算(见2.11),结果为0.75。
#include
main ( )
{
int i = 3;
int j = 4
float f1 =i/j;
float f2= (float) i/j;
printf("3/4== %g or %g depending on the type used. n",f1, f2);
}

第二种情况是在指针类型和void * 类型之间进行强制转换,从而与期望或返回void指针的函数进行正确的交接。例如,下述语句就把函数malloc()的返回值强制转换为一个指向foo结构的指针:
struct foo *p=(struct foo *)malloc(sizeof(struct foo));

请参见:
2.6什么时候应该使用volatile修饰符?
2.8什么时候应该使用const修饰符?
2.11对不同类型的变量进行算术运算会有问题吗?
2.12 什么是运算符升级(operator promotion)?
2.14 什么时候不应该使用类型强制转换(typecast)?
7.5 什么是void指针?
7.6 什么时候使用void指针?
7.21 什么是堆(heap)?
7.27 可以对void指针进行算术运算吗?


不应该对用const或volatile说明了的对象进行类型强制转换,否则程序就不能正确运行。
不应该用类型强制转换把指向一种结构类型或数据类型的指针转换成指向另一种结构类型或数据类型的指针。在极少数需要进行这种类型强制转换的情况下,用共用体(union)来存放有关数据能更清楚地表达程序员的意图。

请参见:
2. 6什么时候应该使用volatile修饰符?
2. 8什么时候应该使用const修饰符?


被多个文件存取的全局变量可以并且应该在一个头文件中说明,并且必须在一个源文件中定义。变量不应该在头文件中定义,因为一个头文件可能被多个源文件包 含,而这将导致变量被多次定义。如果变量的初始化只发生一次,ANSIC标准允许变量有多次外部定义;但是,这样做没有任何好处,因此最好避免这样做,以 使程序有更强的可移植性。
注意:变量的说明和定义是两个不同的概念,在2.16中将讲解两者之间的区别。
仅供一个文件使用的“全局”变量应该被说明为static,而且不应该出现在头文件中。

请参见:
2. 16 说明一个变量和定义一个变量有什么区别?
2. 17 可以在头文件中说明static变量吗?


说明一个变量意味着向编译程序描述变量的类型,但并不为变量分配存储空间。定义一个变量意味着在说明变量的同时还要为变量分配存储空间。在定义一个变量的同时还可以对变量进行初始化。下例说明了一个变量和一个结构,定义了两个变量,其中一个定义带初始化:
extern int decll; / * this is a declaration * /
struct decl2 {
int member;
} ; / * this just declares the type--no variable mentioned * /
int def1 = 8; / * this is a definition * /
int def2; / * this is a definition * /

换句话说,说明一个变量相当于告诉编译程序“在程序的某个位置将用到一个变量,这里给出了它的名称和类型”,定义一个变量则相当于告诉编译程序“具有这个名称和这种类型的变量就在这里”。
一个变量可以被说明许多次,但只能被定义一次。因此,不应该在头文件中定义变量,因为一个头文件可能会被一个程序的许多源文件所包含。

请参见;
2.17可以在头文件中说明static变量吗?


如果说明了一个static变量,就必须在同一个文件中定义该变量(因为存储类型修饰符static和extern是互斥的)。你可以在头文件中定义一个static变量,但这会使包含该头文件的源文件都得到该变量的一份私有拷贝,而这通常不是你想得到的结果。

请参见:
2.16 说明一个变量和定义一个变量有什么区别?


使用关键字const有两个好处;第一,如果编译程序知道一个变量的值不会改变,编译程.序就能对程序进行优化;第二,编译程序会试图保证该变量的值不会因为程序员的疏忽而被改变。
当然,用#define来定义常量也有同样的好处。用const而不用#define来定义常量的原因是const变量可以是任何类型(如结构,而用 #define定义的常量不能表示结构)。此外,const变量是真正的变量,它有可供使用的地址,并且该地址是唯一的(有些编译程序在每次使用用 #define定义的字符串时都会生成一份新的拷贝,见9.9)。

请参见:
2.7 一个变量可以同时被说明为const和volatile吗?
2.8 什么时候应该使用const修饰符?
2.14 什么时候不应该使用类型强制转换(typecast)?
9.9 字符串和数组有什么不同?

阅读(1363) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~