2.4 对数据类型的进一步讨论

数据类型的意义不仅仅在于值的范围、内存中数据的长度以及值的表示方法,事实上,c语言中的所有运算都是和具体的数据类型息息相关的。每一种运算都是针对具体的数据类型而言的。同样一个运算符,对于不同的数据类型来说运算含义并不相同,甚至对于某些数据类型某种运算是存在的,而对于另外一种数据类型这种运算可能根本不存在。因此,离开了具体的数据类型根本就谈不上运算。所以完全可以说,不了解C语言的数据类型就不了解C语言的一切。

int类型可以参与的运算有很多,不可能在此一次完全介绍完。所以在此只介绍简单的几种:“+”、“-”、“*”、“/”、“%”。

2.4.1 int数据类型的运算

1.两个int值的“+”、“-”运算及讨论

两个int值的“+”或“-”运算的含义与算术中的加法或减法的含义类似,但有一个隐含着的限制条件。这个限制条件就是运算结果必须在int类型值的范围之内。

这样,代码中的3+5,程序运算得到的值就是8。这似乎很简单,但有些问题就不那么简单了。比如:

2.4 对数据类型的进一步讨论 - 图1

在平时的算术运算中我们会求出0X4FFFFFFF + 0X4FFFFFFF==0X9FFFFFFE。但在C代码中,由于两个0X4FFFFFFF的类型都是int类型,而0X9FFFFFFE的值明显已经超过了int的表示范围,这种情况叫溢出(Overflow)。

溢出的情形类似于“预备了一桌饭,却来了两桌客人。这个饭怎么吃?”的那种尴尬。没有人事先知道这个饭应该怎么吃。同样,C语言也没规定或说明溢出会产生什么样的后果,编译器一般可以自由地处理这种情况。这种情况在C语言中叫做“未定义行为”(Undefined Behavior)。这种未定义行为在代码的层面上是没有确定的语言含义的,是一种错误的代码表达,因为这种表达什么也没有表达。但是编译器并不会认为0X4FFFFFFF + 0X4FFFFFFF违背C语言的语法,没有任何理由认为0X4FFFFFFF + 0X4FFFFFFF这样两个int类型数据相加与C语言的语法有什么相悖之处。所以,正如前面所提到过的那样,编译器并不拒绝这种模棱两可的忽悠。C语言“信任程序员”,保证这种计算正确性的责任在程序员而不在C语言。编译器只把0X4FFFFFFF + 0X4FFFFFFF这样明显不符合C语言语法形式的表达判为“违法”或“非法”,因为并不存在“ +”这种运算。

这样,我们就可以看出,写C代码可能出现以下3种情况。

(1)语法错误或非法,如0X4FFFFFFF * + 0X4FFFFFFF。

(2)未定义行为,如0X4FFFFFFF + 0X4FFFFFFF,没有语法错误但也没有确切的语言含义。

(3)合法且有确切含义的行为,如3+5。

前两种毫无疑问都属于错误的代码。一种属于胡言乱语,另一种属于痴人说梦。

初学者有时容易陷入追究“未定义行为”其确切含义的陷阱,这种水中捞月式的执着是没有任何意义的。

2.两个int类型量的“*”运算

如果运算结果在int的值的范围内,那么和数学中的乘法运算没有什么差别。在C代码中用“”表示乘法运算是因为键盘上没有“×”,即使有,它也不属于C语言的基本字符集,也就是说“×”不是C语言的“字母”。

如果运算结果不在int的值的范围内,那么两个int值的*运算是未定义行为。

3.另外两种“+”、“-”运算

和前面提到的两个int值做减法运算不同的是,另外一种“-”运算是对单独一个int值进行运算,含义是求这个int量相反数的值。比如:

2.4 对数据类型的进一步讨论 - 图2

在这里“-”是一种运算,这个运算本质上相当于(0-456)。由于(0-456)依然在int类型的值的范围之内,所以值就是-456。

顺便在这里解答一下前面的一道练习题。

没有人会把“123-456”当做int类型的常量,因为“-”在这里是一个运算。同理,由于“456”是一个int类型的常量,而“-”是一种运算,那么“-456”不是int类型的常量是显而易见的吧。

更通俗地讲,在C语言中,由于“-”是一个运算符,相当于自然语言中的“动词”,而常量相当于一个名词,“-456”这样的“动宾词组”无论如何也不可能和“456”这样的“名词”画等号的。

