掌握C语言数据类型

什么是数据类型?

计算机编程语言是用来控制计算机的行为及操作,协助人们解决现实中的问题,其能表达的数据类型也是从实际中提取并抽象出来形成的数据结构描述。

例如:数学中数的基础分类有正整数、负整数、小数等类别,数学中所有关于数的运算都是在基础分类上进行的。计算机出现之前,数学家们用稿纸进行大量的数学运算以求证数学问题和科学计算,这耗费了数学家们太多的精力。随着计算机科技的发展,大量复杂的数学运算交给计算机来执行,极大提高了计算效率,也让数学家们从复杂地数学运算中摆脱出来。

数学运算包含大量的计算表达式,计算机程序需要有连续处理算式和计算数据的能力,下面是一个简单的四则运算算式:

15.8+20*31.5-30

计算机程序要处理上述算式,就需要具备存储小数、整数、运算符的能力。C语言提供了存储小数、整数、运算符的基本数据类型。下图是算式中数据类型到C数据类型的映射图。

图 2-1数学算式数据类型到C语言数据类型的映射

 

变量与数据类型

使用变量可以将数学算式中的数存储起来,下面讨论如何声明或定义一个变量。在C语言中声明一个变量和定义一个变量存在区别:声明变量是通知编译器变量的类型和名字,编译解析声明变量时,不会为该变量分配存储空间,直至该变量被赋值时,才会为变量分配存储空间;定义变量是在声明变量的同时,为变量赋一个初始化值,编译器解析定义变量时,将为该变量分配存储空间。

声明一个变量

语法规则
数据类型  变量名;

其中,数据类型可以是C语言支持的任何数据类型;变量名为声明的变量名称。

示例:声明整型变量

int   number;

示例:声明字符型变量

char  op;

定义一个变量

声明变量的同时并对变量直接赋值,称为定义一个变量。如果在声明变量时没有对变量进行赋值,则应在后面的程序语句中为变量赋值。

示例:定义整型变量

int   number= 30;

示例:为已声明的变量赋值

op=’+’;

下面简要说明一个解析数学算式并存储运算符和运算数的示例,为说明问题起见,给出一个简单数学算式:8.25+30。

声明三个变量用来存储运算数和运算符

float  floatNum;
int   intNum;
char  op;

程序在计算上述数学算式时,首先从左到右扫描数学算式。假设本次扫描不考虑优先级运算,只是完成提取运算数和运算符的功能。扫描过程如下:如果是运算数,判断是整数还是小数,整数赋值给intNum,如果是小数赋值给floatNum,如果是运算符赋值给op。下图是扫描完成后,变量在内存储器的存储情况。

图 2-2 不同数据类型的变量在存储器的存储情况

从图2-2可以看出,不同数据类型的变量在存储器占用的空间也不相同。数据类型为字符型的变量在存储器占用一个字节的空间。C语言对整数类型并没有严格规定其长度(占用存储空间的字节数),只做了宽泛的限制。图2-4整数类型占用4个字节的存储空间,是指在32位操作系统下,整数类型占用4个字节的存储空间。

基础数据类型

C语言基本数据类型见图2-3。

图 2-3 C语言基本数据类型

C语言基本数据类型包括数值、非数值两种类型。数值类型又分为整数类型和非整数类型,整数类型包括整型、短整型和长整型,非整数类型包括单精度浮点和双精度浮点。非数值包括字符类型。

整数类型

C语言的整数类型可以说是复杂多样,根据存储空间和数值范围可分short(短整型,占用2个字节)、int(整型,机器字长)、long(长整型,32位环境占用4个字节,64位环境占用8个字节)、long long(加长整型,占用8个字节),根据整数分类又可分为有符号整数和无符号整数。

对整数而言,C语言为什么要定义这么多不同的类型呢?

在发明C语言的年代,计算机的存储资源是非常珍贵的,程序员设计和编写程序对存储资源要精打细算,否则存储资源就不够用了。在存储资源紧张的情况下,程序员优先选用占用存储空间小且能够满足数据处理要求的整数类型。即使在当前存储资源较为宽松的环境下,程序员在编写代码时,通常能预想到需要使用到的数据范围的大小,这样在处理一个数据时,可以从语言所提供的类型中选用最合适的类型来存储数据。

实际上,C语言对short、int、long 并没有严格规定其长度(占用存储空间的字节数),只做了宽泛的限制:short 至少占用 2 个字节;int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节;short 的长度不能大于 int,long 的长度不能小于 int。

由此可见,short 并不一定真的”短“,long 也并不一定真的”长“,它们有可能和 int 占用相同的字节数。在 16 位系统下,short 的长度为 2 个字节,int 也为 2 个字节,long 为 4 个字节。在32位和64位操作系统下,short 的长度为 2 个字节,int 为 4 个字节,long 也为 4 个字节,long long类型为8个字节。

