C chapter2 数据


基本数据类型

在 C 语言中,仅有 4 种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。所有其他的类型都是从这 4 种基本类型的某种组合派生而来。

整型

整型包括字符、短整型和长整型,它们都分为有符号(singed)无符号(unsigned) 两种版本。

长整型至少应该和整型一样长,而整型至少应该和短整型一样长。

注意:标准并没有规定整型必须比短整型长,只是规定它不得比短整型短。

变量的最小范围:

类型 最小范围
char 0~127
unsigned char 0~255
short -32767~32767
unsigned short 0~65535
int -32767~32767
unsigned int 0~65535
long -2147483647~2147483647
unsigned long 0~4294967295

short int 至少 16 位,long int 至少 32 位。至于缺省(默认)的 int 究竟是 16 位还是 32 位,或者是其他值,则由编译器设计者决定。通常这个选择的缺省值是这种机器最为自然(高效)的位数。同时你还应该注意到标准也没有规定这 3 个值必须不一样。如果某种机器的环境的字长是 32 位,而且没有什么指令能够更有效地处理更短的整型值,它可能把这 3 个整型值都设定为 32 位。

头文件 limits.h 说明了各种不同的整数类型的特点。它定义了下表所示的各个名字。limits.h 同时定义了下列名字:CHAR_BIT 是字符型的位数(至少 8 位);CHAR_MINCHAR_MAX 定义了缺省字符类型的范围,它们或者应该与 SCHAR_MINSCHAR_MAX 相同,或者应该与 0 和 UCHAR_MAX 相同;最后,MB_LEN_MAX 规定了一个多字节字符最多允许的字符数量。

变量范围的限制:

signed unsigned
类型 最小值 最大值 最大值
字符 SCHAR_MIN SCHAR_MAX UCHAR_MAX
短整型 SHRT_MIN SHRT_MAX USHRT_MAX
整型 INT_MIIN INT_MAX UINT_MAX
长整型 LONG_MIN LONG_MAX ULONG_MAX
尽管设计char类型变量的目的是为了让它们容纳字符型值,但字符在本质上是小整型值。缺省的char要么是signed char,要么是 unsigned char,这取决于编译器。这意味着不同机器上的char可能拥有不同范围的值。所以,只有当程序所使用的char型变量的值位于signed charunsigned char的交集中,这个程序才是可移植的。

在一个把字符当作小整型值的程序中,如果显式地把这类变量声明为signedunsigned,可以提高这类程序的可移植性。这类做法可以确保不同的机器中在字符是否为有符号值方面保持一致。

提示:当可移植问题比较重要时,字符是否为有符号数就会带来两难的境地。最佳妥协方案就是把存储于 char 型变量的值限制在signed charunsigned char的交集内,这可以获得最大程度的可移植性,同时又不牺牲效率。

整型字面值

字面值(literal) 这个术语是字面值常量的缩写——这是一种实体,指定了本身的值,并且不允许发生改变。这个特点非常重要,因为 C 标准允许命名常量的创建,它与普通变量极为类似。区别在于,当它被初始化以后,它的值便不能改变。

当程序中出现整型字面值时,它是属于所有整型不同类型中的哪一个?答案取决于字面值是如何书写的,但是你可以在有些字面值的后面添加一个后缀来改变缺省的规则。在整数字面值后面添加字符 L 或 l,可以使这个整数倍解释为long整型值,字符 U 或 u 则用于把数值指定为 unsigned整型值。如果在一个字面值后面添加这两组字符中的各一个,那么它就被解释为unsigned long整型值

在源代码中,用于表示整型字面值的方法有很多。其中最自然的方式是十进制整型值,诸如:

1
123 355 -234

十进制整型字面值可能是intlongunsigned long。在缺省情况下,它是最短类型但能完整容纳这个值。

整型也可以用八进制来表示,只要在数值前面以 0 开头。整数也可以用十六进制来表示,它以 0x 开头。

