打印指针或地址的正确格式说明符是什么?

262

我应该使用哪种格式指定符来打印变量的地址?我在以下三个中感到困惑。

%u - 无符号整数

%x - 十六进制值

%p - 无类型指针

哪种格式是最优打印地址的方式?

6个回答

313
假设您不介意不同平台之间格式的差异和变化,则最简单的答案是标准的%p表示法。
C99标准(ISO/IEC 9899:1999)在§7.19.6.1 ¶8中说:
“p”参数应为指向void的指针。指针的值以一种实现定义的方式转换为打印字符序列。
(在C11 - ISO/IEC 9899:2011中,这些信息在§7.21.6.1 ¶8中。)
在某些平台上,这将包括前导的0x,而在其他平台上则不会,并且字母可以是小写或大写,而C标准甚至没有定义它应该是十六进制输出,尽管我不知道有任何不是这样的实现。
在是否应该使用(void *)转换指针方面存在一些争议。明确表达通常是好的(所以我这样做),标准说“参数必须是指向void的指针”。在大多数机器上,省略显式转换是可以的。但是,在位表示不同于相同内存位置的“其他任何指针”地址的给定内存位置的char *地址的机器上会有影响。这将是一个字地址机器,而不是字节地址机器。这样的机器现在并不常见(可能也不可用),但我在大学毕业后工作的第一台计算机就是这样的机器(ICL Perq)。
如果您对%p的实现定义行为不满意,则可以使用C99 <inttypes.h>uintptr_t
printf("0x%" PRIXPTR "\n", (uintptr_t)your_pointer);

这使你可以微调表示方式以适应自己的需要。我选择将十六进制数字大写,这样数字就是统一的高度,并且在0xA1B2CDEF开头的特征下降出现,而不像0xa1b2cdef那样在数字上下起伏。当GCC能够在编译时读取格式字符串时,(uintptr_t)转换无疑是推荐的。我认为请求转换是正确的,尽管我相信有些人会忽略警告并大多数情况下逃脱惩罚。

Kerrek在评论中提问:

我有些困惑标准提升和可变参数。所有指针是否都会被标准提升为void *?否则,如果int *是2个字节,而void *是4个字节,那么从参数中读取四个字节显然是错误的,不是吗?

我曾经误以为C标准规定所有对象指针必须具有相同大小,所以void *和int *不能有不同的大小。然而,我认为C99标准的相关部分并没有如此强调(尽管我不知道在哪个实现中我所说的是真的或假的):

§6.2.5 类型
¶26 指向 void 的指针应该具有与指向字符类型的指针相同的表示和对齐要求。39) 同样,指向兼容类型的限定或非限定版本的指针应该具有相同的表示和对齐要求。所有结构类型的指针应该具有彼此相同的表示和对齐要求。所有联合类型的指针应该具有彼此相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。 39) 相同的表示和对齐要求意味着可以作为函数参数进行交换、从函数返回值并且作为联合体成员。
(C11 在 §6.2.5 节、¶28 和脚注 48 中完全相同。)
因此,所有指向结构体的指针必须具有相同的大小,并且必须共享相同的对齐要求,即使指针所指向的结构体可能具有不同的对齐要求。联合体也是如此。字符指针和void指针必须具有相同的大小和对齐要求。指向int(指unsigned int和signed int)的变化的指针必须具有彼此相同的大小和对齐要求;其他类型也是如此。但是C标准并没有正式说 sizeof(int *) == sizeof(void *) 。好吧,SO很适合让您检查自己的假设。
C标准明确规定函数指针不需要与对象指针具有相同的大小。这是为了不破坏DOS类系统上的不同内存模型。在那里,您可以拥有16位数据指针但32位函数指针,反之亦然。这就是为什么C标准不强制要求函数指针可以转换为对象指针,反之亦然。
幸运的是(对于针对POSIX的程序员),POSIX填补了这一空白,并规定函数指针和数据指针具有相同的大小:
§2.12.3 指针类型 所有函数指针类型的表示形式应与指向void的指针类型相同。将函数指针转换为void *不会改变其表示形式。由此转换得到的void *值可以使用显式转换回原始函数指针类型,而不会丢失信息。
注意:ISO C标准不要求这样做,但在POSIX兼容性方面是必需的。
因此,在通过可变参数函数(如printf())传递指针时,明确地将其转换为void *是非常可取的,以实现最大的代码可靠性。在POSIX系统上,将函数指针转换为void指针以进行打印是安全的。在其他系统上,这样做不一定安全,也不一定安全地传递指针而不使用转换。

