(作者:徐诚 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位存储空间后,字符型变量可储存的最小值为-27,最大值为27-1,即-128~127;整型变量可存储的最小值为-231,最大值为231-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~264-1。在不使用科学计数法的条件下,变量a可以用来保存C语言中最大的正整数264-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,并且为其赋值。声明语句以后,任何赋值或修改常量变量数值的语句,都将造成编译错误。