当今的计算机系统使用的基本上都是由18世纪德国数理哲学大师莱布尼兹发现的二进制系统。二进制数字系统中只有两种二进制数码——0和1。
“bit”(比特)被创造出来代表“binary digit”,1bit代表一个二进制数位。
为方便起见,我们不妨将“比特”简单地理解为数字逻辑电路中的开关,键控导致电路的通断产生两种状态:断电(低电平)为0,上电(高电平)为1。
2个比特可以组合出4(2^2)种状态,可表示无符号数值范围[0,3];32个比特可以组合出4294967296(2^32)种状态,可表示无符号数值范围[0,4294967295];……。
在有限范围内的可计量数值几乎都可以用二进制数码串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。由于一个位只能表示二元数值,所以单独一位的用处不大。通常将许多位组成一组作为一个基本存储单位,这样就可以存储范围较大的值。以下展示了现实机器中的一些内存位置:
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
|
|
|
|
|
|
|
|
100 |
|
|
|
104 |
|
|
|
|
|
存储单元(byte)的地址,就像门牌号,敬请参考《指针》。
1byte=8bit,底层都是二进制位串进行移位实现相关操作。标准C++中的<bitset>提供了二进制位串操作接口,以下为打印单字节和通用数据类型二进制位串的示例程序,以直观地查看数据的二进制位串。
typedef unsigned char uchar; // 枚举整数x二进制串中含有多少个1,也可以不停右移除以2看有多少个余数 int enum_filled_bits(int x) { int countx = 0; while (x) { countx++; x = x & (x - 1); } return countx; } // 打印单字节数的二进制位串 void binary_print_byte(uchar c) { for(int i = 0; i < 8; ++i) { if((c << i) & 0x80) // 左移 cout << '1'; else cout << '0'; } cout << ' '; } // 打印通用类型的二进制位串 template <class T> void binary_print_multibytes(T val) { void *f = &val; // 取地址 size_t sz = sizeof(T); uchar *pByte = new uchar[sz]; int i; for(i = 0; i < sz; i++) pByte[i] = *((uchar*)&f + i); #ifdef _BIG_ENDIAN for(i = 0; i != sz; i++) binary_print_byte(pByte[i]); #else // for windoze(Intel X86) for(i = sz; i != 0; i--) binary_print_byte(pByte[i-1]); #endif delete[] pByte; cout << endl; }
#include <stdio.h> #include <windows.h> int main(int argc, const char * argv[]) { int i; BYTE byte[9] = {48,49, 50, 51, 52, 53, 54, 55,0}; printf("每1个byte的16进制BYTE值:\n"); for(i = 0; i < 9; i++) { printf("byte[%d] =%x \n", i, byte[i]); } printf("----------------------------------\n"); printf("字符串byte[9]:\n"); BYTE *pBYTE = byte; printf("*pBYTE = %s\n", pBYTE); printf("----------------------------------\n"); printf("每2个byte组合而成的16进制INT16值:\n"); INT16 *pINT16 = (INT16*)pBYTE; for(i = 0; i < 4; i++) { INT16 i16 = *(pINT16 + i); // Debug printf("*(pINT16 +%d) = %x \n", i, *(pINT16 + i)); } printf("----------------------------------\n"); printf("每4个byte组合而成的16进制INT32值:\n"); INT32 *pINT32 = (INT32*)pBYTE; for(i = 0; i < 2; i++) { INT32 i32 = *(pINT32 + i); // Debug printf("*(pINT32 +%d) = %x \n", i, *(pINT32 + i)); } printf("----------------------------------\n"); return 0; }
说明:*(pINT16 + 0) = 3130而不是3031,这是因为x86架构体系的Windows操作系统为小尾端(little endian)系统,也即在起始地址处存放整数的低序号字节(低地址低字节)。关于字节的大小端问题,网络编程中将有所涉及,在嵌入式开发中经常遇到。
这些位置的每一个都被称为字节(byte),每个字节都包含了存储一个字符所需要的位数。在很多现代的机器上,每个字节包含8个位,可以存储无符号值0至255,或者有符号值-128只127,典型的如ASCII码。每个字节通过地址来标识,如上图中的数字所示。
为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。例如,很多机器以字为单位存储整数,每个字一般由2或4个字节组成。下图所示内存位置与上图相同,但这次它以4个字节的字来表示。
尽管一个字(INT32)包含了4个字节,它仍然只有一个地址。至于它的地址是它最左边那个字节的位置还是最右边那个字节的位置,不同的机器有不同的规定。另一个需要注意的硬件事项是边界对齐(boundary alignment)。在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。但这些问题是硬件设计者的事情,它们很少影响C程序员。我们只对两件事情感兴趣:
1).内存中的每个位置由一个独一无二的地址标识。
2).内存中的每个位置都包含一个值。
在实际程序中我们经常根据需要借助强大的指针对一块内存进行操作,再按字节组合析取出所需数据,平时的程序中经常用到通用指针void*(LPVOID)的妙处就在于可以按照需要操作一块内存,以取所需值类型。
以下程序示例了两种强制类型转换:
// 多字节的截取(容易造成数据的丢失!) int i1 = 0x12345678; // 小序存放顺序4byte:0x78,0x56,0x34,0x12 short s1 = (short)i1; // 析取2byte:0x5678 char c1 = (char)i1; // 析取1byte:0x78 // 短字节的扩展 short s2 = 0x5678; // 小序存放顺序2byte:0x78,0x56 int i2 = (int)s2; // 扩展2byte,高位补0:0x00005678
在移动嵌入式领域,统治市场的MIPS和ARM处理器可通过配置寄存器采用不同的字节序,默认采用Little-Endian,但ARM始终采用Big-Endian存储浮点数。早期使用PowerPC处理器的Mac采用大字节序,如今的Mac同Windows PC一样都使用Intel x86芯片,都是小字节序存储的。TCP/IP协议统一规定采用大端方式封装解析传输数据,也称为网络字节顺序(network byte order,TCP/IP-endian)。因此,在进行网络数据的收发时,都需要执行字节序转换。
以下小程序用于测试输出OS X/iOS系统的字节序:
#import <Foundation/Foundation.h> #import <Foundation/NSByteOrder.h> // 预编译警告信息将在build report log中输出 #if __DARWIN_BYTE_ORDER == __DARWIN_BIG_ENDIAN #pragma message("__DARWIN_BIG_ENDIAN") #elif __DARWIN_BYTE_ORDER == __DARWIN_LITTLE_ENDIAN #pragma message("__DARWIN_LITTLE_ENDIAN") #endif #if defined(__BIG_ENDIAN__) #pragma message("__BIG_ENDIAN__") #elif defined(__LITTLE_ENDIAN__) #pragma message("__LITTLE_ENDIAN__") #endif BOOL isBigEndian() { unsigned short v = 0x4321; return (*((unsigned char*)&v) == 0x43); } BOOL isLittleEndian() { static CFByteOrder bo = CFByteOrderUnknown; if (bo == CFByteOrderUnknown) { // run only once union w { short a; // 2 byte char b; // 1 byte } c; c.a = 1; bo = (c.b?CFByteOrderLittleEndian:CFByteOrderBigEndian); // 高位存储低权字节,则为小端 } return bo; } int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"isBigEndian = %d", isBigEndian()); // 0 NSLog(@"isLittleEndian = %d", isLittleEndian()); // 1 NSLog(@"NSHostByteOrder = %ld", NSHostByteOrder()); // NS_LittleEndian=CFByteOrderLittleEndian=1 } return 0; }
以下简单梳理以下OS X和iOS SDK提供的字节序处理接口。
(1)OS X和iOS SDK的usr/inclue/i386/endian.h、usr/inclue/arm/endian.h中定义了__DARWIN_BYTE_ORDER:
#define __DARWIN_BYTE_ORDER __DARWIN_LITTLE_ENDIAN(2)OS X和iOS SDK的<libkern/OSByteOrder.h>中的OSSwap*操作接口:
_OSSwapInt32 // __builtin_bswap32,<libkern/i386/_OSByteOrder.h>、<libkern/arm/OSByteOrder.h> #define __DARWIN_OSSwapInt32(x) _OSSwapInt32(x) // <libkern/_OSByteOrder.h> #define OSSwapInt32(x) __DARWIN_OSSwapInt32(x) // <libkern/OSByteOrder.h>
(3)OS X和iOS SDK的usr/inclue/sys/_endian.h中定义了ntohs/htons、ntohl/htonl等宏:
#define ntohl(x) __DARWIN_OSSwapInt32(x) #define htonl(x) __DARWIN_OSSwapInt32(x)<arpa/inet.h>中包含了<machine/endian.h>和<sys/_endian.h>。
CF_INLINE uint32_t CFSwapInt32(uint32_t arg) { #if CF_USE_OSBYTEORDER_H return OSSwapInt32(arg); #else uint32_t result; result = ((arg & 0xFF) << 24) | ((arg & 0xFF00) << 8) | ((arg >> 8) & 0xFF00) | ((arg >> 24) & 0xFF); return result; #endif }(4)OS X和iOS SDK的Frameworks/Foundation/NSByteOrder.h中定义了基于CFSwap*操作接口进一步封装了NSSwap*操作接口。
NS_INLINE unsigned int NSSwapInt(unsigned int inv) { return CFSwapInt32(inv); }
参考:
《C/C++基本数据类型》
《Pointers》《Pointers and Memory》《Pointers in C》
《字节那些事儿》《字节序》《ARM Endian》
《轻松记住大小端》《大端模式和小端模式》
《大端与小端详解》《详解大端模式和小端模式》