二进制数、位和字节

  • 我们人类常用的十进制数是基于10的幂,计算机适用基底为2的数制系统。
  • 以2为基底表示的数字被称为二进制数。二进制中的2和十进制中的10作用相同。
  • 用二进制系统可以把任意整数(如果有足够的位)表示为0和1的组合。
  • 由于数字计算机通过关闭和打开状态的组合来表示信息,这两种状态分别用0和1来表示,所以使用这套数制系统非常方便。

  1. 二进制整数

    • 通常,1字节包含8位。C语言用字节表示存储系统字符集所需的大小。
    • 可以从左往右给这8位分别编号7~0。在1字节中,编号是7的位被称为高阶位,编号是0的位被称为低阶位。每一位的编号对应2的相应指数。
    • 1字节可存储0~255范围内的数字,总共256个值。或者,通过不同方式的位组合,程序可以用1字节存储-128~+127范围内的整数,总共还是256个值。
  1. 有符号整数
    如何表示有符号整数取决于硬件,而不是C语言。

以下是几种方法:

- **符号量**表示法
    - 概述:最简单的方法是用1位(如高阶位)存储符号,只剩下7位表示数字本身(假设存储在1字节中)。
    - 缺点:这方法的缺点是有两个0:+0和-0。这很容易混淆,而且用两个位组合表示一个值也有些浪费。
    - 表示范围:-127~+127。
- **二进制补码**方法
    - 概述:该方法避免了上述问题,是当今最常用的系统。这种方法和符号量表示法类似,但当符号位是1时表示负值,这两种方法的区别在于如何确定负值:从一个9位组合100,000,000(256的二进制形式)减去一个负数的位组合,结果是该负值的量。
    - 缺点:无,符号量表示法中的-0在这表示-128(256-128=128)。
    - 表示范围:-128~+127。
- **二进制反码**方法
    - 概述:通过反转位组合中的每一位形成一个负数。
    - 缺点:同符号量表示法
    - 表示范围:-127~+127。
  1. 二进制浮点数
    浮点数分两部分存储:二进制小数和二进制指数。

    1. 二进制小数

      • 在十进制小数点后从左往右,各分母(小数位数字除以该分母)都是10的递增次幂。在二进制小数中,使用2的幂作为分母。
      • 许多分数不能用十进制表示法精确的表示。与此类似,许多分数也不能用二进制表示法准确的表示。实际上,二进制表示法只能精确的表示多个1/2的幂的和。
    2. 浮点数表示法
      为了在计算机中表示一个浮点数,要留出若干位(因系统而异)存储二进制分数,其他位存储指数。一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。

其他进制数

计算机界通常使用八进制记数系统十六进制记数系统。因为8和16都是2的幂,这些记数系统比十进制系统更接近计算机的二进制系统。

  1. 八进制

    • 八进制是指八进制记数系统。该系统基于8的幂,用0~7表示数字(正如十进制用0~9表示数字一样)。
    • 每个八进制位对应3个二进制位(2^3=8)。
  2. 十六进制

    • 十六进制是指十六进制记数系统。该系统基于16的幂,用0~15表示数字,但是没有单独的数表示10~15,所以用字母A~F表示。
    • 每个十六进制位对应4个二进制位(2^4=16),那么两个十六进制位恰好对应一个8位字节,因此十六进制很适合表示字节值。

C有两个操控位的工具:第一个是一套(6个)作用于位的按位运算符;第二个是字段数据形式,用于访问int中的位。下面两节将详细介绍。

C按位运算符

  1. 按位逻辑运算符

    1. 二进制反码或按位取反:~
      一元运算符~把1变为0,把0变为1。
    2. 按位与:&
      二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1。
    3. 按位或:|
      二元运算符|通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位至少有一个为1(两个为1也符合)时,结果才为1。
    4. 按位异或:^
      二元运算符^通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位只有一个为1(或理解为两个对应位不同)时,结果才为1。
  2. 用法:掩码
    按位与运算符常用于掩码,所谓掩码指的是一些设置为开(1)或关(0)的位组合。

    • 如假设定义符号常量MASK为2(即,二进制形式为00000010),只有1号位是1,其他都为0。下面语句:
    flags = flags & MASK;

    把flags中除1号位以外的所有位都设置为0。因为使用按位与运算符(&),MASK为0的位结果都将为0,MASK为1的位结果则不变(为flags的值)。这个过程叫使用掩码,因为掩码中的0隐藏了flags中相应的位。

    • 可以这样类比:把掩码中的0看作不透明,1看作透明。表达式flags & MASK相当于用掩码覆盖在flags的位组合上,只有MASK为1的位才可见:
  3. 用法:打开位(设置位)
    有时,需要打开一个值中的特定位,同时保持其他位不变。这种情况可以使用按位或运算符(|)。

    • 以上一节的flags和MASK为例(只有1号位为1)。下面的语句:
