如何编写endian不可知的C/C++代码?

d33*_*tah 35 c c++ endianness

我做了一些谷歌搜索,在这个问题上找不到任何好文章.在实现我想要与endian无关的应用程序时,我应该注意什么?

Car*_*rum 28

只有当你在可能没有相同字节序的系统之间传输字符串敏感的二进制数据(即非文本)时,才有必要关心字节序.通常的解决方案是使用" 网络字节顺序 "(AKA big-endian)来传输数据,然后在必要时在另一端调用字节.

要从主机转换为网络字节顺序,请使用htons(3)htonl(3).要转换回来,请使用ntohl(3)ntohs(3).查看手册页,了解您需要了解的所有信息.对于64位数据,此问题和答案将有所帮助.

  • edA,你的建议让"一个大端系统允许该机器上的代码转换"迫使你在这种类型的系统上拥有特殊代码.那不是"不可知的".忽略大多数时候存在问题并不能解决问题. (3认同)

nos*_*nos 17

在实现我想要与endian无关的应用程序时,我应该注意什么?

首先必须认识到endian成为问题.当你必须从外部的某个地方读取或写入数据时,无论是从文件读取数据还是在计算机之间进行网络通信,它都会成为一个问题.

在这种情况下,endianess对于大于一个字节的整数很重要,因为整数在内存中由不同的平台表示不同.这意味着每次需要读取或写入外部数据时,您需要做的不仅仅是转储程序的内存,或者直接将数据读取到您自己的变量中.

例如,如果你有这段代码:

unsigned int var = ...;
write(fd, &var, sizeof var);
Run Code Online (Sandbox Code Playgroud)

你直接写出了内存的内容var,这意味着数据会被呈现在这些数据的任何地方,就像在你自己的计算机内存中一样.

如果将此数据写入文件,则无论是在大端程序还是小端程序机器上运行程序,文件内容都将不同.所以代码不是endian不可知的,你要避免做这样的事情.

而是专注于数据格式.在读/写数据时,始终先确定数据格式,然后编写代码来处理它.如果您需要阅读一些现有的明确定义的文件格式或实现现有的网络协议,这可能已经为您决定.

一旦你知道数据格式,而不是直接转储一个int变量,你的代码就是这样做的:

uint32_t i = ...;
uint8_t buf[4];
buf[0] = (i&0xff000000) >> 24;
buf[1] = (i&0x00ff0000) >> 16;
buf[2] = (i&0x0000ff00) >> 8;
buf[3] = (i&0x000000ff);
write(fd, buf, sizeof buf);
Run Code Online (Sandbox Code Playgroud)

我们现在选择了最重要的字节,并将其作为缓冲区中的第一个字节,并将最低有效字节放在缓冲区的末尾.buf无论主机的字节顺序如何,该整数都以大端格式表示- 因此此代码与字节序无关.

此数据的使用者必须知道数据以大端格式表示.并且无论程序运行的主机如何,此代码都会读取该数据:

uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[0] << 24;
i |= (uint32_t)buf[1] << 16;
i |= (uint32_t)buf[2] << 8;
i |= (uint32_t)buf[3];
Run Code Online (Sandbox Code Playgroud)

相反,如果您需要读取的数据是小端格式,那么endianess不可知代码就是

uint32_t i ;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i  = (uint32_t)buf[3] << 24;
i |= (uint32_t)buf[2] << 16;
i |= (uint32_t)buf[1] << 8;
i |= (uint32_t)buf[0];
Run Code Online (Sandbox Code Playgroud)

你可以创建一些漂亮的内联函数或宏来包装和解包你需要的所有2,4,8字节整数类型,如果你使用它们并关心数据格式而不是你运行的处理器的字节序,你的代码将会不依赖于它正在运行的结束.

这是比许多其他解决方案更多的代码,我还没有编写一个程序,这项额外的工作对性能产生了任何有意义的影响,即使在1Gbps以上的数据混乱时也是如此.

它还避免了错误的内存访问,您可以通过例如方法轻松获得

uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = ntohl(*(uint32_t)buf));
Run Code Online (Sandbox Code Playgroud)

这也可能导致性能受到打击(在某些情况下微不足道,在其他情况下有很多数量级),而在无法对整数进行未对齐访问的平台上则更糟糕.


Pub*_*bby 14

这可能是一篇很好的文章供你阅读:字节顺序谬误

除了编译器编写者等之外,计算机的字节顺序根本不重要,编译器编写者等对映射到寄存器片段的存储器字节的分配感到困惑.您可能不是编译器编写器,因此计算机的字节顺序对您来说无关紧要.

注意短语"计算机的字节顺序".重要的是外围设备或编码数据流的字节顺序,但是 - 这是关键点 - 执行处理的计算机的字节顺序与数据本身的处理无关.如果数据流对字节顺序B的值进行编码,那么用字节顺序C解码计算机上的值的算法应该是B,而不是B和C之间的关系.

  • 这应该是评论而不是答案. (17认同)
  • *那个人写了一些Unix的一部分...... http://en.wikipedia.org/wiki/Rob_Pike (4认同)
  • @nhahtdh我通常会同意,除了OP提到他在发布之前正在寻找文章,所以我假设发表一篇文章会没事的. (2认同)
  • @Pubby:如果链接死亡,最好提供某种摘要. (2认同)

jst*_*ine 8

几个答案涵盖了文件IO,这当然是最常见的endian问题.我会谈到一个尚未提及的:工会.

以下联合是SIMD/SSE编程中的常用工具,并且不是字节顺序的:

union uint128_t {
    _m128i      dq;
    uint64_t    dd[2];
    uint32_t    dw[4];
    uint16_t    dh[8];
    uint8_t     db[16];
};
Run Code Online (Sandbox Code Playgroud)

访问dd/dw/dh/db表单的任何代码都将以特定于endian的方式执行.在32位CPU上,通常可以看到更简单的联合,允许更容易地将64位算术分解为32位部分:

union u64_parts {
    uint64_t    dd;
    uint32_t    dw[2];
};
Run Code Online (Sandbox Code Playgroud)

因为在这个用例中很少(如果有的话)你想迭代联合的每个元素,我更喜欢写这样的联合:

union u64_parts {
    uint64_t dd;
    struct {
#ifdef BIG_ENDIAN
        uint32_t dw2, dw1;
#else
        uint32_t dw1, dw2;
#endif
    }
};
Run Code Online (Sandbox Code Playgroud)

结果是对任何直接访问dw1/dw2的代码进行隐式endian-swapping.相同的设计方法也可用于上面的128位SIMD数据类型,但最终会更加冗长.

免责声明:由于关于结构填充和对齐的标准定义松散,因此联盟的使用常常令人不悦.我发现工会非常有用并且已经广泛使用它们,并且我在很长一段时间(15年以上)都没有遇到任何交叉兼容性问题.对于任何针对x86,ARM或PowerPC的当前编译器,联合填充/对齐将以预期且一致的方式运行.