第
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;
for(f=first; f<last; f+=small)
;
你甚至可以预先算出循环次数,然后通过这个整数进行循环计数:
float f ;
int count = (last-first)/small ;
for(f=first ; count-->0 ; f+=small)
;
请参见:
2 . 11 对不同类型的变量进行算术运算会有问题吗 ?
2 . 10 怎样判断一个数字型变量可以容纳的最大值 ?
要判断某种特定类型可以容纳的最大值或最小值,一种简便的方法是使用 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 位的数是怎样存储的 ?
2 . 11 对不同类型的变量进行算术运算会有问题吗 ?
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)?
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 <stdio.h>
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 . 13 什么时候应该使用类型强制转换 (typecast)?
在两种情况下需要使用类型强制转换。第一种情况是改变运算分量的类型,从而使运算能正确地进行。下面的程序与 2 . 12 中的例子相似,但有不同之处。变量 n 被赋值为整数 i 除以整数 j 的结果,因为是整数相除,所以结果为 0 。变量 f2 也被赋值为 i 除以 j 的结果,但本例通过 (float) 类型强制转换把 i 转换成一个 float 类型,因此执行的是浮点数除法运算 ( 见 2 . 11) ,结果为 0 . 75 。
#include <stdio.h>
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 指针进行算术运算吗 ?
2 . 14 什么时候不应该使用类型强制转换 (typecast)?
不应该对用 const 或 volatile 说明了的对象进行类型强制转换,否则程序就不能正确运行。
不应该用类型强制转换把指向一种结构类型或数据类型的指针转换成指向另一种结构类型或数据类型的指针。在极少数需要进行这种类型强制转换的情况下,用共用体 (union) 来存放有关数据能更清楚地表达程序员的意图。
请参见:
2. 6 什么时候应该使用 volatile 修饰符 ?
2. 8 什么时候应该使用 const 修饰符 ?
2. 15 可以在头文件中说明或定义变量吗 ?
被多个文件存取的全局变量可以并且应该在一个头文件中说明,并且必须在一个源文件中定义。变量不应该在头文件中定义,因为一个头文件可能被多个源文件包含,而这将导致变量被多次定义。如果变量的初始化只发生一次, ANSIC 标准允许变量有多次外部定义;但是,这样做没有任何好处,因此最好避免这样做,以使程序有更强的可移植性。
注意:变量的说明和定义是两个不同的概念,在 2 . 16 中将讲解两者之间的区别。
仅供一个文件使用的 “ 全局 ” 变量应该被说明为 static ,而且不应该出现在头文件中。
请参见:
2. 16 说明一个变量和定义一个变量有什么区别 ?
2. 17 可以在头文件中说明 static 变量吗 ?
2 . 16 说明一个变量和定义一个变量有什么区别 ?
说明一个变量意味着向编译程序描述变量的类型,但并不为变量分配存储空间。定义一个变量意味着在说明变量的同时还要为变量分配存储空间。在定义一个变量的同时还可以对变量进行初始化。下例说明了一个变量和一个结构,定义了两个变量,其中一个定义带初始化:
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 变量吗 ?
2 . 17 可以在头文件中说明 static 变量吗 ?
如果说明了一个 static 变量,就必须在同一个文件中定义该变量 ( 因为存储类型修饰符 static 和 extern 是互斥的 ) 。你可以在头文件中定义一个 static 变量,但这会使包含该头文件的源文件都得到该变量的一份私有拷贝,而这通常不是你想得到的结果。
请参见:
2 . 16 说明一个变量和定义一个变量有什么区别 ?
2 . 18 用 const 说明常量有什么好处 ?
使用关键字 const 有两个好处;第一,如果编译程序知道一个变量的值不会改变,编译程.序就能对程序进行优化;第二,编译程序会试图保证该变量的值不会因为程序员的疏忽而被改变。
当然,用 #define 来定义常量也有同样的好处。用 const 而不用 #define 来定义常量的原因是 const 变量可以是任何类型 ( 如结构,而用 #define 定义的常量不能表示结构 ) 。此外, const 变量是真正的变量,它有可供使用的地址,并且该地址是唯一的 ( 有些编译程序在每次使用用 #define 定义的字符串时都会生成一份新的拷贝,见 9 . 9) 。
请参见:
2 . 7 一个变量可以同时被说明为 const 和 volatile 吗 ?
2 . 8 什么时候应该使用 const 修饰符 ?
2 . 14 什么时候不应该使用类型强制转换 (typecast)?
9 . 9 字符串和数组有什么不同 ?
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;
for(f=first; f<last; f+=small)
;
你甚至可以预先算出循环次数,然后通过这个整数进行循环计数:
float f ;
int count = (last-first)/small ;
for(f=first ; count-->0 ; f+=small)
;
请参见:
2 . 11 对不同类型的变量进行算术运算会有问题吗 ?
2 . 10 怎样判断一个数字型变量可以容纳的最大值 ?
要判断某种特定类型可以容纳的最大值或最小值,一种简便的方法是使用 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 位的数是怎样存储的 ?
2 . 11 对不同类型的变量进行算术运算会有问题吗 ?
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)?
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 <stdio.h>
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 . 13 什么时候应该使用类型强制转换 (typecast)?
在两种情况下需要使用类型强制转换。第一种情况是改变运算分量的类型,从而使运算能正确地进行。下面的程序与 2 . 12 中的例子相似,但有不同之处。变量 n 被赋值为整数 i 除以整数 j 的结果,因为是整数相除,所以结果为 0 。变量 f2 也被赋值为 i 除以 j 的结果,但本例通过 (float) 类型强制转换把 i 转换成一个 float 类型,因此执行的是浮点数除法运算 ( 见 2 . 11) ,结果为 0 . 75 。
#include <stdio.h>
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 指针进行算术运算吗 ?
2 . 14 什么时候不应该使用类型强制转换 (typecast)?
不应该对用 const 或 volatile 说明了的对象进行类型强制转换,否则程序就不能正确运行。
不应该用类型强制转换把指向一种结构类型或数据类型的指针转换成指向另一种结构类型或数据类型的指针。在极少数需要进行这种类型强制转换的情况下,用共用体 (union) 来存放有关数据能更清楚地表达程序员的意图。
请参见:
2. 6 什么时候应该使用 volatile 修饰符 ?
2. 8 什么时候应该使用 const 修饰符 ?
2. 15 可以在头文件中说明或定义变量吗 ?
被多个文件存取的全局变量可以并且应该在一个头文件中说明,并且必须在一个源文件中定义。变量不应该在头文件中定义,因为一个头文件可能被多个源文件包含,而这将导致变量被多次定义。如果变量的初始化只发生一次, ANSIC 标准允许变量有多次外部定义;但是,这样做没有任何好处,因此最好避免这样做,以使程序有更强的可移植性。
注意:变量的说明和定义是两个不同的概念,在 2 . 16 中将讲解两者之间的区别。
仅供一个文件使用的 “ 全局 ” 变量应该被说明为 static ,而且不应该出现在头文件中。
请参见:
2. 16 说明一个变量和定义一个变量有什么区别 ?
2. 17 可以在头文件中说明 static 变量吗 ?
2 . 16 说明一个变量和定义一个变量有什么区别 ?
说明一个变量意味着向编译程序描述变量的类型,但并不为变量分配存储空间。定义一个变量意味着在说明变量的同时还要为变量分配存储空间。在定义一个变量的同时还可以对变量进行初始化。下例说明了一个变量和一个结构,定义了两个变量,其中一个定义带初始化:
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 变量吗 ?
2 . 17 可以在头文件中说明 static 变量吗 ?
如果说明了一个 static 变量,就必须在同一个文件中定义该变量 ( 因为存储类型修饰符 static 和 extern 是互斥的 ) 。你可以在头文件中定义一个 static 变量,但这会使包含该头文件的源文件都得到该变量的一份私有拷贝,而这通常不是你想得到的结果。
请参见:
2 . 16 说明一个变量和定义一个变量有什么区别 ?
2 . 18 用 const 说明常量有什么好处 ?
使用关键字 const 有两个好处;第一,如果编译程序知道一个变量的值不会改变,编译程.序就能对程序进行优化;第二,编译程序会试图保证该变量的值不会因为程序员的疏忽而被改变。
当然,用 #define 来定义常量也有同样的好处。用 const 而不用 #define 来定义常量的原因是 const 变量可以是任何类型 ( 如结构,而用 #define 定义的常量不能表示结构 ) 。此外, const 变量是真正的变量,它有可供使用的地址,并且该地址是唯一的 ( 有些编译程序在每次使用用 #define 定义的字符串时都会生成一份新的拷贝,见 9 . 9) 。
请参见:
2 . 7 一个变量可以同时被说明为 const 和 volatile 吗 ?
2 . 8 什么时候应该使用 const 修饰符 ?
2 . 14 什么时候不应该使用类型强制转换 (typecast)?
9 . 9 字符串和数组有什么不同 ?