《C语言编程魔法书:基于C11标准》——2.2 整数在计算机中的表示

简介:

本节书摘来自华章计算机《C语言编程魔法书:基于C11标准》一书中的第2章,第2.2节,作者: 陈轶 更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.2 整数在计算机中的表示

我们日常用的整数都是十进制数(Decimal),也就是我们通常所说的逢十进一。因为我们人类有十根手指,所以自然而然地会想到采用十进制的计数和计算方式。然而,现在几乎所有计算机都采用二进制数(Binary)编码方式,所以我们日常所用到的整数如果要用计算机来表示的话,需要表示成二进制的方式。
二进制数则是逢二进一,所以在整串数中只有0和1两种数字。比如,十进制数0,对应二进制为0;十进制数1,对应二进制数1;十进制数2,对应二进制数10;十进制数3,对应二进制数11。因此,对于非负整数而言,二进制数第n位(n从0开始计)如果是1,那么就对应十进制数的2n,然后每个位计算得到的十进制数再依次相加得到最终十进制数的值。比如,一个5位二进制数10010,最低位为最右边的位,记为0号位,数值为0;最高位为最左边的位,记为4号位,数值为1。那么它所对应的十进制数为:24+21=18。因为该二进制数除了4号位和1号位为1之外,其余位都是0,因此0乘以2n肯定为0。图2-3为二进制数10010换算成十进制数的方法图。


<a href=https://yqfile.alicdn.com/19e8f1a80cfa8446aa4df1cd5aeaa5536c8be64b.png" >

在计算机术语中,把二进制数中的某一位数又称为一个比特(bit)。比特这个单位对于计算机而言,在度量上是最小的单位。除了比特之外,还有字节(byte)这个术语。一个字节由8个比特构成。在某些单片机架构下还引入了半字节(nybble或nibble)这个概念,表示4个比特。然后,还有字(word)这个术语。字在不同计算机架构下表示的含义不同。在x86架构下,一个字为2个字节;而在ARM等众多32位RISC体系结构下,一个字表示为4个字节。随着计算机带宽的提升,能被处理器一次处理的数据宽度也不断提升,因此出现了双字(double word)、四字(quad word)、八字(octa word)等概念。双字的宽度为2个字,四字宽度为4个字,所以它们在不同处理器体系结构下所占用的字节个数也会不同。
我们上面介绍了非负整数的二进制表达方法,那么对于负数,二进制又该如何表达呢?在计算机中有原码和补码两种表示方法,而最为常用的是补码的表示方法。下面我们分别对原码和补码进行介绍。
2.2.1 原码表示法
对于无正负符号的原码,其二进制表达如上节所述。而对于含有正负符号的原码,其二进制表示含有一位符号位,用于表示正负号。一般都是以二进制数的最高有效位(即最左边的比特)作为符号位,其余各位比特表示该数的绝对值大小。比如,十进制数6用一个8位的原码表示为0000 0110;如果是-6,则表示为1000 0110。二进制的原码表示示例如图2-4所示。


2c125d7a7a7a53d840865aae93996e603b193379

原码的表示非常直观,但是对于计算机算术运算而言就带来了许多麻烦。比如,我们用上述的6与-6相加,即0000 0110+1000 0110,结果为1000 1100,也就是十进制数-12,显然不是我们想要的结果。所以,如果某个处理器用原码表示二进制数,那么它参与加减法的时候必须对两个操作数的正负符号加以判断,然后再判定使用加法操作还是减法操作,最后还要判定结果的正负符号,可谓相当麻烦。所以,当前计算机的处理器往往采用补码的方式来表达带符号的二进制数。
**2.2.2 补码表示法
**

