本文主要是介绍字节序、大端序与小端序及其相关转换,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
大端序与小端序及其相关转换
一、字节序定义
字节序,为字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据当然就没有顺序可言了,其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题。
二、大端序与小端序
字节序分为两类:Big-Endian和Little-Endian。
1. Little-Endian(小端序)就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
2. Big-Endian(大端序)就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
unsigned int整型数据0x12345678为例,其大端序、小端序的存储内容如图所示
如0x01000000就为内存的低地址端,0x01000003就为内存的高地址端
而数据0x12345678中靠近左边的为高位字节,靠近右边的为低位字节,也就是说0x12为高位字节,0x78为低位字节
unsigned int i= 0x12345678;unsigned char* p = (unsigned char*)&i;printf("%x\n", p[0]); //打印出16进制的78printf("%x\n", p[3]); //打印出16进制的12
如代码所示,对 i 的地址进行类型转换,指针p指向的位置是 i 在内存中存储空间的低地址端的位置,又因为个人电脑内存中存储数据是以小端序来存储的,所以内存低地址端存的是低位字节,所以p[0]以16进制来打印,打印出78,p[3]打印出12
三、网络字节序
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于 TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。比如,以太网头部中2字节的“以太网帧类型”,表示后面数据的类型。UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序; 比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是0x08,0x06。在内存中的映象如下图所示:
栈底 (高地址)
---------------
0x06 -- 低位字节
0x08 -- 高位字节
---------------
栈顶 (低地址)
该字段的值为0x0806。按照大端方式存放在内存中。
四、内存空间中的相关布局
关于内存空间布局情况的说明,大致如下图:
----------------------- 最高内存地址 0xffffffff
| 栈底
.
. 栈
.
栈顶
-----------------------
|
|
\|/
NULL (空洞)
/|\
|
|
-----------------------
堆
-----------------------
未初始化的数据
----------------(统称数据段)
初始化的数据
-----------------------
正文段(代码段)
----------------------- 最低内存地址 0x00000000
以上图为例如果我们在栈上分配一个unsigned char buf[4],那么这个数组变量在栈上是如何布局的呢?看下图:
栈底 (高地址)
----------
buf[3]
buf[2]
buf[1]
buf[0]
----------
栈顶 (低地址)
现在我们弄清了高低地址,接着来弄清高/低字节,如果我们有一个32位无符号整型0x12345678,那么高位是什么,低位又是什么呢?其实很简单。在十进制中我们都说靠左边的是高位,靠右边的是低位,在其他进制也是如此。就拿 0x12345678来说,从高位到低位的字节依次是0x12、0x34、0x56和0x78。
高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义,并用图示说明两种字节序:
以unsigned int value = 0x12345678为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]来表示value:
Big-Endian: 低地址存放高位,如下图:
栈底 (高地址)
---------------
buf[3] (0x78) -- 低位字节
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位字节
---------------
栈顶 (低地址)
Little-Endian: 低地址存放低位,如下图:
栈底 (高地址)
---------------
buf[3] (0x12) -- 高位字节
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位字节
---------------
栈顶 (低地址)
在现有的平台上Intel的X86采用的是Little-Endian,而像Sun的SPARC采用的就是Big-Endian。
五、网络通讯字节的转换
相同字节序的平台在进行网络通信时可以不进行字节序转换,但是跨平台进行网络数据通信时必须进行字节序转换。
原因如下:网络协议规定接收到得第一个字节是高字节,存放到低地址,所以发送时会首先去低地址取数据的高字节。小端模式的多字节数据在存放时,低地址存放的是低字节,而被发送方网络协议函数发送时会首先去低地址取数据(想要取高字节,真正取得是低字节),接收方网络协议函数接收时会将接收到的第一个字节存放到低地址(想要接收高字节,真正接收的是低字节),所以最后双方都正确的收发了数据。而相同平台进行通信时,如果双方都进行转换最后虽然能够正确收发数据,但是所做的转换是没有意义的,造成资源的浪费。而不同平台进行通信时必须进行转换,不转换会造成错误的收发数据,字节序转换函数会根据当前平台的存储模式做出相应正确的转换。当前平台为大端
六、大小端序的判断
(1)通过指针判断(返回真则为小端序,返回假则为大端序)
bool isLittleEndian()
{unsigned int i = 0x12345678;unsigned char* c = (unsigned char*)&i;return(*c == 0x78); //判断是否是低位字节存内存低地址 *c取得就是内存中存放i的低地址
}
(2)通过联合体判断(返回真则为小端序,返回假则为大端序)
bool isLittleEndian()
{union{int i;char c;}udata;udata.i = 1;return(udata.c == 1);
}
(3)linux环境下,通过htonl等函数直接判断
#include <arpa/inet.h>
bool isLittleEndian()
{return (1 != htonl(1));
}
七、大小端的转换
(1)自定义函数
//短整型大小端互换
#define BigLittleSwap16(A) ((((uint16_t)(A) & 0xff00) >> 8 ) | \\(((uint16_t)(A) & 0x00ff) << 8 ))//长整型大小端互换
#define BigLittleSwap32(A) ((((uint32_t)(A) & 0xff000000) >> 24) | \\(((uint32_t)(A) & 0x00ff0000) >> 8 ) | \\(((uint32_t)(A) & 0x0000ff00) << 8 ) | \\(((uint32_t)(A) & 0x000000ff) << 24))
结合判断大小端的函数,如果本机是大端,则可以直接返回,如果本机是小端,则需要进行字节序的转换,或者进行网络数据的转换,再返回
(2)winsock.h头文件中的函数
uint32_t htonl(uint32_t hostlong);//32位的主机字节序转换到网络字节序
uint16_t htons(uint16_t hostshort);//16位的主机字节序转换到网络字节序
uint32_t ntohl(uint32_t netlong);//32位的网络字节序转换到主机字节序
uint16_t ntohs(uint16_t netshort);//16位的网络字节序转换到主机字节序
拿htonl和ntohl来分析,htonl函数的内部实现原理是这样,先判断主机是什么模式存储,如果是大端模式,就跟网络字节序一致,直接返回参数即可,如果是小端模式,则把形参转换成大端模式存储在一个临时参数内,再把临时参数返回;而ntohl函数的实现原理也是一样的过程,但是要注意它的参数,参数是网络字节序,就是大端模式存储,因此当判断主机是大端模式的时候,会直接返回,因为该函数默认会认为形参是网络字节序,把它当大端模式来看,如果判断主机是小端模式,就会将实参做转换,转换的过程并不复杂,就是逆序存储各个字节的数据,所以结果就被转换。
说到这里,可以看出一个规律来,就是如果主机与网络字节序不一致(也就是小端模式),这四个函数的返回值与传递进去的实参值的字节排序肯定是逆序的,所以返回值绝对不等于实参值,例如htonl(1)的结果肯定不是1,而如果主机与网络字节序一致(也就是大端模式),则这四个函数根本就没有做转换操作,而是直接返回实参值,这样他们的返回结果就肯定与实参值相同,即htonl(1)的结果是1。
这样,我们就得到了一个非常简便的判断系统是什么模式的方法,就是直接利用这四个函数来判断,如:
if(1 != htonl(1))
{//小端模式,作相应处理
}
else
{//大端模式,作相应处理
}
或者直接用一个判断if(1 != htonl(1)),只有主机字节序与网络字节序不一致时,才调用那些函数去转换,否则不需要处理,这样可以减少多余的函数调用。
八、其他相关转化
(1)将整数按照“大端序”格式存储在数组中
/*
* Function: ConverseUItoBeArray
* Description: 将无符号整数转换成“大端序”存储的无符号字符数组
* Parameter: srcData --[in] 源整数
* desBeArray --[out] 目标“大端序”存储的数组数据
* Return 0 成功
* 非0 失败
* Note:
* Other:
*/
int MULCONVERSE_CALL ConverseUItoBeArray(unsigned int srcData,unsigned char *desBeArray)
{if (desBeArray == NULL_POINT){return ERR_NULL_POINT;}desBeArray[0] = (unsigned char)(srcData>>24);desBeArray[1] = (unsigned char)(srcData>>16);desBeArray[2] = (unsigned char)(srcData>>8);desBeArray[3] = (unsigned char)srcData;return _SUCCESS;
}
(2)将整数按照“小端序”格式存储在数组中
/*
* Function: ConverseUItoLeArray
* Description: 将无符号整数转换成“小端序”存储的无符号字符数组
* Parameter: srcData --[in] 源整数
* desLeArray --[out] 目标“小端序”存储的数组数据
* Return 0 成功
* 非0 失败
* Note:
* Other:
*/
int MULCONVERSE_CALL ConverseUItoLeArray(unsigned int srcData,unsigned char *desLeArray)
{if (desLeArray == NULL_POINT){return ERR_NULL_POINT;}desLeArray[3] = (unsigned char)(srcData>>24);desLeArray[2] = (unsigned char)(srcData>>16);desLeArray[1] = (unsigned char)(srcData>>8);desLeArray[0] = (unsigned char)srcData;return _SUCCESS;
}
这篇关于字节序、大端序与小端序及其相关转换的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!