当今的计算机系统使用的基本上都是由18世纪德国数理哲学大师莱布尼兹发现的二进制系统。二进制数字系统中只有两种二进制数码——0和1。
“bit”(比特)被创造出来代表“binary digit”,1bit代表一个二进制数位。
为方便起见,我们不妨将“比特”简单地理解为数字逻辑电路中的开关,键控导致电路的通断产生两种状态:断电(低电平)为0,上电(高电平)为1。
2个比特可以组合出4(2^2)种状态,可表示无符号数值范围[0,3];32个比特可以组合出4294967296(2^32)种状态,可表示无符号数值范围[0,4294967295];……。
在有限范围内的可计量数值几乎都可以用二进制数码串组合表示,计算机的内存由数以亿万计的比特位存储单元(晶体管)组成。由于一个位只能表示二元数值,所以单独一位的用处不大。通常将固定位数的位串作为一个基本存储单位,这样就可以存储范围较大的值。
存储单元(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),每个字节都包含了存储一个字符所需要的位数。
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
|
|
|
|
|
|
|
|
在很多现代的机器上,每个字节包含8个位,可以存储无符号值0至255,或者有符号值-128只127,典型的如ASCII码。每个字节通过地址来标识,如上图中的数字所示。
为了存储更大的值,我们把两个或更多个字节合在一起作为一个更大的内存单位。例如,很多机器以字为单位存储整数,每个字一般由2或4个字节组成。下图所示内存位置与上图相同,但这次它以4个字节的字来表示。
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
尽管一个字(INT32)包含了4个字节,它仍然只有一个地址。至于它的地址是它最左边那个字节的位置还是最右边那个字节的位置,不同的机器有不同的规定。另一个需要注意的硬件事项是边界对齐(boundary alignment)。在要求边界对齐的机器上,整型值存储的起始位置只能是某些特定的字节,通常是2或4的倍数。但这些问题是硬件设计者的事情,它们很少影响C程序员。我们只对两件事情感兴趣:
1).内存中的每个位置由一个独一无二的地址标识。
2).内存中的每个位置都包含一个值。
在实际程序中我们经常根据需要借助强大的指针对一块内存进行操作,再按字节组合析取出所需数据,平时的程序中经常用到通用指针void*(LPVOID)的妙处就在于可以按照需要操作一块内存,以取所需值类型。
下图为 MSDN 中 C++ Type System (Modern C++) 的 Fundamental (built-in) types 按字节宽度的层砌图(Layout of Source Language Data Types):
// 多字节的截取(容易造成数据的丢失!) 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)。因此,在进行网络数据的收发时,都需要执行字节序转换。
以下为 MSDN 中关于 Packet byte/bit order 的阐述:
For packets, the bit numbering convention followed is the same as that used in RFCs, namely: the high (most significant) bit of the first byte to hit the wire is in packet bit 0, and the low bit of the last byte to hit the wire is in packet bit 31 (so that the bits are shown from left-to-right in the order they naturally appear over the network).
以下小程序用于测试输出 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> #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); }
(5)OS X和iOS SDK的usr/inclue/sys/_endian.h中定义了ntohs/htons、ntohl/htonl等宏:
请看下面的结构:
struct MyStruct { double dda1; char dda; int type; };
对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?
也许你会这样求:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当在VC中测试上面结构的大小时,你会发现 sizeof(MyStruct)=16。你知道为什么在VC中会得出这样一个结果吗?
其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vs6.0&vs8.0,32位系统)。
类型 |
对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量) |
char |
偏移量必须为sizeof(char),即1的倍数 |
short |
偏移量必须为sizeof(short),即2的倍数 |
int |
偏移量必须为sizeof(int),即4的倍数 |
float |
偏移量必须为sizeof(float),即4的倍数 |
double |
偏移量必须为sizeof(double),即8的倍数 |
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
下面用前面的例子来说明VC到底怎么样来存放结构的。
struct MyStruct { double dda1; char dda; int type; };
为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,依次分配。
(1)先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同,刚好偏移量0刚好为sizeof(double)的倍数,该成员变量占用sizeof(double)=8个字节;
(2)接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用sizeof(char)=1个字节;
(3)接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是=4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;
(4)这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。
(5)所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。
下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况:
struct MyStruct { char dda; double dda1; int type; };
这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruct)=24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明)
struct MyStruct { /*偏移量为0,满足对齐方式,dda占用1个字节*/ char dda; /*下一个可用的地址的偏移量为1,不是sizeof(double)=8的倍数, 需要补足个字节才能使偏移量变为(满足对齐方式),因此VC自动填充7个字节, dda1存放在偏移量为8的地址上,它占用8个字节。*/ double dda1; /*下一个可用的地址的偏移量为16,是sizeof(int)=4的倍数,满足int的对齐方式, 所以不需要VC自动填充,type存放在偏移量为的地址上,它占用4个字节。*/ int type; };
所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruct)为1+7+8+4+4=24。其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。
VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。
VC中提供了#pragmapack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。下面举例说明其用法。
#pragma pack(push) // 保存对齐状态 #pragma pack(4) // 设定为4字节对齐 struct test { char m1; double m4; int m3; }; #pragma pack(pop) // 恢复对齐状态
以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragmapack(4)改为#pragmapack(16),那么我们可以得到结构的大小为24。(请读者自己分析)
在VC6中,Project SettingsàC/C++àStruct member alignment中默认值为8Bytes *。Struct member alignment用以指定数据结构中的成员变量在内存中是按几字节对齐的,根据计算机数据总线的位数,不同的对齐方式存取数据的速度不一样。这个参数对数据包网络传输等应用尤为重要,不是存取速度问题,而是数据位的精确定义问题,一般在程序中使用#pragma pack来指定。
参考:
《Pointers》《Pointers and Memory》《Pointers in C》
《字节那些事儿》《字节序》《ARM Endian》
《什么是内存对齐》
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。