c语言数据类型-1

C语言数据类型-1

关键词

数据类型、整数数据类型、补码编码、有符号数、其它整数类型

一、摘要

刚开始学习时,不必了解所有的细节,就像学习开车之前不必详细了解汽车内部引擎的原理一样。——《C Primer Plus》第六版

  在本篇博客中,笔者将简要介绍C语言的基本数据类型。其中要点为:补码编码原理以及采用该种编码的原因、简单介绍IEEE—745标准、数据类型之间的转化。这些内容在所有C语言书籍中都有讲解,本文主要以《CSAPP》第三版以及《C Primer Plus》第六版作为参考,面向语言学习入门者,简要地讲解该部分内容。由于笔者水平有限,如有错误,敬请指出。

二、写在前面

​ 如果你只是想直接了解补码的值如何计算,浮点数如何表示,那么其实非常简单。对于补码而言,我们先将数拷贝为两份。将其中一份最高位(根据二进制数的位数来确定,正数最高位为0,负数为1)去掉,然后将另一份最高位之后全置零,两份相减即可得到补码值的大小。即最高位表示负权,补码值为负权加上剩下数的大小。

例如,11100101为一个8位二进制数,则该数的符号位为1,补码的大小为 -1 * 2^7 + 1 * 2^6 + 1* 2^5 + 1* 2^2 +1* 2^0 。

11011为一个8位二进制数,则该数的符号为0,补码的大小为1 * 2^4 + 1* 2^3 + 1* 2^1 +1* 2^0

浮点数不过是计算机中的科学计数法( ab^c ),只不过在这里a为一个纯小数,b由常见的10变成了2(某些情况下,浮点数表示有特殊含义,但不在本文讨论范围中)。在IEEE—745标准中,明确规定了不同长度的浮点数各位上表示的含义。*在读浮点数时,根据标准转化为科学计数法即可得到浮点数数值。

​ 言尽于此,你已经学会了如何读出以及初步使用这些基础的数据类型。但这只是解决了方法论的问题,而并未解决认识论的问题。稍加思考你一定会问,“c语言为什么需要使用这样的数据类型?” ”为何一定要使用补码这样违反直觉的表示方法?“ “在特殊情况下,这些精巧的数据结构会造成什么意想不到的后果?“

​ 如果你有对这些问题感到好奇,并且有一定的空余时间,那么欢迎阅读博客的下面部分。

三、数据类型简介

1、计算机中的数据存储

毫无疑问,计算机有两项功能是最重要的:一是计算,二是储存。计算的对象、计算得到的结果都要储存在一定空间中,才能为人所用。在现实生活中,我们通常会将各种物品储存在分类好的隔箱中,计算机也遵循这样的思路。这样做的好处是显而易见的:既能用大隔箱套小隔箱方便查找,隔箱与隔箱之间的数据也不会相互干扰。计算机是一个标准化的系统,因此储存的隔箱也是严格按照标准设定的。为了减少浪费,设计者将最小的隔箱的大小定为数据能分割的最小单位,即1个信息位( 1 bit ),它仅能通过高低电位表示两种状态,我们将这两种状态分别令为0和1。

2、二进制编码

​ 在这里考一下大家,人的一只手最多能表示多少个数?只有5个吗?当然不是,我们至少能清晰表达32种不同的状态,对应起来就能表示0-31。 这是如何办到的呢?很简单,我们从小拇指开始,分别将5个手指看成5个不同的数位。每个数位我们根据手指的屈伸,可以取0、1两种情况,根据排列组合的知识,我们可以知道这一共可以表示32种不同的情况。同样,如果我们有5盏具有一定顺序的灯,灯的开与关分别表示两种不同的状态,那么我们也可以表示32(2^5)种状态。按照此规律,我们不难推算出:如果有8盏这样的灯,我们可以表示出256(2^8)种不同的状态;如果有16盏,则可表示65536(2^16)种状态;如果有32盏,则可以达到4294967296(2^32)种,这已经是一个很惊人的数字了,而我们只是简单用到了32盏有顺序的灯而已。

3、数据类型的抽象化

​ 这样简单高效的表示方法自然受到了计算机行业的喜爱。半导体技术能方便地存储高低电平两种信号,因此储存2进制数据成为了计算机行业的通用存储方式。但是这些存储元件实际上类似于一条近乎无限延展的方格链,我们直接存储明显不够高效。那自然就想到将这么长的链条划分为若干段。但我们有各种各样的信息需要储存,例如储存技术部所有小朋友的姓名需要划分十个小格子,而储存全校的姓名就需要一万个,因此统一划分为一种样式明显是不合理的。于是结合前文中提到的大隔箱套小隔箱的方式,我们建立起了一个标准的划分方法,确保绝大部分计算机都能识别出该种方法,这就是计算机的数据类型,即规定多少个方格划分出来储存特定的一类数据。

四、c语言中的数据类型

1、字节(byte)与char类型

在大部分语言中,我们规定8个小灯组成最小的格子,即1字节(byte),这是计算机中数据储存的基本单位,它能表示256种不同的状态很明显,这一种类型储存的数据一定不能很大,比如储存的数据有356种不同的形态,它一定放不下。但有一种常用的数据类型,它的状态数很少,这就是我们平常使用的字符。24个英文字符算上各种标点符号一起,也不会超过256种。因此我们将各种字符边上号,让它对应256种状态的一种,这就是char类型的数据,它由一个字节(8盏小灯组成的小格子)来表示。这里就可以回答为什么字符都是正数。所谓的字符和整数一一对应,不过是我们给字符的一个编号,它的实质是由此将字符映射到一个字节(256种状态)之中。那么我能用正数编码,完成映射,为何要加入负数来干扰呢?但这同样也给我们一个启示,char类型只是规定了划分格子的数量,即能容纳的大小,并不能限制里面存储的数是正数还是负数(更准确的说法是有符号数与无符号数)。我们同样可以用char类型来储存很小的正数或负数,不过这仅在极特殊的情况下会使用。