那么问题来了,在实际编程中如何确定一个整型占用的存储空间大小呢?

C语言提供了sizeof运算符来获取数据类型占用的字节数,sizeof运算符传入的参数可以是数据类型,也可以是变量名称。例如下面的代码分别输出了不同数据类型的字节数:

#include <stdio.h>
void main()
{
char ch = 'a';
printf("short:%d字节\n",sizeof(short));
printf("int:%d字节\n",sizeof(int));
printf("long:%d字节\n",sizeof(long));
printf("long:%d字节\n",sizeof(long long));
printf("ch:%d字节\n",sizeof(ch));
}

执行上述代码,输出结果为:

 

整数类型有正整数和负整数之分,在C语言中,规定整型的最高位为符号位,最高位为“0”表示正数,最高位为“1”表示负数,其它位表示数值。因此整型类型的数据能够表示的最小值为:-2n-1 —2n-1-1(n为该类型所占存储空间的二进制位数)。

一个数在计算机中的二进制表示形式,  叫做这个数的机器数。例如:十进制数+3,使用8个二进制位表示,转换成二进制就是00000011。如果是 -3 ,就是10000011 。00000011和10000011就是机器数。

因为最高位是符号位,因此机器数的形式值不一定等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。为区别起见,后面的内容将带符号位的机器数对应的真正数值称为机器数的真值。

下面我们编写一个简单的程序,分别输出+3和-3的机器数,查看机器数的形式值是否为机器数的真值。

#include <stdio.h>
void printf_binary(char n);
void main()
{
char positive = 3;
char negative  = -3;
printf_binary(positive);
printf_binary(negative);
}
// 输出二进制数
void printf_binary(unsigned char n)
{
char i=0;
for(i=0; i<8; i++ ){
if(n&(0x80>>i)){
printf("1");
}else{
printf("0");
}
}
printf("\n");
}

char为字符类型,占用一个字节的存储空间,值范围为-128~127。printf_binary(unsigned char n)函数将十进制数转换为二进制数输出,函数及函数内的语句同学们先不要急于了解,后面的课程都会讲到,重点关注程序的输出结果

从输出结果可以看出,+3的机器数为0000011,其形式值为3,机器数的形式值和真值相同。-3的机器数为11111101,其形式值为253,真值为-3,机器数的形式值和真值不相同,若把+3和-3的机器数相加,结果为0,运算结果正确。实际上,计算机是以补码的方式来存储数值的。

计算机为什么以补码方式来存储数值呢?对于一个数,计算机要使用一定的编码方式进行存储,原码、反码、补码是机器存储一个具体数字的编码方式。

原码就是符号位加上真值的绝对值,即最高位是符号位,其余位用来表示值。例如1个8位的二进制数:

[+3]   原码:0000 0011
[-3]   原码:1000 0011

原码比较容易理解,但不利于计算机的加减计算,因为原码带符号位,若对两个原码表示的机器数直接进行加减运算,运算结果并不正确。若让计算机能够识别符号位,计算机的运算电路设计就会非常复杂。因此计算机一般不采用原码的方式来存储数值。

既要让符号位参与运算,又要让运算电路设计简单,使用补码存储数值就会解决这个问题。

补码应用了数学中同余的概念,同余的两个数具有互补关系。给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,a和b具有互补关系。

回到整数类型, 补码的计算规则为最高位是符号位,0表示正数,1表示负数,正数的补码等于本身,负数的补码等于反码(正数的反码等于本身,负数的反码除符号位外,各位取反)+1。

例如:

 

前面讨论的都是有符号整数,可以表示正负数。若只需要处理正整数,可以在上述类型关键字前面添加unsigned关键字表示无符号整数,两个关键字用空格隔开,因为不需要符号位,因此无符号整数表示的范围为:0 —2n-1。下表列出了符号整数的类型。

表 2-1无符号整数

类型 存储空间 描述 系统位数
unsigned int 2~4字节 无符号整型 16位或32位系统
unsigned short 2字节 无符号短整型 16位或32位系统
unsigned long 4字节 无符号长整型 16位或32位系统

整型变量可按如下方式声明:

int  pageNumber;
long int  size;
short   age;
unsigned short  readCount;

在一条语句中,可以声明多个同一类型的整型变量,每个变量之间用逗号分隔:

int  pageNumber, likeNumber,readCount;

整型变量可按如下方式定义:

int   pageNumber=230;
short  age = 21;
unsigned short  readCount=1260;

在定义变量或为变量赋值时,常常会用到一些数值,这些值通常称为字面值。字面值有三种不同的表示形式:十进制、八进制和十六进制。

