【问题标题】:Aliasing of otherwise equivalent signed and unsigned types其他等效的有符号和无符号类型的别名
【发布时间】:2026-02-13 16:25:02
【问题描述】:

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 的类型定义(正确吗?)。在 C++ 中,char16_t 是它自己的原始类型,它与uint_least16_t 兼容。与 C 不同,C++ 似乎也不例外,允许兼容但不同的类型使用别名。

【问题讨论】:

  • “X 读取 Y”是什么意思?
  • X x = <value>; Y* yp = (Y*)&x; Y y = *yp; 例如。
  • Annex J.3.5(实现定义的行为),C11 标准草案:— The result of, or the signal raised by, converting an integer to a signed integer type when the value cannot be represented in an object of that type (6.3.1.3).我邀请您查看第 6.3.1.3 节。它回答了你的问题。
  • @EOF 谢谢!没有意识到转换的处理方式与算术溢出不同。
  • C 和 C++ 标准是为了适应一些非常奇怪的 CPU 架构而编写的,因此有一些看似合理的保证是不会实现的。然而,我们 99.9% 的人永远不会遇到奇怪的架构。您通常可以依靠编译器来简单地根据类型重新解释位模式。其他任何事情都是如此令人惊讶的结果,以至于它不可能在现实世界中持续存在。

标签: c++ c language-lawyer


【解决方案1】:

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

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

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

取决于表示。使用二进制补码和等效填充,是的。但不是有符号大小 - 从intunsigned 的转换总是通过同余定义:

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

现在考虑

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

如果解释为unsigned,这肯定是215+1。不过,演员阵容会产生 216-1

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

同样,使用二进制补码和等效填充,是的。有了带符号的幅度,我们可能有-0

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

没有。它们是独立的类型。

可以char16_t*uint_least16_t*别名吗?

技术上不是,但这似乎是对标准的不必要限制。

类型 char16_tchar32_t 表示不同的类型,具有相同的 大小、签名和对齐方式为uint_least16_tuint_least32_t,分别在<cstdint>,称为底层 类型。

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

【讨论】:

  • "为什么它是未定义的?"如果UINT_MAX > INT_MAX,我认为(int)UINT_MAX 是未定义的。原来它只是实现定义的。
  • @TavianBarnes 是的,请参阅 [conv.integral]/3。
  • 标准似乎保证 intN_t 类型使用 2's-compliment 表示。所以只要它们存在,uint32_t *int32_t * 可能会别名,对吧?但除此之外,似乎一切都取决于实施。
  • @TavianBarnes:是的,它们可以别名,它们是对应的有符号和无符号类型。从 C11 开始,允许重新定义 typedefs(当然,如果兼容的话),所以我认为您可以通过 typedef unsigned uint32_t; 等进行一些静态断言。如果编译成功,即使 intuint32_t 也可以使用别名。
  • 我认为标准的限制没有必要。可能存在优化机会(有时某些编译器可能会选择使用它们),这对程序员来说并不是真正的限制(只需声明您想要如何使用它们的变量)。尤其是对于(u)int8_t,这可能很有用,因为与char 不同,这不能为所有内容设置别名(这对于优化编译器来说是可怕的)。
【解决方案2】:

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

对于使用二进制补码的系统,类型双关和有符号到无符号转换是等价的,例如:

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

这里,u1u2 具有相同的值。这是迄今为止最常见的设置(例如,Gcc 记录了所有目标的这种行为)。但是,C 标准还使用反码或符号大小来表示有符号整数的机器。在这样的实现中(假设没有填充位和陷阱表示),整数值的转换和类型双关的结果可能会产生不同的结果。例如,假设符号幅度和 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 整数类型

对于 unsigned char 以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(后者不需要任何一个)。如果有 N 个值位,每个位应代表 12N-1 之间的 2 的不同幂,以便该类型的对象能够表示从 02N-1 的值,使用纯二进制表示;这应称为值表示。未指定任何填充位的值。

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

  • 符号位为0的对应值取反(符号和幅度);
  • 符号位的值为 -2M(二进制补码);
  • 符号位的值为 -2M-1(反码)。

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

例如,unsigned int 可能有值位,其中相应的有符号类型 (int) 有一个填充位,类似unsigned u = ...; int n = *(int *)&u; 可能会导致此类系统上的陷阱表示(读取未定义)行为),但反之则不然。

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

认为,标准将允许其中一种类型具有填充位,该位始终被忽略(因此,两个不同的位模式可以表示相同的值并且可以设置该位在初始化时),但对于其他类型来说是一个 always-trap-if-set 位。然而,这种余地至少受到同上的限制。 p5:

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


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_least16_t 兼容。与 C 不同,C++ 似乎没有例外允许兼容但不同的类型为别名。

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

【讨论】:

    【解决方案3】:

    由于 c 标准没有准确定义应如何存储单数整数,因此没有定义这种情况。所以你不能依赖内部表示。也不会发生溢出。如果您只是对指针进行类型转换,则不会发生其他任何事情,那么在以下计算中对二进制数据进行另一种解释。

    编辑
    哦,我误读了“但不是等效的整数类型”这句话,但我保留了这一段以供您参考:

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

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

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

    【讨论】:

      【解决方案4】:

      关于您对unsigned int*int* 的问题:如果 实际类型中的值不适合您正在阅读的类型, 行为未定义,仅仅是因为标准忽略了定义 在这种情况下的任何行为,以及标准未能定义的任何时候 行为,行为未定义。在实践中,你几乎总是 获得一个值(没有信号或任何东西),但值变化 取决于机器:带有符号大小或 1 的机器 例如,补码将导致不同的值(两种方式) 来自通常的 2 的补码。

      对于其余部分,intlong 是不同的类型,无论它们是什么 表示,int*long* 不能别名。同样,当你 比如说,在 C++ 中,char16_t 在 C++ 中是一个不同的类型,但在 C++ 中是一个 typedef C(所以关于别名的规则不同)。

      【讨论】:

        最近更新 更多