例:

1
2
0123 012345 00342
0x32 0xffff 0xab2342

在八进制字面值中,数字 8 和 9 是非法的。在十六进制字面值中,可以使用字母 ABCDEF 或 abcdef。八进制和十六进制字面值可能的类型是intunsigned intlongunsigned long。在缺省情况下,字面值的类型就是上诉类型中最短但足以容纳整个值的类型。

另外还有字符常量。它们的类型总是int。你不能在它们后面添加unsignedlong后缀。字符常量就是一个用单引号包围起来的单个字符(或字符转义序列或三字母词),如:

1
'M'  '\n'  '??('  '\377'

标准也允许诸如 ‘abc’ 这类的多字节字符常量,但它们的实现在不同的环境中可能不一样,所以不鼓励使用。

最后,如果一个多字节字符常量的前面有一个 L,那么他就是宽字符常量(wide characterliteral)。

如:

1
L'X' L'e^'

当运行时环境支持一种宽字符集时,就有可能使用它们。

提示:整型字面值采用何种书写方式,应该取决于这个字面值使用时的上下文环境

当一个字面值用于确定一个字中某些特定位的位置时,将它写成十六进制或八进制更为合适,因为这种写法更清晰地显示了这个值的特殊本质。

例如,983040 这个值在第 16~19 位都是1,如果它采用十进制写法,你绝对看不出这一点。但是,如果将它写成十六进制的形式,它的值就是 0XF00,清晰地显示出那几位都是 1 而剩余的位都是 0。如果在某种上下文环境中,这些特定的位非常重要时,那么把字面值写成十六进制形式可以使操作的含义对于读者而言更为清晰。

如果一个值被当作字符使用,那么把这个值表示为字符常量可以使这个值的意思更为清晰。

例如:

1
2
value=value-48;
value=value-\60;

和下面这条语句

1
value=value-'0';

的含义完全一样,但最后一条语句的含义更为清晰,它用于表示把一个字符转换为二进制值。更为重要的是,不管你所采用的是何种字符集,使用字符常量所产生的总是正确的值,所以它能提高程序的可移植性。

枚举类型

枚举(enumerated) 类型就是指它的值为符号常量而不是字面值的类型,它们以下面这个形式声明:

1
enum JSJ{ CUP,PINT,QUART}

这条语句声明了一共类型,称为 JSJ。这种类型的变量按下列方式声明:

1
ecum JSJ jug,can,bottle

如果某种特别的枚举类型的变量只使用一个声明,你可以把上面两条语句组合成下面的样子:

1
2
ecum{CPU,PINT,QUART}
jug,can,bottle;

这种类型的变量实际上以整型的方式存储,这些符号名的实际值都是整型值。这里 CPU 是 0,PINT 是 1,以此类推。

适当的时候你可以为这些符号名指定特定的整型值,如下所示:

1
enum {CPU=1,PINT=2,CALLON=3}

只对部分符号名用这种方式进行赋值也是合法的。如果某个符号名未显示指定一个值,那么它的值就比前面一个符号名的值大 1。

提示:符号名被当作整型常量处理,声明为枚举类型的变量实际上是整数类型。这个事实意味着你可以给 Jar_Type 类型的变量赋诸如 -623这样的字面值,你也可以把 CPU 这个值赋给任何整型变量。但是你要避免以这种方式使用枚举,因为把枚举变量同整数无差别地混合在一起使用,会削弱它们值的含义。

浮点型

诸如 3.14159 和 6.324x10^2 这样的数值无法按照整数存储。第一个数并非整数,而第二个数远远超出了计算机整数所能表达的范围。但是,它们可以用浮点数的形式存储。它们通常以一个小数以及一个以某个假定数为基数的指数组成,例如:

1
.3243fx16  .110010010000111111x2^2