flags = flags | MASK;

把flags的1号位设置为1,且其他位不变。因为使用按位或运算符(|),MASK为0的位结果都为flags的位本身,MASK为1的位结果都为1。

  1. 用法:关闭位(清空位)
    和打开特定的位类似,有时也需要在不影响其他位的情况下关闭指定的位。

    • 假设关闭变量flags中的1号位。同样,MASK只有1号位为1(即打开):
flags = flags & ~MASK;

首先MASK按位取反后就变成除了1号位为0其他都为1了,然后按位与运算,1号位已经为0,则无论flags的对应位为何结果都为0,其他位则不变。

  1. 用法:切换位
    切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符(^)。

    • 如果使用^组合一个值和一个掩码,将切换该值与MASK为1的位相对应的位,该值与MASK为0的位相对应的位不变:
flags = flags ^ MASK;

把flags的1号位切换,且其他位不变。因为使用按位异或运算符(^),MASK为0的位结果都为flags的位本身,MASK为1的位结果都是对flags的对应位取反。

  1. 用法:检查位的值
    前面介绍了如何改变位的值。有时需要检查某位的值。
if((flags & MASK) == MASK){}

不能直接比较,因为是比较的值而非单独的位,因此,必须覆盖(用法:掩码)flags中的其他位,只用1号位和MASK比较。

  1. 移位运算符

    1. 左移:<<
      左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
    2. 右移:>>
      右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢失。对于无符号类型,用0填充空出的位置;对于有符号类型,其结果取决于机器。
    3. 用法:移位运算符

      • 移位运算符针对2的幂提供快速有效的乘法和除法:
      number << n; //number乘以2的n次幂
      number >> n; //如果number为正,则用number除以2的n次幂

      这些移位运算符类似于在十进制中移动小数点来乘以或除以10。

      • 移位运算符还可以用于从较大单元中提取一些位。

位字段

  • 操作位的第2种方法是位字段。位字段是一个signed intunsigned int类型变量中的一组相邻的位。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。
//4个1位的字段
struct {
    unsigned int autfd : 1;
    unsigned int bldfc : 1;
    unsigned int undln : 1;
    unsigned int itals : 1;
} prnt;        //变量prnt被存储在int大小的内存单元中,但此例只使用了其中4位
  • 带有位字段的结构提供一种记录设置的方便途径。许多设置就是简单的二选一。内含位字段的结构允许在一个存储单元中存储多个设置。
  • 有时,某些设置也有多个选择,因此需要多位来表示。这没问题,字段不限制1位大小。
struct {
    unsigned int code1 : 2;
    unsigned int code2 : 2;
    unsigned int code3 : 8;
} prcode;

以上不管位数多少,赋值时要确保不超出字段可容纳的范围。如1位时只能存储0或1。

  • 如果声明的位数超过了一个unsigned int类型的大小会用到下一个unsigned int类型的存储位置。
  • 一个字段不允许跨越两个unsigned int之间的边界(即不能一部分在前一个数据单元,一部分在后一个数据单元里)。编译器会自动移动跨界的字段,保持unsigned int的边界对齐。一旦发生这种情况,前一个unsigned int中会留下一个未命名的“洞”。
  • 可以使用未命名的字段宽度“填充”未命名的“洞”。使用一个宽度为0的未命名字段迫使下一字段与下一个整数对齐
struct {
    unsigned int field1 : 1;
    unsigned int         : 2;
    unsigned int field2 : 1;
    unsigned int         : 0;
    unsigned int field3 : 1;
} stuff;
  • C以unsigned int作为位字段结构的基本布局单元。即使一个唯一的成员是1位字段,该结构大小也是一个unsigned int类型的大小。
  • 在同类型的编程问题中,位字段和按位运算符是两种可替换的方法,用哪种方法都可以。

对齐特性

  • C11的对齐特性比用位填充字节更自然,它们还代表了C在处理硬件相关问题上的能力。在这种上下文中,对齐指的是如何安排对象在内存中的位置。
  • _Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后面的圆括号中写上类型即可:
size_t d_align = _Alignof(float);    //如果d_align的值是4,代表4是存储该类型值相邻地址的字节数,即保持两个数据单元之间相隔4字节,即使有空余位。
  • 一般而言,对齐指都应该是2的非负整数次幂。较大的对齐值被称为stricter或stronger,较小的对齐值被称为weaker。
  • 可以使用_Alignof说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。

标签: C/C++, C-Primer-Plus

添加新评论