正由于原码含有上述缺点,所以人们开发出了另一种带符号的二进制码表示法——补码。补码与原码一样,用最高位比特表示符号位,其余各位比特则表示数值大小。如果符号位为0,说明整个二进制数为正数或零;如果为1,那么表示整个二进制数为负数。当符号位为0时,二进制补码表示法与原码一模一样,但是当符号位为负数时,情况就完全不同了。此时,对二进制数的补码表示需要按以下步骤进行:
1)先将该二进制数以绝对值的原码形式写好;
2)对整个二进制数(包括符号位),每一个比特都取反。所谓取反就是说,原来一个比特的数值为0时,则要变1;为1时,则要变0。
变换好之后,将二进制数做加1计算,最终结果就是该负数的补码值了。
下面我们还是用6来举例,+6的二进制补码跟原码一样,还是0000 0110。而-6的计算过程,按照上述流程如下:
1)先将-6用绝对值+6的形式表示:0000 0110;
2)对每个比特位取反,包括符号位在内,得到:1111 1001;
3)将变换好的数做加1计算,最终得到:1111 1010。
由于二进制补码的表示与通常我们可直接读懂的二进制数的表示有很大不同,所以给定一个二进制补码,我们往往需要先获得其绝对值大小才能知道它的具体数值。获得其绝对值的过程为:先判定符号位,如果符号位为0,那么就以通常的二进制数表示法来读即可。如果符号位为1,那么就以上述同样的过程得到其对应的绝对值。比如,如果给定1111 1010这个二进制数,我们看到最高位符号位为1,说明是负数,我们就以上述过程来求解:
1)先将该二进制数每个比特做取反计算,得到:0000 0101;
2)然后将变换得到的值做加1计算,最终获得:0000 0110。
所以1111 1010的绝对值为0000 0110,即6。
对于补码表示,我们已经知道最高位比特表示符号位,其余的表示具体数值。但是这里有一个特殊情况,即符号位为1,其余位比特为都为0的情况。比如一个8位二进制补码:1000 0000,此时它的值是多少?因为我们通过上述流程,求得其绝对值的大小也是1000 0000,所以当前大部分计算机处理器的实现将它作为-128,但估计仍然有一些处理器会把它作为-0。因为C语言标准中对于数值范围的表示已经明确表示出8位带符号的整数范围可以是-128到+127,也可以是-127到+127,但最小值不得大于-127,最大值不得小于+127。第5章会有更详细的描述。
补码的这种表示法的优点就是可以无视符号位,随意进行算术运算操作。比如,像我们上面所举的例子:6+(-6),计算结果:

0000 0110+1111 1010=0000 0000

最后,上述计算结果的最高位符号位所产生的进位被丢弃(在处理器中可能会设置相应的进位标志位)。我们自己计算的话也非常方便,在计算过程中,无需关心两个二进制补码的正负数的情况,也无需关心符号位所产生的影响。我们只需要像计算普通二进制数一样去计算即可。把最终的计算结果拿出来判断,是正数还是负数。当然,二进制补码会产生溢出情况,比如两个8位二进制补码加法:

120+50=0111 1000+0011 0010=1010 1010

然而,这个数并不是170,而是-86。首先,170已经超出了带符号8位二进制数可表示的最大范围了;其次,最高位变为1,用补码表示来讲就是负数表示形式。所以,这两个正数的加法计算就产生了负数结果,这种现象称为上溢。如果我们要避免在计算过程中出现上溢情况,需要用更高位宽的二进制数来表示,以提升精度。比如,如果我们将上述加法用16位二进制数表示,那么就不会有上溢问题了。
另外,在C语言标准中没有明确规定C语言编译器的实现以及运行时环境必须采用哪种二进制编码方式,而是对整数类型标明最大可表示的数值范围。目前大部分C语言实现都是对带符号整数采用补码的表示方式。这些会在第5章做进一步讲解。
2.2.3 八进制数与十六进制数
上面我们对二进制数编码形式做了比较详细的介绍。我们在编写程序或者查看一些计算机相关的技术文档时常常还会碰到八进制数与十六进制数的表示,尤其是十六进制数用得非常多。下面我们就简单介绍一下这两种基数(radix)的表示方法。
这里跟各位再分享一个术语——基数。基数也就是我们通常所说的,某一个数用多少进制表达。对于像“01001000是几进制数”这种话,如果用更专业的表达方式来说的话就是,“01001000的基数是几”。基数为2就是二进制;基数为10则是十进制。
八进制数是逢八进一,因此每位数的范围是从0~7。八进制数转十进制数也很简单,我们可以用二进制数转十进制数类似的方法来炮制八进制数转十进制数——以一个八进制数每位数值作为系数,然后乘以8n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前该位所对应的位置索引(同样以0开始计)。比如,八进制数5271对应的十进制数的计算过程如图2-5所示。


690b2248f20aa2511106b92450fa250af0481850