也许有反直觉,但很多c语言编译器实际上将char类型视为有符号数。c标准并不保证这一点,因此如果要通过char类型存入有符号数,应该提前声明为有符号类型。不过,在绝大多数情况下,程序对char类型是否为有符号并不敏感。

2、Int类型

​ 前面我们提到正数和负数,很明显我们需要用一定的方格去储存这两种数据类型,因为计算机的本质就是计算,而计算一定需要数字。首先,我们考虑最简单的自然数。毫无疑问,这因该是我们日常使用最多的数据,它和生活是息息相关的。还记得前文所说吗?计算机中存储的只是不同的状态,我们按照实际需求,对这些状态赋予意义。那对于自然数,最简单的方法就是从0开始,一种状态对应一个数。那么什么样的状态对应什么样的数呢?这就需要我们前文介绍的二进制编码。解决了对应关系,那么我们需要担心的就是表示数据的范围。前面的1字节肯定是不够的,256种状态,只能表示0-255,小学生的加减乘除题都会超过这个范围。那我们给一百个格子好不好?也不行,虽然表示的数范围广了,但大部分后面的数根本用不上,白白浪费了很多格子的空间。因此我们在空间与范围间取舍,有了2个格子(16盏灯)、4个格子(32盏灯)、8个格子(64盏灯),三种不同的划分方法,根据我们要表示的数据范围,选择其中的一种划分方法即可。比如选2个格子,那么你有了65536种不同的状态,能表示0-65535的数,已经能满足很多运算的需求了。超过了怎么办?那就选4个格子的呗,此时你已经能表示4294967296种状态,可以应付绝大多数情况了。这三种格子,就是c语言中的3种无符号整数数据类型:unsigned short、unsigned int、 unsigned long。

3、补码表示

​ 那负的整数怎么办呢?这是我们遇到的第一个难题。一种简单的解决思路是和负数的表示一样,把这些格子中的头一盏灯作为符号位,如果不亮就表示正数,亮了就表示负数。这样的表示有两个巨大的缺点。首先,符号位始终需要单独标记,不能直接参与运算。否则两个负数相加,符号位会由1变为0,结果变成了一个正数。固定的符号为还会导致难以进行位运算,而左移右移对于加速乘法或除法是至关重要的。其次,0在这种方法下会有两种不同的表示方式,比如二进制数10000000与二进制数00000000均表示0。相当于一个人有两个名字,这在标准化、精密的计算机系统中,显然不是最优的。

​ 为了解决这两个问题,它的符号位最好可以一起参与计算,并且0的表示方法应该唯一。于是我们发明了补码。不同的书籍对于这部分的介绍有一些差别,较权威的《CSAPP》中,将补码的核心概括为“将字的最高有效位解释为负权”。以我个人的认知而言,补码实际上基于一种简单而优雅的数学运算——取模(%)我们如果对于每一次正数加法进行取模,会发现存在一些越加越少的情况。比如 (3 + 6) % 12 = 9,而 (3 + 10) % 12 =1。这里负数就悄悄出现了。因为负数实际上就是一种特殊的减法运算,一个数的负数也就是零减去该数。对于第二个例子而言,3 + 10 =1,那么10在这里就与 -2 相等同了。也就是说,在模运算的参与下,我们可以使用一个很大的数来代替负数。而这个数要有多大,则取决于模的大小。这种方法为什么好?因为我们在刚才的计算中丝毫没有去管符号位,只是进行正常加法运算,0的表示也是很自然的。由此,我们将大于等于某个值的数化出来作为负数,小于某值的数作为正数,正好解决了符号位不能参与加减的问题。在下文中,我们通过一个例子来解释。

​ 在8位的补码表示中,这个模的大小为2^8(256),因此二进制数10000000(128)为最大负数-128,二进制数11111111(255)为最小的负数-1,二进制数01111111为最大的正数(127),二进制数0000000为0,任何二进制码小于128的为正数,而大于等于的为负数。我们可以进行一些验算,比如任何数加上255再%256,就相当于-1。任何一个正数加上128再%256结果二进制码都大于128,为一个负数。二进制数11111111+二进制数11111111 = 11111110(进位舍去)等价于-1 - 1 = -2。需要注意的是,这种表示方法的表示范围是严格界限的,若两正数相加之和大于了127,则结果会变为一个很小的负数;同理两个负数之和小于-128,则结果会变为一个正数。这种现象就是我们所谓的溢出。

五、总结

​ 读到这里,我们已经以计算机中的数据存储为基点,概括性的介绍了C语言的字符以及整数表示方法。这里需要提醒一下,由于C语言标准的复杂性和一些底层实现过程中的差别,会有一些特例出现。但为了学习的流畅性,笔者认为细枝末节的部分可以省略。在下一部分中,我们将介绍更为有趣的float类型、字符串以及不同类型之间的转换。如果本篇博客能对你的学习产生帮助,笔者不甚荣幸。

 wechat
Hi!欢迎您扫码提出意见或者投稿!