在其他方面等效的有符号和无符号类型的别名

Aliasing of otherwise equivalent signed and unsigned types

本文关键字:无符号 类型 别名 符号 方面 其他      更新时间:2023-10-16

C和C++标准都允许相同整数类型的有符号和无符号变体相互别名。例如,unsigned int*int*可以是别名。但这并不是故事的全部,因为它们显然有不同的可代表价值。我有以下假设:

  • 如果通过int*读取unsigned int,则该值必须在int的范围内,否则会发生整数溢出,并且行为未定义。这是正确的吗
  • 如果通过unsigned int*读取int,则负值环绕,就像它们被广播到unsigned int一样。这是正确的吗
  • 如果该值在intunsigned int的范围内,则通过任一类型的指针访问该值是完全定义的,并给出相同的值。这是正确的吗

此外,兼容但不等价的整数类型如何?

  • intlong具有相同范围、对齐等的系统上,int*long*可以别名吗?(我想不会。)
  • char16_t*uint_least16_t*可以别名吗?我怀疑这在C和C++之间是不同的。在C中,char16_tuint_least16_t的typedef(正确吗?)。在C++中,char16_t是它自己的基元类型,与uint_least16_t兼容。与C不同,C++似乎无一例外地允许兼容但不同的类型进行别名

如果通过int*读取unsigned int,则值必须为在CCD_ 23的范围内或者发生整数溢出并且行为是不明确的。这是正确的吗?

为什么它是未定义的?不存在整数溢出,因为不进行转换或计算。我们采用unsigned int对象的对象表示,并通过int来查看它。unsigned int对象的值以何种方式转置为int的值完全由实现定义。

如果通过unsigned int*读取int,则负值换行就好像它们被强制转换为无符号的int。这是正确的吗?

取决于表示形式。有二的补码和等价的填充,是的。但没有带符号的幅度-从intunsigned的投射总是通过同余定义的:

如果目标类型为unsigned,则结果值为与源整数一致的最小无符号整数(模2n,其中n是用于表示无符号类型的位数)。[注:在二的补码表示中转换是概念性的,位模式没有变化(如果没有截断)。——尾注]

现在考虑

10000000 00000001  // -1 in signed magnitude for 16-bit int

如果将其解释为unsigned,则这肯定是215+1。铸造会产生21-1

如果值在int和unsigned int的范围内,通过任何一种类型的指针访问它都是完全定义的,并且给出相同的值。这是正确的吗?

同样,使用2的补码和等价填充,是的。有了符号星等,我们可能得到-0

在CCD_ 36和CCD_,int*long*可以别名吗?(我想不会。)

否。它们是独立的类型。

char16_t*uint_least16_t*可以别名吗?

技术上没有,但这似乎是标准的一个不必要的限制。

类型char16_tchar32_t表示具有相同大小、有符号性和对齐为uint_least16_tuint_least32_t,在<cstdint>中分别称为类型。

因此,它实际上应该是可能的,没有任何风险(因为不应该有任何填充)。

如果通过unsigned int*读取int,负值会环绕起来,就像它们被投射到unsigned int一样。这是正确的吗

对于使用二的补码的系统,类型punning和有符号到无符号的转换是等效的,例如:

int n = ...;
unsigned u1 = (unsigned)n;
unsigned u2 = *(unsigned *)&n;

这里,u1u2都具有相同的值。这是迄今为止最常见的设置(例如,Gcc记录了其所有目标的这种行为)。然而,C标准也处理使用1的补码或符号幅度来表示有符号整数的机器。在这样的实现中(假设没有填充位和陷阱表示),整数值转换和类型双关的结果可能会产生不同的结果。举个例子,假设符号大小和n初始化为-1:

int n = -1;                     /* 10000000 00000001 assuming 16-bit integers*/
unsigned u1 = (unsigned)n;      /* 11111111 11111111
effectively 2's complement, UINT_MAX */
unsigned u2 = *(unsigned *)&n;  /* 10000000 00000001
only reinterpreted, the value is now INT_MAX + 2u */

转换为无符号类型意味着比该类型的最大值多加/减一,直到值在范围内。取消引用转换后的指针只是重新解释位模式。换句话说,u1初始化中的转换在2的补码机上是非运算的,但需要在其他机器上进行一些计算。

如果通过int*读取unsigned int,则该值必须在int的范围内,否则会发生整数溢出,并且行为未定义。这是正确的吗

