(作者:徐诚 http://blog.csdn.net/shizhebsys 保留版权)
C 语言程序中用于运算的数据可以分为常量与变量两种基本类型。常量是直接在代码中所出现的数据,运算过程中不能修改常量值。变量是 C 语言程序在内存中为数据动态划分出的定长存储空间,运算过程中可以修改变量值。为了让读者能够更深入的了解常量与变量的本质,在介绍常量与变量前,我们首先需要认识计算机内部数据存储机制。
3.1.1 内部存储器、寄存器和数据存储形式
在计算机的电路中,用于存放运算数据的设备有内部存储器(内存)和寄存器。内存是主要的存储设备,对应的电路模块称之为内存条。寄存器的容量非常小,但是处于 CPU 的内部,因此访问速度非常快。
数据以二进制形式存储在内存或寄存器中,最小的存储单位为位( bit ),每次可操作的最小存储单位为字节( byte )。例如十进制整数 87 对应的二进制数为 01010111 ,至少需要 1 字节的存储空间,在内存中的存储形式如图 3.1 所示。
图 3.1 1 字节存储空间模拟图
内存地址用以表示字节单元在内存中的位置,计算机可通过内存地址访问相应的内存单元。由于计算机的结构差异非常大,每次运行程序时内存的状况也不相同,因此存放数据的位置也不一样。在 C 语言程序中,变量是程序运行时动态划分的内存单元,其本质是某一内存单元在程序中的映射。这样,设计程序时不用考虑数据具体存放的位置,只用变量的名称就可以访问相应的内存地址。变量的声明形式为:
[modifier] type name [= value];
其中, modifier 是修饰符, name 是变量的名称, value 是在声明时为变量赋予的初始值。声明语句结束后,必须使用分号结束一行。
例如为了保存十进制整数 87 声明了一个字符型变量,变量名为 a 。该变量规定的长度为 1 字节,程序运行时,操作系统会为变量 a 划分出 1 字节的存储空间,通过名称 a 可对相应的存储空间进行数据的读或写操作。如下列源代码所示:
#include <stdio.h> // 包含基本输入输出头文件
int main() // 主函数
{
char a; // 声明字符型变量 a
a = 87; // 向变量 a 写入数据
printf("%d", a); // 输出变量 a 的数值到终端
return 0; // 退出程序
}
代码运行时,“ char a ”表达式进行声明操作,要求操作系统为字符型变量 a 分配内存。假设分配的内存地址为 0x30 ,名称 a 就可以代表以该地址开始,长度为 1 字节的存储空间。操作系统会将这一段内存空间保护起来,不会将同样的空间分配给其他程序或变量。“ a = 87 ”表达式对 a 进行赋值操作,实际上就是把数据写入到相应的内存单元中。程序结束后,操作系统会进行内存回收,分配给变量 a 的内存空间被标为空闲,其他程序可以获得该空间。
不同数据类型的变量差别在于对应存储空间的长度,该长度还会因为计算机硬件结构和编译器类型的差别而不同。例如整型变量的长度对应于凌动处理器和 GCC 编译器的长度为 4 字节,存放负整数 -87 的整型变量在内存中的存储形式如图 3.2 所示。
图 3.2 4 字节存储空间模拟图
整型变量的长度为 4 个字节,但是只需要第 1 个字节的地址就能访问到整个空间。因为变量类型已经为变量定义了存储空间长度的信息,第 1 个字节的地址称为首地址,只用通过偏移量就能得到其他字节的地址。
为了保存正负符号,存储空间中的第 1 位是符号位。正数对应 0 ,负数对应 1 。被符号占用 1 位存储空间后,字符型变量可储存的最小值为 -2 7 ,最大值为 2 7 -1 ,即 -128 ~ 127 ;整型变量可存储的最小值为 -2 31 ,最大值为 2 31 -1 。 0 被作为正数保存,因此正数最大值的数值比负数最大值的数值要少 1 。
存储到寄存器的原理与存储到内存非常相似,在变量声明表达式前加入 register 标识符可将变量声明为寄存器变量。如下例所示:
register int a; // 声明寄存器整型变量 a
上述语句声明了寄存器整型变量 a ,其长度同样是 4 字节,并且有自己的地址。但是由于寄存器的资源非常有限,通常只将需要高频率访问的变量声明为寄存器变量。操作系统和编译器考虑到程序性能优化的问题,并不一定会将用户声明的寄存器变量保存在寄存器中,而是转换为普通变量。
一切在代码中直接出现的数据都是常量,例如“ a = 87 ”表达式中的数值 87 即常量。常量在内存中的存储位置不被程序设计者关心,程序中也无法直接得到常量的地址,因此常量是不可修改的。由此我们可以用内存地址是否能被程序得到来区别常量与变量,这也是常量与变量的本质性区别。
3.1.2 数据类型
认识了数据存储形式后,数据类型就比较容易理解。本小节所讨论的数据类型指的是 C 语言中原始的数据类型,实际上数据类型直接的差别在于存储空间长度。另外,还将涉及是否保存正负数符号,以及是否使用浮点方式来保存小数和指数。有正负符号的数称为有符号数,没有正负符号的数称为无符号数。有符号数可以存储负数,无符号数只能存储正数。不使用浮点形式的数称之为整数,使用浮点形式的数称之为浮点数。除此以外还有一种空值类型,它不能保存任何数据,存储空间长度为 0 。
C 语言的所有类型都是从 5 种最原始的类型发展而来的,见表 3.1 所示。
表 3.1 ANSI C 标准基本类型的字长与范围表
类型 |
说明符 |
长度 |
值域 |
字符型 |
char |
1 字节 |
-128 ~ 127 |
整型 |
int |
4 字节 |
- 2147483648 ~ 2147483647 |
单精度浮点型 |
float |
4 字节 |
约精确到 6 位数 |
双精度浮点型 |
double |
8 字节 |
约精确到 12 位数 |
空值型 |
void |
0 字节 |
无值 |
其中字符型和整型有无符号数和有符号数的差别,区别在于说明符前加入了 signed 和 unsigned 修饰符,见表 3.2 所示。
表 3.2 无符号与有符号类型的字长与范围表
类型 |
说明符 |
长度 |
值域 |
无符号字符型 |
unsigned char |
1 字节 |
0 ~ 255 |
有符号字符型 |
signed char |
1 字节 |
-128 ~ 127 |
无符号整型 |
unsigned int |
4 字节 |
0 ~ 4294967295 |
有符号整型 |
signed int |
4 字节 |
- 2147483648 ~ 2147483647 |
由于字符型和整型默认为有符号数,所以通常在声明时可以省略 signed 修饰符。另外,整型数据可以使用 short 和 long 修饰符来定义为短整型和长整型,见表 3.3 所示。
表 3.3 短整型和长整型的字长与范围表
类型 |
说明符 |
长度 |
值域 |
短整型 |
short int |
2 字节 |
-32768 ~ 32767 |
整型 |
long int |
4 字节 |
- 2147483648 ~ 2147483647 |
长整型 |
long long int |
8 字节 |
-9.223372e+18 ~ 9.223372e+18 |
在使用短整型和长整型时,可省略 int 说明符。因此,声明短整型可使用 short 修饰符作为说明符,声明长整型可使用 long long 作为说明符,而 long 和 int 说明符是等价的。 unsigned 、 signed 修饰符与 short 、 long 说明符可以同时使用,如下例所示:
unsigned long long a; // 声明无符号长整型变量 a
该行代码声明了无符号长整型变量 a ,其值域范围为 0 ~ 2 64 -1 。在不使用科学计数法的条件下,变量 a 可以用来保存 C 语言中最大的正整数 2 64 -1 。
3.1.3 常量的形式
在 C 语言中,常量出现的形式共有 4 种,分别是直接常量、符号常量、枚举常量和常量变量。其中,前 3 种是严格意义上的常量,而常量变量是一种特殊的常量。
1 .直接常量
所有在 C 语言源代码中直接出现的数值、字符和字符串都是直接常量。如下列源代码所示:
float pi = 3.141593; // 声明单精度浮点型变量 pi 并赋值
char c = 'a'; // 声明字符型变量 c 并赋值
printf("a cup of coffee"); // 在终端上输出一行字符串
代码中定义了 2 个变量,并使用直接常量为其赋值。其中,数值 3.141593 在类型上属于浮点型常量,“ a ”属于字符型常量。最后一行使用 printf() 函数输出了字符串“ a cup of coffee ”,此处使用的是字符串常量。
注意:字符型常量必须使用单引号包围,如 'a' 。字符串常量必须使用双引号包围,如 "a cup of coffee" 。如果使用双引号包围一个字符,如 "a" ,那么编译器会认为这是一个字符串,并在其后自动加上字符串结束符。如果用单引号包围一个字符串,如 'a cup of coffee' ,编译器将认为这是语法错误,并抛出错误提示信息。
2 .符号常量
使用“ #define ”定义的常量称之为符号常量。符号常量定义的形式为:
#define NAME value
其中, NAME 是符号常量的名称, value 必须是一个直接常量。通常,符号常量声明在源代码的最上方,并且用大写字母作为其名称。如下例所示:
#include <stdio.h> // 包含基本输入输出头文件
#define PI 3.141593 // 定义符号常量 PI
#define C 'a' // 定义符号常量 C
#define S "a cup of coffee" // 定义符号常量 S
int main() // 主函数
{
printf("%f/n", PI); // 输出符号常量所代表的数值
printf("%c/n", C);
printf("%s/n", S);
return 0; // 主函数结束
}
代码中定义了 3 个符号常量,在主函数中,这些符号常量所代表的数值被 printf() 函数输出。我们可以简单的认为,符号常量所做的仅仅是在源代码中进行的字符串替换。编译器编译时,所以常量 PI 都会被替换为 3.141593 。
使用符号常量有三点好处,其一是易于记忆,例如我们可以为某个常量定义一个较容易理解的名称,如 PI 。其二是表达简洁,在上例的代码中,仅仅用一个字母 S 就能代理整个字符串“ a cup of coffee ”。其三是数值容易修改,如果某个直接常量需要多次使用,一旦该常量的值必须被调整时,往往需要修改多处代码;而使用符号常量代替了源代码中所有直接常量后,修改时只用在符号常量定义部分修改。因此我们建议读者尽量在代码中使用符号常量。
注意:符号常量定义的行尾不需要使用分号“ ; ”结束该语句,否则会造成语法错误。
3 .枚举常量
使用 enum 定义的常量称之为枚举常量,它是一种聚合类型。枚举常量定义的形式为:
enum name {CON1 [= INT], CON2 [= INT], …};
其中, nume 为枚举类型名称, CON1 、 CON2 为枚举成员名称。枚举成员的数值在定义后不可改变,并且能作为常量使用,被称为枚举常量。枚举成员可用整型常量赋值,第 1 个枚举成员默认值为 0 ,其后枚举成员的默认值为前一个枚举成员值加 1 的结果。如下例所示:
enum week {MON = 1, TUE, WED, THU, FRI, SAT, SUN}; // 定义枚举类型和成员,将 MON 的值设置为 1
printf("%d", SAT); // 输出成员 SAT 的值
代码中定义了枚举类型 week ,其中有 7 个成员,第 1 个成员 MON 的值设置为 1 。然后, printf() 函数输出了成员 SAT 的值。根据枚举类型的默认值规则可知, SAT 的值为 6 。
4 .常量变量
使用 const 修饰符声明的变量称之为常量变量。从本质上来说,常量变量依然属于变量的一种,但是程序运行过程中不能修改其值。如下例所示:
const int id = 15; // 声明常量变量 id 并赋值
代码中声明了常量变量 id ,并且为其赋值。声明语句以后,任何赋值或修改常量变量数值的语句,都将造成编译错误。