八进制数对应于二进制数的话正好占用3个比特(范围从000~111),一般在通信领域以及信息加密等领域会用到八进制编码方式。而十六进制数比八进制数用得更多,因为十六进制数正好占用4个比特,即4位二进制数(范围从0000~1111)。4个比特相当于半个字节。所以,无论是开发工具还是程序调试工具,一般都会用十六进制数来表示计算机内部的二进制数据,这样更易读,而且也更省显示空间(因为一个字节原本需要8位二进制数,而十六进制数只要两位即可表示)。下面就介绍一下十六机制数的表示方法。
十六进制数逢十六进一,因此每一位数的范围是从0到15。由于我们通常在数学上所用的十进制数无法用一位来表示10~15这6个数,因而在计算机领域中,我们通常用英文字母A(或小写a)来表示10;B(或小写b)来表示11;C(或小写c)来表示12;D(或小写d)来表示13;E(或小写e)来表示14;F(或小写f)来表示15。十六机制数转十进制数的方式与八进制数转十进制数类似——以一个十六进制数每位数值作为系数,然后乘以16n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前位所对应的位置索引(同样以0开始计)。比如,一个4位十六进制数C0DE的计算过程如图2-6所示:


3036a5259e5beea7f36dbb925e292bb6a2c51581

上述4位十六进制数C0DE,倘若用二进制数表示,则为:1100 0000 1101 1110。可见,用十六进制数表示要简洁得多,而且换算成十进制数也相对比较容易,尤其对于一个字节长度的整数来说。为了能更快速地换算二进制数、十进制数与十六进制数,请各位读者务必熟记下表:


110a9e3b15287af238b3c218a759a688384c9988

习惯上,用0或0o打头的数表示八进制数,0x打头的数表示十六进制数。比如,0123、0777表示八进制数;0x123,0xABCD表示十六进制数。

相关文章
|
24天前
|
存储 编译器 C语言
【C语言】数据类型全解析:编程效率提升的秘诀
在C语言中,合理选择和使用数据类型是编程的关键。通过深入理解基本数据类型和派生数据类型,掌握类型限定符和扩展技巧,可以编写出高效、稳定、可维护的代码。无论是在普通应用还是嵌入式系统中,数据类型的合理使用都能显著提升程序的性能和可靠性。
41 8
|
27天前
|
C语言
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性
C语言编程中,错误处理至关重要,能提升程序的健壮性和可靠性。本文探讨了C语言中的错误类型(如语法错误、运行时错误)、基本处理方法(如返回值、全局变量、自定义异常处理)、常见策略(如检查返回值、设置标志位、记录错误信息)及错误处理函数(如perror、strerror)。强调了不忽略错误、保持处理一致性及避免过度处理的重要性,并通过文件操作和网络编程实例展示了错误处理的应用。
59 4
|
2月前
|
NoSQL C语言 索引
十二个C语言新手编程时常犯的错误及解决方式
C语言初学者常遇错误包括语法错误、未初始化变量、数组越界、指针错误、函数声明与定义不匹配、忘记包含头文件、格式化字符串错误、忘记返回值、内存泄漏、逻辑错误、字符串未正确终止及递归无退出条件。解决方法涉及仔细检查代码、初始化变量、确保索引有效、正确使用指针与格式化字符串、包含必要头文件、使用调试工具跟踪逻辑、避免内存泄漏及确保递归有基准情况。利用调试器、编写注释及查阅资料也有助于提高编程效率。避免这些错误可使代码更稳定、高效。
469 12
|
3月前
|
存储 算法 Linux
C语言 多进程编程(一)进程创建
本文详细介绍了Linux系统中的进程管理。首先,文章解释了进程的概念及其特点,强调了进程作为操作系统中独立可调度实体的重要性。文章还深入讲解了Linux下的进程管理,包括如何获取进程ID、进程地址空间、虚拟地址与物理地址的区别,以及进程状态管理和优先级设置等内容。此外,还介绍了常用进程管理命令如`ps`、`top`、`pstree`和`kill`的使用方法。最后,文章讨论了进程的创建、退出和等待机制,并展示了如何通过`fork()`、`exec`家族函数以及`wait()`和`waitpid()`函数来管理和控制进程。此外,还介绍了守护进程的创建方法。
C语言 多进程编程(一)进程创建
|
3月前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。
|
3月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
3月前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
3月前
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。