浮点数包括floatdoublelong double类型。通常,这些类型分别提供单精度、双精度以及在某些支持扩展精度的机器上提供扩展精度。C 标准仅仅规定long double至少和double一样长,而double至少和float一样长。标准同时规定了一个最小范围:所有浮点数类型至少能够容纳从 10^-37 到 10^37 之间的任何值。

头文件float.h定义了名字FLT_MAXDBL_MAXLDBL_MAX,分别表示floatdoublelong double所能存储的最大值。而FLT_MINDBL_MINLDBL_MIN则分别表示floatdoublelong double能够存储的最小值。这个文件另外还定义一些和浮点值的实现有关的某些特性的名字,例如浮点数所使用的基数、不同长度的浮点数的有效数字的位数等。

浮点数字面值重视写成十进制的形式,它必须有一个小数点或一个指数,也可以两者都有。

例:

1
3.14159  1E10 25.  .5  6.023e23

浮点数字面值在缺省情况下都是double类型的,除非它的后面跟一个 L 或 l 表示它是一个long double类型的值,或者跟一个 F 或 f 表示它是一个float类型的值。

指针

指针是 C 语言如此流行的一个重要原因。指针可以有效地实现诸如树和表这类高级数据结构。

变量的值存储与计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用,就像一条街道上的房子由它们的门牌号码标识一样。指针只是地址的另一个名字罢了。指针变量就是一个其值为另外一个(一些)内存地址的变量。C语言拥有一些操作符,你可以获得一个变量的地址,也可以通过一个指针变量取得它所指向的值或数据结构。

指针常量(pointer constant)

指针常量与非指针常量在本质上是不同的,因为编译器负责把变量赋值给计算机内存中的位置,程序员实现无法指定某个特定的变量将存储到内存中的哪个位置。因此,你通过操作符获得一个变量的地址而不是之间把它的地址写成字面值常量的形式。

例如,如果我们希望指定变量 xyz 的地址,我们无法书写一个类似0xff2044ec这样的值,因为我们不知道这是不是编译器实际存放这个变量的内存位置。事实上,当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不相同。因此,把指针常量表达式数值字面值的形式几乎没有用处,所以 C 语言内部并没有特地定义这个概念。

字符串常量(string literal)

字符串常量就是一串以NUL字节结尾的零个或多个字符。字符串通常存储在字符数组中,这也是 C 语言没有显示的字符串类型的原因。由于 NUL 字节是用于终结字符串的,所以在字符串内部不能有NUL字节。不过,在一般情况下,这个限制并不会造成问题。之所以选择NUL作为字符串的终止符,是因为它不是一个可打印的字符。

字符串常量的书写方式是用一对双引号包围一串字符,如下所示:

1
"Hello"  "\aWarning\a"  "Line 1\nLine2" ""

最后一个例子说明字符串常量(不像字符常量)可以是空的。尽管如此,即使是空字符串,依然存在作为终止符的 NUL 字节。

如果在使用字符串时需要修改字符串,请把它存储于数组中。

之所以把字符串常量和指针放在一起讨论,是因为在程序中使用字符串常量会生成一个 “指向字符的常量指针”。当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身。因此,你可以把字符串常量赋值给一个 “指向字符的指针”,后者指向这些字符所存储的地址。但是,你不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。

基本声明

基本的数据类型已经知道了,接下来就要学习如何声明变量了。

变量声明的基本格式:

1
说明符 声明表达式列表

对于简单的类型,声明表达式列表就是被声明的标识符的列表。对于更为复杂的类型,声明表达式列表种的每个条目实际上是一个表达式,显示被声明的名字的可能用途。

说明符(specifier) 包含了一些关键字,用于描述被声明的标识符的基本类型。说明符也可以用于改变标识符的缺省存储类型和作用域。

相等的整型声明:

short signed short unsigned short
int signed int unsigned int
long signed long unsigned long

初始化