不完全是。位模式必须表示新类型中的有效值,旧的是否可表示并不重要。来自C11(n1570)[省略脚注]:

6.2.6.2整数类型

对于除无符号字符以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(不必有任何后者)。如果存在N值位,则每个位应表示12N-1之间的2的不同幂,因此该类型的对象应能够使用纯二进制表示来表示从02N-1中的值;这将被称为价值表示。任何填充位的值都是未指定的。

对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。不需要任何填充位;signed char不应具有任何填充位。应恰好有一个符号位。作为值位的每个位应具有与对应无符号类型的对象表示中的相同位相同的值(如果有符号类型中有M值位,无符号类型中存在N,则M≤N)。如果符号位为零,则不应影响结果值。如果符号位为1,则应通过以下方式之一修改该值:

  • 符号位为0的对应值被否定(符号和幅度)
  • 符号位具有值-2M(二的补码)
  • 符号位具有值-2M-1(一的补码)

这些应用中的哪一个是实现定义的,比如符号位为1且所有值位为零(对于前两个)的值,还是符号位且所有值位数为1(对于1的补码)的值是陷阱表示还是正常值。在符号和幅度以及1的补码的情况下,如果这个表示是一个正常值,它被称为负零

例如,unsigned int可以具有值位,其中相应的有符号类型(int)具有填充位,类似于unsigned u = ...; int n = *(int *)&u;的东西可能会在这样的系统上产生陷阱表示(读取它是未定义的行为),但不是相反。

如果值在intunsigned int的范围内,则通过任一类型的指针访问它是完全定义的,并给出相同的值。这是正确的吗

I认为,标准将允许其中一种类型具有填充位,该填充位总是被忽略(因此,两个不同的位模式可以表示相同的值,并且该位可以在初始化时设置),但如果为另一种类型设置位,则该位总是陷阱。然而,这种回旋余地至少受到同上第5页:的限制

任何填充位的值都是未指定的。符号位为零的有符号整数类型的有效(非陷阱)对象表示是相应无符号类型的有效对象表示,并且应表示相同的值。对于任何整数类型,所有位为零的对象表示应为该类型中值零的表示。


intlong具有相同范围、对齐等的系统上,int*long*可以别名吗?(我想不会。)

如果你不使用它们,它们当然可以;)但不,以下内容在此类平台上无效:

int n = 42;
long l = *(long *)&n; // UB

char16_t*uint_least16_t*可以别名吗?我怀疑这在C和C++之间是不同的。在C中,char16_tuint_least16_t的typedef(正确吗?)。在C++中,char16_t是它自己的基元类型,与uint_list16_t兼容。与C不同,C++似乎无一例外地允许兼容但不同的类型进行别名

我不确定C++,但至少对C来说,char16_t是一个typedef,但对uint_least16_t来说不一定,它很可能是某些特定于实现的__char16_t的typedef、某些与uint_least16_t不兼容的类型(或任何其他类型)。

由于c标准没有准确定义单整数的存储方式,因此没有定义这种情况。所以你不能依赖内部的表现。也没有发生溢出。如果您只是键入一个指针,则不会发生其他情况,然后在下面的计算中对二进制数据进行另一种解释。

编辑
哦,我误读了短语"但不是等价的整数类型",但我保留了这段话以供您感兴趣:

你的第二个问题有更多的麻烦。许多机器只能从正确对齐的地址读取数据,数据必须位于类型宽度的倍数上。如果你从一个不可被4除的地址读取一个int32(因为你抛出了一个2字节的int指针),你的CPU可能会崩溃。

你不应该依赖于类型的大小。如果选择其他编译器或平台,则longint可能不再匹配。

结论:
不要这样做。您编写了高度依赖于平台(编译器、目标机器、体系结构)的代码,这些代码将错误隐藏在禁止任何警告的强制转换之后。

关于unsigned int*int*的问题:如果实际类型中的值与您正在读取的类型不匹配行为是未定义的,仅仅是因为标准忽略了定义在这种情况下的任何行为,以及标准未能定义的任何时间行为,行为是未定义的。在实践中,你几乎总是获得一个值(没有信号或任何东西),但值变化取决于机器:具有符号大小或1的机器例如,补码将产生不同的值(双向)从通常的2的补码。

对于其余的,intlong是不同的类型,无论它们并且int*long*不能别名。同样,作为你例如,在C++中,char16_t在C++中是一个不同的类型,但在C(所以关于混叠的规则是不同的)。