Linux-C成长之路(二):基本数据类型

C语言是一种强数据类型编程语言,换句话讲,不像弱数据类型的语言比如shell脚本语言那样,没有特殊的变量数据类型,统统都是字符串。而C语言是有严格的数据类型的规定的。来看一下下面这段代码:

//example2.c
#include <stdio.h>

int main(void)
{
        char c = 'A';
        int i = 100;

        float f1 = 3.14;
        double f2 = 2.69E-12;

        printf("%c, %d, %f, %lf\n", c, i, f1, f2);
        return 0;
}

这是一段最简单的代码,程序定义了4个类型不一的变量,它们分别是char型变量c,int型变量i,float型变量f1,和double型变量f2. 而且在定义的时候给它们分别进行了初始化。

首先来观察一下这个语句:

char c = 'A';

这是一个定义语句,这条语句的确切含义是:要求系统为我开辟一块内存空间,用来存放一种称之为 char 类型的数据。其中标识符 c 就是这块内存空间的名字,因此这是一块有名内存(后面会提到匿名内存),我们可以通过 c 这个名字来访问这块内存。

那么这块内存空间 c 有多大呢? 这跟定义时指明的数据类型相关,由于定义的时候指明是 char 类型,因此这块内存的大小就是 1 个字节(即 1 byte,亦即 8 bits)。

另外更为重要的是,由于定义时指明了这块内存的数据类型是 char ,因此以后程序在解释这块内存 c 时就会将它解释成一个 字符,而不是一个浮点数。这里必须搞清楚的一点是:在内存中只有1和0组成的数据序列,定义时规定的数据类型就是用来告诉系统,将来拿到一块充满了1和0的内存时,该怎么去解释它,因此不同的解释方法,将会得到完全不同的结果。

上面的定义 char c = 'A', 第一步,在内存中开辟了一块空间,名字叫 c ,而且专门用来存放字符(亦即char型)数据,紧接着我们就将字符常量 'A' 赋值给 c ,这里有个问题,字符常量 'A' 是怎么放进去这块内存的呢? 不可能在内存单元里面刻了一个字母 'A' 吧?

答案当然是否定的,内存中根本不可能存储一个字母'A',因为内存只能存储1和0. 怎么办呢? 很简单,人为规定一个数字来对应字母'A'就可以了,比如就用65吧,既然我们无法真正存储字母 ‘A',那就存储65吧! 不用担心你的'A'会丢失,因为按照这个规定,以后我们凡是遇到 char 型的单字节内存,而且里面如果恰好是一个65,我们就知道,哦!原来这家伙是'A'!

完全一样的道理,我们也无法真正存储字母'B','C','D',以及任何其他可见或不可见字符,比如 '@'  '#'  '$'  '!' '~'  '\n'  '\b' 等等,所有这些字符都必须统统像'A'那样用一个数字来代替它们,于是,我们就有了所谓的ASCII码表。亦即数字 -- 字符 对照表。

我们可以在Ubuntu的man帮助手册中查看ASCII码表:

vincent@ubuntu:~$ man ascii

上图是ASCII码表的一部分,我们现在很清楚地知道:在计算机的内存中,实际上是不存在字符的,而仅仅存放代表这些字符的数字,我们将这些人为规定的数字称为这些字符的ASCII码值。

现在回过头来再看看这个定义语句: char c = 'A' ;

这句话的含义是:在内存中开辟一块空间(1个字节),并命名为 c , 并且与此同时,将字母‘A' 的ASCII码值65放到这个字节里面。

再来看定义语句 int i = 100;

现在理解起来就很容易了:在内存中开辟一块空间(4个字节),并命名为 i ,并且与此同时,将 100 放到这块内存中。

同理,对于float f2 = 3.14 和 float f2 = 2.69E-12 而言:在内存中开辟了两块空间(4字节和8字节),并且与此同时,将3.14和2.69 乘以 10 的 -12 次方放到这两块内存中。

现在来考虑一下整型int,int 的意思是 integer ,亦即整数。显然 int 就是专门用来存放整型数据的。比如 int i = 623723869; 其真实的物理存储如下:


这里的 int 类型占用了内存中的4个字节,但这并不是绝对的,实际上,int 类型究竟会占几个字节,要根据具体的软硬件平台而定,C语言标准并没有规定 int 类型要占用几个字节,这样的结果是不同平台的 int 型变量的长度的不一致性,这将导致程序的移植问题。(稍后我们会看到如何解决这个问题)

从上图中可以看到,程序在存储数字的时候,都是用二进制来存储的,比如上面的 i 的值623723869,相当于二进制 00100101 0010110101000101 01011101。这就是整型数据在内存中的存储。

另外我们要注意,最高位实际上是一个符号位,那意味着:假如最高位是0,则这是一个正数,假如最高位是1,则这是一个负数。当最高位是1时,这个负数是以补码的方式存储的,比如:


补码是真正的物理存储格式。也就是说,整型数据 -9, 在内存中的表示是: 11111111 11111111 11111111 11110111. (4个字节)

另外,有时候我们可能不需要4个字节来表达一个整型,比如一个人的年龄,一般范围是0岁 - 120岁 左右 ,这时不需要太大的内存,那样只会浪费空间。有些时候我们又需要表达更大的数据,比如表达银河系中的恒星个数,这时可能4个字节的整型都不足以表达。