此外,在前面程序代码2-6中的“-2147483647-1”,由于值在int值的范围之内,所以结果也是int类型。但是由于2147483648已经不在int值的范围之内了,所以-2147483648的类型显然不是int。然而,在那种把-456也当做常量的误导下,则很容易以为-2147483648也是一个int类型的值。

这里看到的有趣的现象是,同一个“-”,但却有两种语法意义。C语言中的这种一词多义的现象比比皆是。在学习时必须充分加以重视和注意。

编译器究竟如何判断“-”这种“多义词”的具体含义呢?

答案是,编译器是通过“-”在代码中所处的具体上下文来确定“-”的真正含义的。当“-”运算符前后各有一个运算对象时,它们表示的是减法运算;当这个运算符只在后面有一个运算对象时,表示的是求负值运算。

对于“+”运算,也存在类似的情况。单独一个int类型值可以做“+”运算,计算结果为这个int值的值。比如:

  • 8的值为8。

这个运算是某些喜欢对称美感的C语言专家们后来生硬地加到C语言里的一种运算,目的是为了给“一”(求负值运算)找一个配偶,传统C里并没有这个运算。

4.两个int类型值的“/”运算

“/”在C语言中有时表示除法运算。然而离开了具体的数据类型谈除法是没有意义的。“/”运算符非常清楚地证实了这个观点。

在C程序中,1/2的运算结果是0而不是0.5。也就是说两个int类型量做“/”运算应该得到一个int值,这相当于数学中的“整除”。

然而1./2.得到的结果却是0.5,这里1.和2.是另外一种类型的数据。这个事实清楚地再次表明了所有的运算都与具体的数据类型有关,离开了数据类型就根本谈不上“运算”这两个字。

那么-2/3的值是什么,-1还是0?我不得不遗憾地告诉你,在现行的国家标准(GB/T 15272-94程序设计语言C)中对这种情形的运算结果竟然忘记了规定(15)。所以从理论上来说,我国人民谁也不知道C代码中的-2/3的值是几,至少是不应该知道。

而在C89中,对两个int做“/”运算且除不尽情况的规定是:

当两个int皆为正时,结果舍去代数商的小数部分的整数值。比如3/2的值为1。

两个int中有负值的情况下,结果为向0取整还是向INT_MIN取整是由编译器自行决定的(Implementation-defined)。也就是说,编译器可以规定-2/3的值为-1也可以规定这个值为-2,无论哪种都符合C语言标准。而-2/-3的结果究竟是1还是2也是由编译器决定的。

前面提到过合乎C语言语法要求的代码可能是“未定义行为”或“合法且有确切含义的行为”,如图2-7所示,现在我们进一步地看到,在“合法且有确切含义的行为”中,其确切含义有的是C语言规定的,有的则是编译器自行规定的。

2.4 对数据类型的进一步讨论 - 图3

图2-7 C代码的几种可能

至于两个int中有负值做“/”运算的规则究竟应该是什么,可以查阅编译器的使用手册,也可以通过程序的运行结果来归纳总结。

程序代码2-7

2.4 对数据类型的进一步讨论 - 图4

输出结果如下:

2.4 对数据类型的进一步讨论 - 图5

根据这个结果,可以归纳出,在Dev C++中,两个int类型值做“/”运算结果是向0取整的。而C99对两个int类型值做“/”运算的规定是向0取整。本书代码中整数的除法,均默认为是遵守这个规则条件下的代码。

此外,当除数为0时,结果未定义。有的编译器在发现明显的除以0的情况时会认为是一种语法错误而停止继续编译,而有的除以0的代码错误要到运行时才可能会被发现。

5.两个int类型量的“%”运算

%运算的含义是求除法运算得到的余数,读做“求余”。例如7%2求得的值为1,7%2可以读做“7对2求余”。

%运算是根据“/”运算定义的,所以也是一种运算结果与编译器相关的运算。在“两个int做“/”运算的规定是向0取整”的前提下,“%”运算的结果的符号总是和被除数一致的。请自己推导出这个结论。

练习

仿照程序代码2-7,编程归纳你所使用的编译器的“%”运算规则。

6.再次重申

C语言的所有运算都是和具体的数据类型息息相关的,离开了具体的数据类型根本谈不上运算。如果想学好C语言,这句话值得再三强调。不懂得数据类型的程序员,只具有写“Hello,World!”代码的资格。

练习

1.编程计算345 + 789并输出,检验输出是否与手算结果相同。

2.编程计算1234567890 * 7并输出,检验输出是否正确。

3.编程计算123/0并输出,观察程序反应。

