2.4 对数据类型的进一步讨论
数据类型的意义不仅仅在于值的范围、内存中数据的长度以及值的表示方法,事实上,c语言中的所有运算都是和具体的数据类型息息相关的。每一种运算都是针对具体的数据类型而言的。同样一个运算符,对于不同的数据类型来说运算含义并不相同,甚至对于某些数据类型某种运算是存在的,而对于另外一种数据类型这种运算可能根本不存在。因此,离开了具体的数据类型根本就谈不上运算。所以完全可以说,不了解C语言的数据类型就不了解C语言的一切。
int类型可以参与的运算有很多,不可能在此一次完全介绍完。所以在此只介绍简单的几种:“+”、“-”、“*”、“/”、“%”。
2.4.1 int数据类型的运算
1.两个int值的“+”、“-”运算及讨论
两个int值的“+”或“-”运算的含义与算术中的加法或减法的含义类似,但有一个隐含着的限制条件。这个限制条件就是运算结果必须在int类型值的范围之内。
这样,代码中的3+5,程序运算得到的值就是8。这似乎很简单,但有些问题就不那么简单了。比如:
在平时的算术运算中我们会求出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量相反数的值。比如:
在这里“-”是一种运算,这个运算本质上相当于(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-7 C代码的几种可能
至于两个int中有负值做“/”运算的规则究竟应该是什么,可以查阅编译器的使用手册,也可以通过程序的运行结果来归纳总结。
程序代码2-7
输出结果如下:
根据这个结果,可以归纳出,在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-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这种数据类型。
这种类型的名称还有两种等价的写法:signed和signed 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)变量的定义方法