数据存储方式、传输方式、字节序问题很基础,但也很重要,下面简单总结一下:
1. 数据存储方式(主要以数值和字符类型的数据为例说明)
1)数据(包括数值和字符)在计算机内存中都是以二进制的形式存放
数值 --- 补码
字符 --- 对应的ASCII码
数据类型 | 占用字节 | 数据类型 | 占用字节 |
---|---|---|---|
bool | 1 | unsigned int | 4 |
char | 1 | long | 4 |
short | 2 | unsigned long | 4 |
short int | 2 | float | 4 |
unsigned short | 2 | double | 8 |
int | 4 | void* | 4 |
long int | 4 | enum | 4 |
通过下面的举例,深入理解数值和字符在内存中的存储方式。
定义变量:
char szDig[4] = {0x11, 0x00, 0x11, 0x11};
char szTmp[10] = {0x22, 0x00, 0x22, 0x22};
【实验一】对szDig分别取strlen和sizeof操作,结果如下:strlen(szDig) = 2 (字符串结束符'/0'的ASCII码为0)
sizeof(szDig) = 4
【实验二】对szDig和szTmp进行strcat操作
strcat(szTmp, szDig);
结果szTmp数组中各项值为 (十六进制):11 22 00 00 00 00 ... ...
结论:对于非字符串,即便是char类型数组,也慎用strlen、strcat等字符串操作函数。而如下的定义方式可以通过strlen取长度:
char szDig[] = "0000";
2)整型数据在计算机内存中都是以补码的方式存放
正数的补码是其本身
负数的补码是其原码(除符号位外)各位取反,然后加1得到
机器数有三种编码方式:原码、反码和补码。
原码 --- 有效数值部分照抄,符号位正负(或者+-)分别用0 1表示。
反码 --- 可由原码得到。
正数的反码与原码一样;
负数的反码是其原码(除符号位外)各位取反而得到。
补码 --- 可由原码得到。
正数的补码与原码一样;
负数的补码是其原码(除符号位外)各位取反,并加1而得到。
如,定义整型变量 a 和 b:
char a, b;
a = 1;
b = -5;
则,a(1)在内存中为 00000001
b(-5)在内存中为 11111011
即,-5 原码:10000101 反码:11111010 补码:11111011
3)字符型数据在计算机内存中以其对应的ASCII码方式存放
如,定义字符变量 cha 和 chb:
char cha, chb;
cha = 'x';
chb = '1';
则,cha('x')在内存中为 120,转换成二进制为 1111000
chb('1')在内存中为 49,转换成二进制为 0110001
2.数据传输
数据(包括数值和字符)在网络中也都是以二进制 0/1 方式传输,通信双方需提前约定好传输的为字符串还是数值,
或者哪一部分是字符串,哪一部分是数值,以便接收方能够采用对应的方式读取准确的数据。
借用网上的一个例子来说明数据的发送,如下:
int i=999;
char szBuf[1024] = {0};
char *szTmp = "this is a character";
memcpy(szBuf, &i, sizeof(int));
memcpy(szBuf + sizeof(int), szTmp, strlen(szTmp));
send(socket, szBuf, sizeof(int) + strlen(szInput), 0);
无论是数值还是字符型的数据,都可以通过内存直接拷贝至待发送的buf。
(需要注意的就是,不要对非字符串char数组采用strlen获取长度。)
另外,需要考虑的就是下面要提到的字节序的问题。
3. 字节序
掌握字节序之前,需要了解两个概念:网络字节序和主机字节序
是否需要考虑字节序转换的两个条件:
a)对于本地是小端序存储的设备来说,在准备进行网络数据传输时,需要考虑进行字节转换。
b)如果要发送的数据在本地定义的数据类型(包括一些数据结构的成员)超过2字节,就发送时,就需要考虑字节序转换问题。
比如,short、long类型的数据
在发送时,需要分别使用 htons 和 htonl 进行字节序转换;
在接收时,需要分别使用 ntohs 和 ntohl 进行字节序转换。
下面借用网上一篇帖子来解释一下字主机序和网络序(http://www.cnblogs.com/jacktu/archive/2008/11/24/1339789.html)
1)主机字节序
不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序
最常见的有两种
① Little endian:将低序字节存储在起始地址
② Big endian:将高序字节存储在起始地址
LE little-endian (小端序)
最符合人的思维的字节序
地址低位存储值的低位
地址高位存储值的高位
怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说低位值小,就应该放在内存地址小的地方,
也即内存地址低位。反之,高位值就应该放在内存地址大的地方,也即内存地址高位。
BE big-endian (大端序)
最直观的字节序
地址低位存储值的高位
地址高位存储值的低位
为什么说直观,不要考虑对应关系,只需要把内存地址从左到右按照由低到高的顺序写出,
把值按照通常的高位到低位的顺序写出,两者对照,一个字节一个字节的填充进去。
例子:在内存中双字0x01020304(DWORD)的存储方式
内存地址 4000 4001 4002 4003
LE 04 03 02 01
BE 01 02 03 04
例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x34 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
x86系列CPU都是little-endian的字节序.
2)网络字节序
网络字节顺序,是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
socket编程时,尤其需要注意网络字节序的问题,有下面四个字节序转换函数:
htons 将unsigned short类型从主机序转换到网络序
htonl 将unsigned long类型从主机序转换到网络序
ntohs 将unsigned short类型从网络序转换到主机序
ntohl 将unsigned long类型从网络序转换到主机序
在使用little endian的系统中 这些函数会把字节序进行转换;
在使用big endian类型的系统中 这些函数会定义成空宏。
同样,在网络程序开发时,或是跨平台开发时,也应该注意保证只用一种字节序,不然两方的解释不一样就会产生bug。
注:
① 网络与主机字节转换函数:htons ntohs htonl ntohl (s 就是short l是long h是host n是network)
② 不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表。
处理器 操作系统 字节排序
Alpha 全部 Little endian
HP-PA NT Little endian
HP-PA UNIX Big endian
Intelx86 全部 Little endian <--- x86系统是小端字节序系统
Motorola680x 全部 Big endian
MIPS NT Little endian
MIPS UNIX Big endian
PowerPC NT Little endian
PowerPC 非NT Big endian <--- PPC系统是大端字节序系统
RS/6000 UNIX Big endian
SPARC UNIX Big endian
IXP1200 ARM 全部 Little endian