4.编程计算123%2、-123%2、123%-2、-123%-2并输出,检查与你事先预想的结果是否一致。

2.4.2 数学公式与数据类型

例题:编程计算1+2+3+……+100=?

分析:根据数学知识可以知道,2.4 对数据类型的进一步讨论 - 图6,所以可由该式直接计算答案。

程序代码2-8

2.4 对数据类型的进一步讨论 - 图7

屏幕输出如下:

2.4 对数据类型的进一步讨论 - 图8

很明显,程序的输出结果是错误的。然而本例题的目的和意义正在于此。事实上,错误是编程的一部分,无论你怎样逃避,它常常在你不经意的地方,在你自以为正确的时候出现。在学习编程的过程中,不可能回避错误更不应该回避错误,当然,也不应当惧怕错误。经验表明,我们从错误和失败中学到的东西往往更多些也更深刻些。正所谓“最好的锤炼方法是失败。没有什么比经历失败更能锻炼人了”,对于学习编程来说,尤其如此。所以,要是现在为止你到还没犯过错误,我劝你赶紧犯一次。前面的例题就算是我替你犯的一个错误,请自己找出错误的原因并改正。这个错误与数据类型有关。很多初学者往往误以为数学公式可以拿过来就在代码中使用。实际上,不是这样的。

每个人都会犯错误。没有人知道从根本上杜绝错误的方法(除非不编程)。关于编程,目前我们所知道的只是良好的编程习惯可以减少错误——尤其是那些低级错误。所以,请及时养成良好的编程习惯——越早越好,最好从开始学习编程就开始培养良好的编程习惯。

此外,需要知道的是,测试也是编程不可缺少的一个环节——对程序的运行结果应该在运行前有所估计或设想,并在程序运行之后进行认真的考察与验证。没有测试就没有代码的质量可言,甚至没有程序的正确性可言。一个代码编译的通过往往离程序正确还有相当遥远的距离。那种把编译通过理解为程序正确的想法是很幼稚可笑的。

2.4.3 数据类型——代码与编译器的约定

1.数据类型的重要性

代码对计算机的命令是通过编译器转述的,所以代码直接面对的读者是人和编译器。

C语言的编译器的脾气是“非礼勿听”,它拒绝接受代码违反C语法,但并不拒绝接受模棱两可的忽悠,甚至将其当成言之凿凿的正经话来听。

当代码需要计算机处理一个数据时,需要通过数据类型告诉编译器这个数据的位数及数据的存储格式,这是数据类型的“两个基本点”之一。而数据类型另一个“基本点”则是,用潜台词向编译器申告了对这个数据的处理方式(也就是运算规则)的要求。数据类型是代码世界与机器世界之间的一种约定,是联系代码与机器数的纽带与桥梁。

由此不难得出结论,数据类型的问题是C语言的一个基本问题。

事实上,C语言的关键字有2/3左右是与数据类型直接相关或间接相关的。从某种意义上来说,本书从头到尾都涉及到数据类型问题。因此完全可以说,不深刻了解数据类型就不可能真正懂得C语言。

2.特点和要领

在代码中表示数据类型,有时是以坦率赤裸裸的方式进行的,有时是以含蓄的方式表达的。在学习数据类型时要特别注意以下几点。

(1)数据类型的名称(赤裸裸的数据类型要求要靠名称)。

(2)该类型的取值范围(不可以要求给大数穿小鞋)。

(3)存储空间的大小及格式(16)(该数据类型的机器数表示)。

(4)该类型常量在源代码中的写法(含蓄的数据类型要求)。

(5)可进行的运算种类及规则(数据类型的后续效应)。

(6)该类型变量的定义方法(与前面的1、2、3、5都有关)。

下面,对int这种数据类型做一小结。

3.int类型小结

(1)类型名称

int类型的名称为“int”“int”是C语言的一个关键字,它的用处就是在代码中表示int这种数据类型。

这种类型的名称还有两种等价的写法:signedsigned int

(2)值的范围

INT_MIN~INT_MAX(在limits.h文件中定义,对于Dev C++这两个值分别为-2147483647-1和2147483647)。

(3)存储空间的大小及格式

对于Dev C++:4byte(32bit),二进制补码

(4)int类型常量

共有十进制、八进制、十六进制3种写法,值不得超过INT_MAX。

(5)运算种类及规则

详见2.4.1小节。此外其他与这个类型有关的运算将在后面陆续介绍。

(6)变量的定义方法

2.4 对数据类型的进一步讨论 - 图9