【问题标题】:printf("%p") and casting to (void *)printf("%p") 并转换为 (void *)
【发布时间】:2014-09-12 02:50:59
【问题描述】:

在最近的一个问题中,有人提到用 printf 打印指针值时,调用者必须将指针强制转换为 void *,如下所示:

int *my_ptr = ....

printf("My pointer is: %p", (void *)my_ptr);

对于我的一生,我无法弄清楚为什么。我找到了this question,几乎是一样的。问题的答案是正确的 - 它解释了整数和指针的长度不一定相同。

当然,这是真的,但是当我已经有一个指针时,就像上面的例子一样,我为什么要从int * 转换为void *?什么时候 int * 与 void * 不同?事实上,(void *)my_ptr 何时生成任何不同于简单的my_ptr 的机器代码?

更新: 多位知识渊博的响应者引用了该标准,称传递错误的类型可能会导致未定义的行为。如何?我希望printf("%p", (int *)ptr)printf("%p", (void *)ptr) 生成完全相同的堆栈帧。这两个调用何时会生成不同的堆栈帧?

【问题讨论】:

  • 您将获得一大堆“未定义行为”的答案,本质上,您使用int*void* 没有区别(正如您所说,没有机器为这种类型的转换生成代码)。
  • 实际上,我得到了一些很好的答案来解释理论上的差异。
  • 如果您找到一个平台(编译器、链接器、IDE、CPU、操作系统等),在该平台下这两个选项会产生两个不同结果,请告诉我...事实上,如果您可以将此要求作为问题的一部分添加,那将会很有趣...
  • 关于不同类型的指针表示可能不同的“真实架构”,请参阅此最佳答案:stackoverflow.com/questions/8007825/…。 char/void 指针与字指针具有不同的表示形式。
  • @barakmanos “使用int*void* 没有区别”是一个危险的低标准编码。它的真正意思是“在我尝试的极少数情况下,我没有注意到失败。”当您编写导致未定义行为的代码时,您永远不知道将来何时会失败。升级你的 C 库? BOOM! 使用新版本的编译器重新编译? BOOM 将您的代码移植到另一个架构? BOOM 在完美的条件下编写没有错误的 C 代码已经够难的了。在您的代码中故意 UB 没有可接受的理由。永远。

标签: c pointers casting printf void


【解决方案1】:

%p 转换说明符需要 void * 类型的参数。如果您不传递 void * 类型的参数,则函数调用会调用未定义的行为。

来自 C 标准(C11,7.21.6.1p8 格式化输入/输出函数):

"p - 参数应该是一个指向 void 的指针。"

C 中的指针类型不需要具有相同的大小或相同的表示。

具有不同指针类型表示的实现示例是 Cray PVP,其中指针类型的表示对于 void *char * 是 64 位,但对于其他指针类型是 32 位。

请参阅“Cray C/C++ 参考手册”,“9.1.2.2”中的表 3http://docs.cray.com/books/004-2179-003/004-2179-003-manual.pdf

【讨论】:

  • Nitpick:你不能“调用”未定义的行为;这是一个程序有的东西,或者说没有。程序的行为,未定义(即没有明确定义的行为)
  • @LightnessRacesinOrbit 嘘,我们都是强大的巫师,拥有通过召唤undefined behavior的无敌武器来引发不可预知的混乱和绝对恐怖的罕见力量,以使程序员无处不在而闻名颤抖吧!
  • @LightnessRacesinOrbit 它可以取决于执行的流程,例如 if ( getchar() == 'x' ) 1/0; 只有在输入有 x 时才会有未定义的行为。对我来说,使用“调用 UB”来表示“如果执行到这个语句,程序的行为是未定义的”似乎很好。
  • @LightnessRacesinOrbit "invokes undefined behavior" 是 C 中的一个惯用表达。它在 C 中有着悠久的传统,从高级成员在 comp.lang.c 中的使用到 C 委员会成员的使用。跨度>
  • @ouah:然而,如今的含义与已发布的 C 编程标准基本原理中描述的含义大相径庭,因为后者包括“不可移植”但正确的操作,包括“流行的扩展”,预计绝大多数实现都会以相同的方式处理。
【解决方案2】:

在 C 语言中,所有指针类型的表示形式都可能不同。所以,是的,int *void * 不同。一个能够说明这种差异的真实平台可能很难(或不可能)找到,但在概念层面上,差异仍然存在。

换句话说,一般情况下,不同的指针类型有不同的表示。 int * 不同于 void *double *。就 C 语言而言,您的平台对 void *int * 使用相同的表示形式只不过是巧合。

该语言声明某些指针类型需要具有相同的表示形式,其中包括 void *char *,指向不同结构类型的指针,或者说,int *const int *。但这些只是一般规则的例外。

【讨论】:

  • 当然,所有具有相同表示的指针并不是巧合,而是故意的(还记得远大的指针吗?呸),但仍然 - 很好的答案。一旦允许,我就会接受。
  • 如果现实生活中的平台是找不到的,那怎么可能是巧合呢,而是每个平台上都是一样的,你能怀疑清楚吗?
  • @Suraj Jain:首先,“今天不可能找到”并不一定意味着“明天不可能找到”。如果语言作者继续保持这种区别,那肯定是有原因的。
  • 其次,在现实生活中的平台中,有效表示并不完全相同。底层存储的大小和种类可能相同,但表示很容易不同。例如,严格执行数据对齐的平台(如 Sun Sparc)将要求将 double * 指针的最低位设置为零,而 char * 指针没有这样的要求。即使物理上为这些指针分配了相同的存储空间,尝试将char * 指针用作double * 指针也很容易导致陷阱。
