(我想这个问题可能适用于许多类型语言,但我选择使用C ++作为例子。)
为什么没有办法写:
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
指定特定成员,变量和参数的字节顺序?
我知道变量的类型不仅决定了用于存储值的字节数,还决定了在执行计算时如何解释这些字节。
例如,这两个声明每个都分配一个字节,对于两个字节,每个可能的8位序列都是有效值:
signed char s;
unsigned char u;
但是相同的二进制序列可能被不同地解释,例如,分配到11111111
时,s
表示-1,而分配给u
时表示255。当有符号和无符号变量涉及相同的计算时,编译器(主要)负责正确的转换。
根据我的理解,字节序只是同一原则的变体:基于编译时间信息对二进制模式的不同解释。
在允许低级编程的类型语言中使用该功能似乎是显而易见的。但是,这不是C,C ++或我所知的任何其他语言的一部分,我没有在网上找到任何关于此的讨论。
我会在询问后的第一个小时内从许多评论中总结一些内容:
此外,现在我意识到签名和字节序不是一个完美的类比,因为:
big int
和little int
都具有完全相同的值范围。unsigned char
表示,并且(假设char
有8位)130不能用signed char
表示。因此,改变某些变量的字节顺序永远不会改变程序的行为(除了逐字节访问),而签名通常会改变。
答案 0 :(得分:53)
[intro.abstract]/1
本文档中的语义描述定义了一个参数化的非确定性抽象机器。 本文档不要求符合实现的结构。 特别是,它们不需要复制或模拟抽象机器的结构。 相反,需要符合实现来模拟(仅)抽象机器的可观察行为,如下所述。
C ++无法定义字节序限定符,因为它没有字节序的概念。
关于signness和endianness之间的区别,OP写道
在我的理解中,字节序只是同一原则[(signness)]的变体:基于有关存储它的存储器的编译时信息对二进制模式的不同解释。
我认为签名都具有语义和代表性方面 1 。 [intro.abstract]/1
暗示的是C ++只关心语义,并且从不解决有符号数应该在内存 2 中表示的方式。实际上,"sign bit" only appears once in the C++ specs并参考实现定义的值
另一方面,字节序仅具有代表性的方面:字节序没有传达意义。
使用C ++ 20,出现std::endian
。它仍然是实现定义的,但让我们在不依赖old tricks based on undefined behaviour的情况下测试主机的endian。
1)语义方面:有符号整数可以表示零以下的值;代表性方面:例如,需要保留一点来传达正/负号
2)同样,C ++从未描述应如何表示浮点数,经常使用IEEE-754,但这是由实现做出的选择,无论如何由标准强制执行:[basic.fundamental]/8
"浮点类型的值表示是实现定义的" 。
答案 1 :(得分:37)
除了YSC的回答之外,让我们来看看你的示例代码,并考虑它可能实现的目标
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
您可能希望这将精确指定与架构无关的数据交换(文件,网络,等等)的布局
但这可能无法奏效,因为有些事情仍未明确:
little int32_t
,big int64_t
和int16_t
,如果这是您想要的#pragma
或__attribute__((packed))
或其他一些编译器特定的扩展名或者,您可能只想反映某些指定硬件的字节顺序 - 但big
和little
并未涵盖此处的所有可能性(只有两种最常见)。
因此,该提案不完整(它没有区分所有合理的字节排序安排),效率低下(它没有实现它所规定的内容),并且还有其他缺点:
性能
从本机字节顺序更改变量的字节顺序应该禁用算术,比较等(因为硬件不能在此类型上正确执行它们),或者必须静默注入更多代码,本机创建有序的临时工作。
这里的论点并不是手动转换为/从本机字节顺序更快,它明确地控制它可以更容易地减少不必要的转换次数,并且比转换是隐式的更容易理解代码的行为。
复杂性
所有重载或专用于整数类型的东西现在需要两倍的版本,以应对它传递的非本机端字节值的罕见事件。即使它只是一个转发包装器(有几个转换器可以转换为原生订单),但它仍然有很多代码没有明显的好处。
反对更改语言以支持此功能的最后一个论点是您可以轻松地在代码中执行此操作。更改语言语法是一件大事,并且不会像类型包装器那样提供任何明显的好处:
// store T with reversed byte order
template <typename T>
class Reversed {
T val_;
static T reverse(T); // platform-specific implementation
public:
explicit Reversed(T t) : val_(reverse(t)) {}
Reversed(Reversed const &other) : val_(other.val_) {}
// assignment, move, arithmetic, comparison etc. etc.
operator T () const { return reverse(val_); }
};
答案 2 :(得分:3)
整数(作为数学概念)具有正数和负数的概念。这个抽象的符号概念在硬件中有许多不同的实现。
Endianness不是一个数学概念。 Little-endian是一种硬件实现技巧,用于在具有16或32位寄存器和8位存储器总线的微处理器上提高多字节二进制补码整数运算的性能。它的创建需要使用术语big-endian来描述寄存器和内存中具有相同字节顺序的所有其他内容。
C抽象机包括有符号和无符号整数的概念,没有细节 - 不需要二进制补码算术,8位字节或如何在内存中存储二进制数。
PS:我同意网络或内存/存储中的二进制数据兼容性是PIA。
答案 3 :(得分:2)
这是一个很好的问题,我经常认为这样的事情会很有用。但是,您需要记住C的目标是平台独立性,并且当这样的结构转换为某种底层内存布局时,字节序非常重要。例如,当您将uint8_t缓冲区转换为int时,可能会发生此转换。虽然字节顺序修饰符看起来很整洁但程序员仍需要考虑其他平台差异,例如int大小和结构对齐以及打包。 对于防御性编程,当您希望找到粒度控制某些变量或结构如何在内存缓冲区中表示时,最好编写显式转换函数,然后让编译器优化器为每个支持的平台生成最有效的代码。
答案 4 :(得分:2)
Endianness本身并不是数据类型的一部分,而是存储布局。
因此,它不会真正类似于有符号/无符号,而更像是结构中的位字段宽度。与那些类似,它们可用于定义二进制API。
所以你有类似
的东西int ip : big 32;
它将定义存储布局和整数大小,让编译器最好地将字段的使用与其访问相匹配。我不清楚允许的声明应该是什么。
答案 5 :(得分:-1)
Endianness是特定于编译器的,因为它是特定于机器的,而不是作为平台独立性的支持机制。标准 - 是一种抽象,不考虑强加规则使事情“简单” - 它的任务是在编译器之间创建相似性,允许程序员为他们的代码创建“平台独立性” - 如果他们选择这样做如此。
最初,市场份额平台之间存在很多竞争,而且编译器通常由微处理器制造商编写为专有工具,并支持特定硬件平台上的操作系统。英特尔可能不太关心编写支持摩托罗拉微处理器的编译器。
毕竟C是由贝尔实验室发明的重写Unix。