八进制表示:在八进制数值前面加前缀数字0,其数码取值为0—7,例如:023、0457、01329等;

十六进制表示:前缀为“0X”或“0x”,数码取值0—9、A—F、或a—f。例如:0X2A、0XA0、0Xffff等;

十进制表示:既无前缀也无后缀。例如:236、56、7890等。

在字面值后面可以添加u或U(unsigned)、l或L(long)、u/U与l/L的组合(如:ul、lu、Lu等),表示该字面值的类型。例:100u; 123u; 0x123L;

浮点类型

浮点类型用来存储实数,为什么在C中把实数称为浮点数呢?在C语言中实数是以指数形式存放在存储单元中,类似于科学计数法形式,如:2.1E5、3.7e-2等,其中e或E之前必须有数字,且e或E后面的指数必须为整数。

一个大于0的实数可以用下面指数的方式来表示:

a * 10n (1<=a<10,n为整数)

其中,a是该数值的有效位数,有效位数从左边第一个不是0的数起,到末尾数字为止,所有的数字(包括0,科学计数法不计10的n次方),称为有效数字。例如:光速是3E8,其有效数字是1位,值是3;世界人口数大约是6.1E9,其有效数字是2位,值是6.1。

n是该数值的整数部分减1的正整数。

一个小于0的实数可以用下面指数的方式来表示:

a * 10-n (1<=a<10,n为整数)

a的取值同上面相同,n的取值为原数中左边第一个不为0的数字前面所有的0的个数(包括小数点前面的0)。

一个实数表示为指数形式,通过移动小数点可以有多种表示方法。例如:实数3.14159可以表示为以下的指数形式:

3.14159E0
0.314159E1
314.159E-2

上面的指数形式因为小数点所在位置不同,指数也不同,但实际表示的数值都是3.14159。因此只要在小数点位置浮动的同时改变指数的值,就可以保证它的值不会改变。由于小数点位置可以浮动,所以实数的指数形式称为浮点数。

在C语言中,浮点类型有float和double两种,分别表示单精度浮点数和双精度浮点数。精度是指描述一个数值的准确程度,在数学运算中,经常会用到近似数,近似数与原数值非常相近,但又不完全符合原数值,只能说在某种程度上近似。精度与近似数相似,也是用一个与原数值非常相近的数代替原来的数值。

前面说过存储一个数值所用的字节越多,其精度越高,数值范围也越大。由此看来,精度与存储字节数密切相关,float类型的存储空间是4个字节,其表示的值范围约为10-38到1038,double类型的存储空间是8个字节,其表示的值范围约为10-308到10308,float存储数值的精度和范围要小于double存储数值的精度和范围。因此,float是单精度数值,double是双精度数值。

图 2-4浮点型变量占用的存储空间

实数的指数形式有多种表示方法,计算机在存储实数时,一般采用标准化的指数形式,即小数点前的数字为0,小数点后第1为数字不为0。例如:0.314159E1就是3.14159的标准化的指数形式。

float类型的变量占用4个字节的存储空间,系统存储float类型的实数时,将实数分为小数部分和指数部分。最高位为符号位,小数部分占23位,剩余的8位存储实数的指数部分(包括指数的符号)。由于用二进制形式表示一个实数以及存储单元的长度是有限的,因此不可能得到完全精确的值,只能存储成有限的近似值。小数部分占的位数愈多,数的有效数字愈多,精度也就愈高。指数部分占的位数愈多,则能表示的数值范围越大。

double类型的变量占用8个字节的存储空间,最高位为符号位,小数部分占52位,剩余的11位存储实数的指数部分(包括指数的符号)。

float变量可按如下方式定义:

float  price = 12.35f;
float  average = 89.2f;

double变量可按如下方式定义:

double  pi = 3.14159265;
public  double  average = 89.2987017;

字面值赋值给float变量时,数值尾部要加上小写“f”或大写“F”声明为float类型的数值,不然编译器会给出从“double”到“float”截断的警告。因为在C语言中,带小数的字面值默认为是double类型,double类型转换为float类型,自然要损失精度,位数被截断了。

C语言还提供了long double类型,表示长双精度浮点数,但C语言并没有long double的确切精度,对于不同系统平台可能有不同的实现,有的是8字节,有的是10字节,有的是12字节或16字节,C语言函数sizeof(long double)可以输出当前系统平台下long double占用的字节数。

浮点类型字面值的后缀有:f或F(单精度浮点数)、l或L(长双精度浮点数),因为浮点型常数总是有符号的,所以没有u或U后缀。例:1.23e5f;  1.23L;  -123.45f。

字符类型

在编写程序时,我们还经常会遇到需要存储并操纵字符型数据的情况。例如:计算数学算式时,需要存储运算符,这时就需要一种可以存储单个字符数据(字符包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号)的数据类型。C语言提供了一种char数据类型,可以满足存储单个字符的需要。