3
我对标准Promotions和变参参数有些困惑。所有指针都会被标准提升为 void* 吗?否则,如果 int* 是两个字节,而 void* 是4个字节,那么从该参数读取四个字节显然是一个错误,对吗? - Kerrek SB
1
请注意,POSIX(POSIX 2013)的更新已删除了2.12.3节,并将大部分要求转移到dlsym()函数中。有一天我会写出这个变化......但是“有一天”不是“今天”。 - Jonathan Leffler
这个答案也适用于函数指针吗?它们可以转换为 void * 吗?嗯,我看到你在这里的评论(https://dev59.com/nmox5IYBdhLWcg3wqmBt#H4LinYgBc1ULPQZFsr8O)。由于只需要单向转换(函数指针到 void *),那么它就可以工作了? - chux - Reinstate Monica
1
@chux: 严格来说,答案是“不可以”,但在实践中答案是“可以”。 C标准不能保证函数指针可以被转换为void *并且再次转回时不会丢失信息。从实用角度来看,在很少的机器上,函数指针的大小与对象指针的大小不同。我认为,如果在这样的机器上打印函数指针,标准没有提供方法来解决这种转换问题。 - Jonathan Leffler
“而不丢失信息”与打印无关。这有帮助吗? - chux - Reinstate Monica
显示剩余6条评论

76

p是打印指针的转换说明符。使用它。

int a = 42;

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

记住省略类型转换是未定义行为,并且使用 p 转换说明符打印是以一种实现定义的方式进行的。


3
请原谅,为什么省略强制类型转换会被视为"未定义行为"?如果你只需要地址而不是值,那么这个问题是否与变量的地址有关呢? - valdo
11
@valdo因为C语言规范如此规定(C99,7.19.6.1p8):“参数必须是指向void的指针。” - ouah
14
并非所有指针的大小/表示方式都必须相同。 - caf

38
使用 %p 表示指针类型,不要使用其他格式。标准并没有保证您可以将指针视为任何特定类型的整数,因此使用整数格式实际上会得到未定义的行为(例如,%u 需要一个无符号整数,但是如果 void* 具有与 unsigned int 不同的大小或对齐要求,该怎么办?)
*) [请参见 Jonathan 的精彩回答!] 作为替代方案,您可以使用 C99 中添加的 <inttypes.h> 中的指针特定宏。
在 C 中,所有对象指针都可以隐式转换为 void*,但是为了将指针作为可变参数传递,必须显式地进行转换(因为任意对象指针只能 可转换,而不能 等同于 void 指针)。
printf("x lives at %p.\n", (void*)&x);

3
所有的对象指针都可以转换为void *(虽然对于printf()函数,由于它是一个可变参数函数,你技术上需要显式转换)。函数指针不一定可以转换为void * - caf
2
标准C并不要求函数指针可以在转换为void *后再转回函数指针而不会丢失信息;幸运的是,POSIX明确要求这一点(并指出这不是标准C的一部分)。因此,在实践中,你可以这样做(将void (*function)(void)转换为void *,然后再转回void (*function)(void)),但严格来说,这并不是C标准所规定的。 - Jonathan Leffler
2
Jonathan和R.:这一切都非常有趣,但我很确定我们并不是在尝试打印函数指针,所以也许这不是讨论这个问题的正确场所。我更想看到一些支持来证明我坚持不使用“%u”的观点! - Kerrek SB
3
在所有机器上,%u%lu都是错误的。 printf的规范非常清楚,当传递的类型与格式说明符所需的类型不匹配时,行为是未定义的。类型的大小是否匹配(这取决于机器)并不重要;必须匹配类型,并且它们永远不会匹配。 - R.. GitHub STOP HELPING ICE
1
@Destructor:%u 需要一个 unsigned int,而你正在传递一个 int *,因此这是未定义行为。 - Kerrek SB
显示剩余5条评论

9
作为其他(非常好的)答案的替代方案,您可以将指针转换为 uintptr_t intptr_t (来自 stdint.h / inttypes.h ),并使用相应的整数转换说明符。这样可以更灵活地格式化指针,但严格来说,实现不需要提供这些typedef。

考虑以下关于编程的内容:考虑 #include <stdio.h> int main(void) { int p=9; int* m=&s; printf("%u",m); } 使用 %u 格式说明符打印变量地址是否是未定义行为? 变量地址在大多数情况下是正数,所以我可以使用 %u 而不是 %p 吗? - Destructor
1
@Destructor:不,"%u"是用于“unsigned int”类型的格式,不能与指向“printf”的指针参数一起使用。 - R.. GitHub STOP HELPING ICE

6

你可以使用%x或者%X或者%p,它们都是正确的。

  • 如果你使用%x,地址将以小写字母形式给出,例如:a3bfbc4
  • 如果你使用%X,地址将以大写字母形式给出,例如:A3BFBC4

这两种都是正确的。

如果你使用%x或者%X,它会考虑六个位置用于表示地址,而如果你使用%p,它会考虑八个位置来表示地址。例如:


2
欢迎来到SO。请花些时间查看其他答案,它们清楚地解释了一些您忽略的细节。 - AntoineL
1
特别是%X是完全错误的。它期望无符号整数,如果系统的指针大小与整数大小相同,则可能会“工作”,但在今天的桌面系统上不是这样。正确的方法是使用格式%p并将指针转换为(void*),因此您提供的任何解决方案都不正确。 - Weather Vane

0

标准是%p

如果在64位系统上%x只打印整个地址的部分,请改用%lx%llx


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接