在一个声明中,你可以给一个标量变量指定一个初始值,方法是在变量名后面跟一个等号(赋值号),后面是你想要赋给变量的值。

例:

1
int i=1;

这条语句声明 i 为一个整型变量,其初始值为 15。

声明简单数组

声明一个一维数组,在数组名后面要跟一对方括号,方括号里面是一个整数,指定数组中元素的个数。

例:

1
int values[10];

上述代码中我们声明了一个整型数组,数组包含 10 个整型元素。

我们可以从另一个角度理解上述代码,我们利用 名字 values 加一个下标,产生一个类型为 int 的值(共有 10 个整型值)。

数组的下标总是从 0 开始,最后一个元素的下标是元素的数目减 1。

C 数组另一个值得关注的地方是,编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。这种不加检查的行为有好处也有坏处。好处是不需要浪费时间对有些已知是正确的数组下标进行检查。坏处是这样做将使无效的下标引用无法被检测出来。

一个良好的经验法则是:

如果下标值是从那些已知是正确的值计算得来,那么就无需检查它的值。如果一共用作下标的值是根据某种方法从用户输入的数据产生而来的,那么在使用它之前必须进行检测,确保它们位于有效的范围之内。

声明指针

声明表达式也可用于声明指针。

例:

1
int *a;

这条语句表示 *a 产生的结果类型是int。知道了*操作符执行的是间接访问操作以后,我们可以推断 a 肯定是一个指向int的指针。

警告:C 在本质上是一种自由形式的语言,这很容易诱使你把星号写在靠近类型的一侧,如:int * a;
这个声明与前面一个声明具有相同的意思,而且看上去更为清楚,a 被声明为类型为 int* 的指针。

隐式声明

C 语言中有几种声明,它的类型名可以省略。

typedef

C语言支持一种叫作 typedef 的机制,它允许你为各种数据类型定义新名字。typedef 声明的写法和普通的声明基本相同,只是把 typedef 这个关键字写在声明的前面。

例:

1
typedef char * string;

这个声明把标识符 string 作为指向字符的指针类型的新名字。你可以像使用基本类型一样在下面的声明中使用这个新名字。

例如:

1
string name;

声明 name 是一个指向字符的指针。

使用typedef声明类型可以减少使声明变得又臭又长的危险,尤其是那些复杂的声明。而且,如果你以后觉得应该修改程序所使用的一些数据的类型时,修改一个typedef声明比修改程序中与这种类型的所有变量(和函数)的所有声明要容易得多。

提示:你应该使用typedef而不是 #define来创建新的类型名,因为后者无法正确地处理指针类型。

例如:

1
2
#define d_ptr_to_char char *
d_ptr_to_char a,b;

正确地声明了 a,但是 b 却被声明为一个字符。在定义更为复杂的类型名字时,如函数指针或指向数组的指针,使用typedef更为合适。

常量

通过使用 const 关键字来声明常量。

例:

1
2
int const a;
const int a;

这两条语句都把 a 声明为一个整数,它的值不能被修改。

当然,由于 a 的值无法被修改,所以你无法把任何东西赋值给它。如此一来,你怎样才能让它在一开始拥有一个值呢?

有两种办法:首先,你可以在声明时对它进行初始化,如下所示:

1
int const a =10

其次,在函数中声明为 const 的形参在函数被调用时会得到实参的值。

当涉及到指针变量时,情况就变得更加有趣,因为有两样东西都有可能成为常量——指针变量和它所指向的实体。

例:

1
int *pi;

pi 是一个普通的指向整型的指针。而变量

1
int const *pci;

则是一个指向整型常量的指针。你可以修改指针的值,但你不能修改它所指向的值。相比之下:

1
int * const cpi;

则声明 cpi 为一个指向整型的常量指针。此时指针是常量,它的值无法修改,但你可以修改它所指向的整型的值。

1
int const * const cpci;

最后,在 cpci 这个例子里,无论是指针本身还是它所指向的值都是常量,不允许修改。