针对这些特殊情况,我们可以用 short 或者 long 来修饰关键字 int,调整数据范围。比如:

short  int  a;    // 经常省略 int ,写成:short a;
long  int  b;  // 经常省略 int ,写成:long b;

我们称 a 是一个短整型的变量,它的长度不会比 int 长。称 b 为长整型变量,它的长度不会比 int 短。 大家有没有觉得这段叙述有点怪? 为什么不直接说它们占用多少个字节呢?因为我们也无法确定,C语言标准就是像刚才那么说的,并没有规定它们占用几个字节,具体的情况要根据软硬件平台而定。

但是也是有规律的,一般而言:

char 类型 占用1个字节

short 类型 占用2个字节

long 类型 占用的字节数跟CPU的字长相等

而 int 类型则不一定,比如在字长是64位的CPU平台下,可能是4个字节也可能是8个字节。

现在的问题是:如果我需要在不同的平台下移植我的程序,而且我要求我的变量的大小是固定的,比如4个字节大小,怎么办呢?

办法是,不能直接使用基本数据类型来定义,而是要将它们稍微封装一下,具体如下:比如在A平台中,int 占2字节,long 占4字节。 在B平台中,int 占4字节,long 占8字节。

那如果我要定义一个占用4个字节的整型变量 var , 就不能定义成 int var; 或者 long var; 因为这样无法让其同时在A平台中和B平台中长度一致,我们说这样的数据类型是不具可移植性的。那怎么才可以有可移植性呢?

在A 平台中,我们把 int 取个别名,叫做 int16_t, 把 long 也取个别名,叫 int32_t

在B平台中,我们把 int 取个别名,叫做 int32_t ,把 long 也取个别名,叫 int64_t

这样,我们如果想要定义一个长度固定为 4 个字节的整形变量var,我们可以这样定义: int32_t var; 这样,不管你将程序放在A平台中还是B平台中,都可以实现长度固定的想法。

至于如何给int 和 long 取别名就很简单了,在A平台中,我们提供这样的代码:

typedef int int16_t;
typedef long int32_t;

在B平台中,我们提供这样的代码:

typedef int int32_t;
typedef long int64_t;

typedef 就是给一种数据类型取别名的,这样,当你的程序需要从一个平台移植到另一个平台时,只需要重新编译就可以了,不需要修改任何代码。

另外,还有一个关键字可以用来修饰整型: unsigned,比如这样定义变量:

unsigned int a;
unsigned short int b;
unsigned long c;

上面定义了三个 “无符号” 整型变量a,b和c。这些变量不能表示负数,系统在解释它们的时候,不再将最高位解释成符号位,而是当做最高权值位。

现在来谈谈浮点数,我们可以定义 float 型单精度浮点变量,double 型双精度浮点变量,long double 型长双精度浮点变量。它们可以用来存储带小数的实数。比如:

float f1;
double f2;
long double f3;

一般而言,它们分别占用4个,8个和12个字节的内存,占用的内存空间越多,能表达的范围和精度越大。具体它们是怎么存储的,精度又怎么确定,请看我的另一篇博客:http://www.linuxidc.com/Linux/2014-05/101243.htm  , 里面有详细的剖析。

浮点数常量一般有两种表达方式,一种就是最简单的方式,直接写,比如: 3.1415, 或者877.52 等。 另一种就是科学计数法,比如: 前面的 877.52 可以写成 8.7752E2,这的 E2 代表的是10的2次方,同样道理,如果是 4.21E-5, 则表示4.2 乘以 10的 -5 次方。 E可以写成小写的e。

最后一个问题,类型转换的问题。当在一个表达式中出现有不同的数据类型的时候,会有两种结果,第一:类型不兼容,编译出错! 比如,你将一个浮点数跟一个结构体相加,完全牛头不对马嘴,编译直接出错。 第二,类型不同但可兼容,比如 一个整型跟一个浮点型相加,则会将整型临时提升为浮点型,再相加。我们现在来讨论第二种情况。

举个例子:

int a = 100;
double f1 = 3.14;
float f2;

f2 = a + f1;

代码中,三个变量类型都不相同,但是是兼容的,于是,在执行第5行的时候,系统会将所有的变量临时地提升为它们中精度最高的那种类型,比如在我们这个例子中,f1 是double 类型的,精度最高,因此其他的所有变量都将在这个运算过程中被临时地提升为 double 型参与运算,并最终的结果保存在一个 float 型的变量 f2 中。这么做的原因在于,系统在默认的情况下,只能采取最保守的方法,用最大的代价来保证用户数据在运算的过程中不丢失精度,因为如果你将一个浮点数转化成整型的话,其小数部分就会丢失了。这种系统帮我们自动将“小”类型转换成“大”类型的行为,称为隐式类型转换。
 
那如果我非要把精度高的数据类型转化成精度低的类型呢? 也可以,那就要显式地指明,比如上面的那个例子:

int a = 100;
double f1 = 3.14;
float f2;

f2 = a + (int)f1;

在 f1 的前面加了一对圆括号,里面写了一个 int ,表示要在中间的运算过程中强制将 f1 临时降格为 int 类型(这样可能会导致精度的丢失),然后参与运算。

最后要注意,不管是隐式类型转换,还是强制类型转换,转换的都是中间过程中的临时“替身”, 变量本身的类型和值不会发生变化。

  

猜你喜欢

转载自blog.csdn.net/li_Xing666/article/details/81639138
今日推荐