计算机中的字符一般采用ASCII码表示,ASCII码是一种标准的字符编码方式,规定每个字符对应一个数,例如:十进制数65对应大写字母A,97对应小写字母a。ASCII编码最后一次更新是在1986年,到目前为止共定义了128个字符。

ASCII字符与编码映射范围表:

标准ASCII码使用7位二进制数来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符。char数据类型占用一个字节的存储空间,可以表示8位二进制数,完全可以存储ASCII码。

char变量可按如下方式定义:

char  code=’a’, op=’*’,digit=’0’;

任意单个字符,加单引号。

char  code =97

可以直接把ASCII码十进制数97赋值给char型变量code。

上面例句中的‘a’,  ’*’,  ‘0’为字符字面值,字符字面值需要使用一对单引号括起来,括号内只能包含一个字符。除了字符常量可以赋值给char型变量外,0~255的整数常量也可以赋值给char型变量。char型变量通常用来存储字符,若存储的数值超过ASCII码范围,变量的值没有实际意义。

标准C语言没有提供byte数据类型,byte占1个字节的存储空间,表示的数值范围是0~255。若需要使用byte数据类型,可以使用unsinged char来表示byte数据类型。

类型转换

C语言是强类型语言,变量的数据类型被指定后,会一直保持该数据类型。同时C语言对参与赋值运算和算术运算的操作数数据类型要求必须一致,当参与运算的操作数数据类型不一致时,就需要对操作数的数据类型进行类型转换,把参与运算的操作数转换为同一数据类型后,再进行运算。

隐式类型转换

例如:

double PI = 3.14;
int  radius = 5;
double s;
s = PI * PI * radius;

在上面的例句中,表达式PI * PI * radius有二个操作数,PI是double类型,radius是整数类型。因为操作数的数据类型不一致,C语言编译器会对数据类型做强制转换,将radius的整数类型强制转换为double类型。

这种转换是C编译器自动进行的,开发者不需要进行任何操作,由C编译器自动完成,这种类型的转换也称为隐式转换。

由于不同的数据类型存储空间和表示的数值精度是不同的,因此在数据类型的转换过程中,就会存在数值精度丢失和数值溢出的问题。例如:double类型转换为float类型,数值精度就会丢失;long类型转换为int类型时,如果long类型变量存储的数值超过了int类型能够存储的数值范围,就会发生数值溢出。

为了避免在转换过程中发生数值精度丢失和溢出的问题,隐式转换都会遵循从低级数据类型到高级类型的转换规则,也可以说是从数值存储精度小的类型到存储数值精度高的类型转换。这些数据类型按数值存储范围大小依次为:

short->int->long->float->double

表2-4列出了数据类型隐式转换的一般规则。

表 2-2 数据类型隐式转换的一般规则

操作数1的数据类型 操作数2的数据类型 转换后的数据类型
short\char int int
short\char\int long long
short\char\int\long float float
short\char\int\long\double double double

【例2-3】类型的隐式转换练习

程序清单 sample.c

#include <stdio.h>
int main()
{
// 声明char类型的变量
char chTemp = 65;
// 声明int类型的变量
int nTemp = 34;
// 声明float类型的变量
float fTemp = 29.6f;
// 声明double类型的变量
double dTemp = 86.69;
printf("char类型的数据与float类型的进行相加运算,运算结果为:%.2f\n",(chTemp+fTemp));
printf("float类型的数据与double类型的进行相加运算,运算结果为:%.2f\n",(fTemp+dTemp));
printf("char类型的数据与int类型的进行相加运算,运算结果为:%d\n",(chTemp+nTemp));
}

显示类型转换

显示类型转换是相对隐式转换来说的,隐式转换由C编译器自动进行,不需要开发者做任何操作。显示类型转换需要开发者在代码中对数据类型进行显示类型转换。

当进行数据类型的显示转换时,程序员需要自身判断类型转换过程中是否会发生数值溢出或精度丢失,当由精度高的类型转换为精度低的类型时,会发生精度丢失。

显示转换的一般形式为:

(类型名)要转换的变量或常量

例如:

// 将数值36.9强制转换为int,精度丢失
int nTemp = (int)36.9;
// 声明double类型的变量
double dTemp = 12.15;
// 将double类型强制转换为int,精度丢失
int nV  = (int)dTemp;

在上面的例句中,36.9是数值常量,默认为double类型,显示转换为int类型并赋值给nTemp,此时nTemp的值为36,精度丢失。

dTemp是double类型的变量,将dTemp的值赋值给int变量时,需要进行显示转换,int变量只存储double变量的整数部分,小数部分丢失。