当你声明变量时,如果变量的值不会被修改,你应当在声明中使用 const 关键字。

#define指令是另一种创建名字常量的机制。

例:

1
2
#define PI=3.1415926535
const double pi=PI;

在这种情况下,使用#define比使用 const 变量更好。因为只要允许使用字面值常量的地方都可以使用前者,比如声明数组的长度。 const 变量只能用于允许使用变量的地方。

提示:名字常量非常有用,因为它们可以给数值起符号名,否则它们就只能写成字面值的形式

作用域

当变量在程序的某个部分被声明时,它只有在程序的一定区域才能被访问。这个区域由标识符的作用域(scope)决定。标识符的作用域就是程序中该标识符可以被使用的区域。例如,函数的局部变量的作用域局限于该函数的函数体。这个规则意味着两点。首先,其它函数都无法通过这些变量的名字访问它们,因为这些变量在它们的作用域之外便不再有效。其次,只要分属不同的作用域,你可以给不同的变量起同一个名字。

编译器可以确认 4 种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。标识符声明的位置决定它的作用域。

代码块作用域

位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope) ,表示它们可以被这个代码块中的所有语句访问。

当代码块处于嵌套状态时,声明与内层代码块的标识符的作用域到达该代码块的尾部便告终止。然而,如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。

提示:你应该避免在嵌套的代码块中出现相同的变量名。

文件作用域

任何在代码块之外声明的标识符都具有文件作用域(file scope),它表示这些标识符从它们的声明之处知道它所在的源文件结尾处都是可以访问的。在文件中定义的函数名也具有文件作用域,因为函数名本身并不属于任何代码块。我应该指出,在头文件中编写并通过#include指令包含到其他文件中的声明就好像它们是直接写在那些文件中一样。它们的作用域并不局限于头文件的文件尾。

原型作用域

原型作用域(prototype scope) 只适用于在函数原型中声明的参数名。在原型中(与函数的定义不同),参数的名字并非必需。但是,如果出现参数名,你可以随你所愿给它们取任何名字,它们不必与函数定义中的形参名匹配,也不必与函数实际调用时所传递的实参匹配。原型作用域防止这些参数名与程序其他部分的名字冲突。事实上,唯一可能出现的冲突就是在同一个原型中不止一次地使用同一个名字。

函数作用域

最后一种作用域的类型是函数作用域(function scope)。它只适用于语句标签,语句标签用于 goto 语句。基本上,函数作用域可以简化为一条规则——一个函数中的所有语句标签必须唯一。

链接属性

当组成一个程序的各个源文件分布被编译之后,所有的目标文件以及那些从一个或多个函数库中引用的函数链接在一起,形成可执行程序。

标识符的链接属性(linkage) 决定如何处理在不同文件中出现的标识符。标识符的作用域与它的链接属性有关,但这两个属性并不相同;

链接属性一共有 3 种——external(外部)、internal(内部)和 none(无)。没有链接属性的标识符(none)总是被当作单独的个体,也就是说该标识符的多个声明被当作独立不同的实体。

存储类型

变量的存储类型(storage class)是指存储变量值的内存类型。变量的存储类型决定变量何时创建

变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)变量。对于这类变量,你无法为它们指定其他存储类型。静态变量在程序运行之前创建,在程序的整个指向期间始终存在。它始终保持原先的值,除法给它赋一个不同的值或者程序结束。

static关键字

当用于不同的上下文环境时,static 关键字具有不同的意思。

当它用于函数声明时,或用于代码块之外的变量声明时,static 关键字用于修改标识符的链接属性,从 external 改为 internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件种访问。

当它用于代码块内部的变量声明时,static 关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。用这种方式声明的变量在程序执行之前创建,并在程序的整个执行期间一直存在,而不是每次在代码块开始执行时创建,在代码块执行完毕后销毁。即存储在数据段而不是堆栈。