【解决方案3】:

其他人已经充分解决了将int * 传递给具有固定数量参数的原型函数的情况,该函数需要不同的指针类型。

printf 不是这样的功能。它是一个可变参数函数,因此 默认参数提升 用于其匿名参数(即格式字符串之后的所有内容),并且如果每个参数的提升类型不完全匹配格式效应器预期的类型,行为未定义。特别是,即使int *void * 具有相同的表示,

int a;
printf("%p\n", &a);

有未定义的行为。

这是因为调用框架的布局可能取决于每个参数的具体具体类型。为指针和非指针类型指定不同参数区域的 ABI 在现实生活中已经出现(例如,摩托罗拉 68000 希望您尽可能将指针保留在地址寄存器中,将非指针保留在数据寄存器中)。我不知道有任何现实世界的 ABI 可以分隔不同的 pointer 类型,但这是允许的,听到这样的 ABI 并不会让我感到惊讶。

【讨论】:

  • 我想为常用的首字母缩略词ABI添加一个引用
  • 这个解释一直让我不满意——为什么标准不能在调用可变参数函数时强制对任何指针类型隐式提升到 void *,就像它对 float 所做的那样—— > double 和整数类型?在几乎任何平台上,它都是无操作的,我希望它即使在上面引用的 Cray 上也相当便宜(几乎肯定比 floatdouble 的转换便宜)。
  • @MatteoItalia 是的,我不知道他们为什么不这样做,但即使在 C11 中也不存在(据我快速浏览)。
  • @MatteoItalia 1980 年代后期 C 的标准化是关于编纂现有 实现。鉴于现有的内存模型that had variable pointer sizes,可能没有共同的基础来以不会破坏现有代码库的方式标准化可变参数函数中的隐式指针提升。仅仅标准化 void 类型本身可能是委员会所能做到的——K&R C 没有 void 类型。
  • @AndrewHenle:另一方面,该标准的作者在已发布的基本原理中明确承认,实现可以并且在许多情况下应该在实现质量的基础上扩展通过指定他们将如何处理标准未强加要求的操作,以适合目标平台和预期用途的方式使用语言。如果一个构造在 95% 的实现中表现良好,但在 5% 的实现中出现不可预测的故障,则使用该构造的代码将是“不可移植的”,但这并不意味着“错误”。
【解决方案4】:

c11:7.21.6 格式化输入/输出函数(p8):

p 参数应该是一个指向void的指针。指针的值为 在实现定义中转换为打印字符序列 方式。

【讨论】:

    【解决方案5】:

    实际上,除了古代大型机/小型机,不同的指针类型极不可能有不同的大小。但是它们有不同的类型,并且根据printf 的规范,使用格式说明符的错误类型参数调用它会导致未定义的行为。这意味着不要这样做。

    【讨论】:

    • 古代大型机。这或许可以解释。
    • 即使在现代微控制器中,由于哈佛架构中的数据和指令地址分开,函数指针和数据指针仍然可能具有不同的大小
    • @LưuVĩnhPhúc:函数指针与 OP 的问题完全不同。它们甚至不一定具有与数据指针之间的转换。 OTOH 所有数据指针都有与void * 之间的转换,并且通常以完全相同的方式表示。
    • 但在打印函数指针时不需要转换为 void * 吗?
    • @LưuVĩnhPhúc:没有可移植的方法来打印函数指针,尽管实现定义的转换为 void * 通常适用于您可能关心的系统。
    【解决方案6】:

    使用 printf 打印指针值时,调用者必须将指针强制转换为 void *

    即使转换为 void * 也不足以满足所有指针的需求。

    C 有两种指针:指向对象的指针和指向函数的指针。

    任何对象指针都可以毫无问题地转换成void*

    printf("My pointer is: %p", (void *)my_ptr); // OK when my_ptr points to an object
    

    函数指针void *的转换没有定义。

    考虑 2021 年的系统,其中 void * 为 64 位,函数指针为 128 位。


    C 确实指定了(我的重点)

    任何指针类型都可以转换为整数类型。除非前面指定,结果是实现定义的。如果结果不能以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。 C17dr § 6.3.2.3 6

    打印函数指针可以尝试:

    printf("My function pointer is: %ju", (uintmax_t) my_function_ptr); // Selectively OK
    

    C 缺乏真正通用的指针,也缺乏打印函数指针的简洁方式。

    【讨论】:

      【解决方案7】:

      解决问题:

      这两个调用何时会生成不同的堆栈帧?

      编译器可能会注意到行为未定义,并发出异常、非法指令等。编译器不需要尝试生成堆栈帧、函数调用或其他任何东西。

      See here 是编译器在另一种 UB 情况下执行此操作的示例。它不是生成带有空参数的遵从指令,而是生成ud2 非法指令。

      根据语言标准未定义行为时,对编译器的行为没有要求。

      【讨论】:

        猜你喜欢
        • 2019-07-03
        • 2023-04-11
        • 2015-12-03
        • 1970-01-01
        • 2018-01-21
        • 1970-01-01
        • 2020-10-24
        • 2018-12-16
        相关资源
        最近更新 更多