如何格式化函数指针?

54

在 ANSI C 中有没有打印函数指针的方法?当然这意味着你需要将函数指针转换为 void 指针,但似乎这是不可能的?

#include <stdio.h>

int main() {
    int (*funcptr)() = main;

    printf("%p\n", (void* )funcptr);
    printf("%p\n", (void* )main);

    return 0;
}

$ gcc -ansi -pedantic -Wall test.c -o test
test.c: In function 'main':
test.c:6: 警告:ISO C不允许将函数指针转换为对象指针类型
test.c:7: 警告:ISO C不允许将函数指针转换为对象指针类型
$ ./test
0x400518
0x400518

虽然“工作”,但是非标准的...


好的,我本来要接受一个可行的答案,但它被删除了(尽管它毫无意义)。 - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
我已经很久没有使用C语言了,但它变得越来越玄学了,C语言现在怎么了?Joseph Quinsey观察到当使用这个标志“-O2 -Wstrict-aliasing”时,dreamlax的建议也会有一个警告。 - Michael Buen
哎呀,我真希望我能负担得起那个规格... - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
@Longpoke:使用这个PDF,http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf。它是带有修正的C99规范,所以你必须要小心一点,但是C89和C99之间微妙的不同很少见(代码编译时表现不同)。通常它们是截然不同的,所以如果你尝试使用C99中没有的东西,GCC与`-pedantic`将无法编译它。 - Steve Jessop
哇,好像有人刚刚给这个点了个赞-1... - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
显示剩余7条评论
6个回答

52

唯一合法的方法是使用字符类型访问指针组成的字节。像这样:

#include <stdio.h>

int main() {
    int (*funcptr)() = main;
    unsigned char *p = (unsigned char *)&funcptr;
    size_t i;

    for (i = 0; i < sizeof funcptr; i++)
    {
        printf("%02x ", p[i]);
    }
    putchar('\n');

    return 0;
}

void *类型的左值检查函数指针的字节,或任何非字符类型,都是未定义的行为。

组成函数指针的那些字节实际上代表着什么意思,取决于具体的实现。例如,它们可能只是一个函数表中的索引;或者甚至可以是函数名的前N个字符,在调用函数指针时在符号表中查找。函数指针需要支持的唯一操作是通过它调用函数,并与另一个函数指针或NULL进行比较以获取严格的相等性/不相等性,因此在实现方式方面有很大的自由度。


10
@Chris说:uintptr_t与函数指针没有关系,而且是可选的。因此从编写完全可移植的角度来看,它并不更好。 - Steve Jessop
7
遗憾的是,在符合 C 语言标准的情况下,如果没有为打印函数指针添加 printf 格式说明符,那么这是您所能做的最好的事情。字节序是一个红鲱鱼 - "函数指针" 没有保证一定是某种数字类型(例如,它可能是函数名称的前64个字符,在使用函数指针进行调用时会在符号表中查找)。这就是为什么函数指针不能转换为其他指针类型,并且也不能从其他指针类型转换回来 - 这样实现就可以广泛地灵活处理它们。 - caf
1
@BlueMoon:sizeof不能应用于函数设计符,但是funcptr不是函数设计符,它是指向函数的指针,对其应用sizeof是完全合法的。 - caf
1
将函数指针强制转换为void *或任何非字符类型是未定义的行为。为什么将其转换为char *是有效的,而转换为void *则不是?两个指针具有相同的表示和对齐要求。此外,根据附录A.6.2,当“将指向函数的指针转换为对象的指针或将对象的指针转换为函数的指针(3.3.4)”时,会发生UB。您确定在答案中所做的事情完全有效吗? - alecov
1
@Alek:通过“将函数指针解释为void *”我是指读取函数指针的字节,就好像它是一个void *对象一样。char *同样无效-那不是字符类型。将其解释为字符类型将会把函数指针的字节读取为charunsigned char - caf
显示剩余12条评论

5
这里使用联合体可以避免警告/错误,但结果仍然(很可能)是未定义的行为:
#include <stdio.h>

int
main (void)
{
  union
  {
    int (*funcptr) (void);
    void *objptr;
  } u;
  u.funcptr = main;

  printf ("%p\n", u.objptr);

  return 0;
}

你可以比较两个函数指针(例如:printf ("%i\n", (main == funcptr));)使用if语句测试它们是否相等(我知道这完全违背了目的,可能没有意义),但是实际上输出函数指针的地址的操作取决于您目标平台的C库和编译器的供应商。


1
使用联合体进行类型转换不是未定义的行为。这是实现定义的行为,在大多数编译器上它会按照你的期望执行。 - Rufflewind

2

使用gcc 4.3.4,使用开关 -O2 -Wstrict-aliasing,dreamlax的答案会产生:

warning: dereferencing type-punned pointer will break strict-aliasing rules

添加:我认为对于caf关于字节序和大小的答案的反对是有道理的,他的解决方案没有解决这个问题。Dustin建议使用union转换可能是合法的(尽管从我所读到的资料中似乎存在一些争议,但你的编译器比法律更重要)。但是他的代码可以通过一行代码进行简化(或者根据你的喜好进行混淆):

printf("%p\n", ((union {int (*from)(void); void *to;})funcptr).to);

这将消除gcc的strict-aliasing警告(但是否“正确”?)。

如果您正在使用-pedantic开关或使用SGI IRIX等操作系统,则聚合转换将无法“工作”,因此您需要使用:

printf("%p\n", ((union {int (*from)(void); void *to;} *)&funcptr)->to);

关于原始问题:它的起源在于使用-pedantic,我认为这有点学究气。
进一步编辑:请注意,在最后一个示例中不能使用main,如下所示:
printf("%p\n", ((union {int (*from)(void); void *to;})   main).to);  // ok
printf("%p\n", ((union {int (*from)(void); void *to;} *)&main)->to); // wrong!

因为当然&main会衰变成main


2
有用的信息,但这并没有真正回答他的问题,它应该作为我的回答的评论而不是一个完全独立的回答。 - dreamlax
@dreamlax:很抱歉要单独回答,但我只有31个stackoverflow积分,少于需要评论的50个积分。但我会尽快将此答案转换为评论。 - Joseph Quinsey
@caf:抱歉,我看到你已经在你的答案中提到了关于dreamlax答案的这个观点。 - Joseph Quinsey
main 不是会“退化”(更好的说法是“转换”)成 &main 吗? - Lover of Structure

2

试试这个:

#include <stdio.h>
#include <inttypes.h>


int main() {
    int (*funcptr)() = main;
    unsigned char *p = (unsigned char *)&funcptr;
    int i;

    /* sample output: 00000000004005e0 */
    printf("%016"PRIxPTR"\n", (uintptr_t)main);
    /* sample output: 00000000004005e0 */
    printf("%016"PRIxPTR"\n", (uintptr_t)funcptr);

    /* reflects the fact that this program is running on little-endian machine
    sample output: e0 05 40 00 00 00 00 00 */
    for (i = 0; i < sizeof funcptr; i++)
    {
        printf("%02x ", p[i]);
    }
    putchar('\n');

    return 0;
}

使用了这些标志:

gcc -ansi -pedantic -Wall -O2 -Wstrict-aliasing c.c

使用这些标志没有发出警告


在一台机器上,存储指向函数的指针所需的字节数比存储指向UINT的指针所需的字节数更多(除了pre-386的分段内存,编译器有三种指针类型:near、far、huge(huge只是编译器魔法而非真正的机器架构))。我记得在汇编语言中,无论指针指向什么(UINT、char、short、function等),都没有变量大小指针。 - Michael Buen
2
哈佛架构可能具有不同大小的指令和数据指针,这就是为什么C将它们视为不兼容的原因。我无法立即命名一个具有不同大小的处理器(在C++中更容易,其中成员指针通常具有不同且经常可变的大小),但我也不能从自己的无知中得出结论,即没有任何处理器具有不同大小的指针。问题是关于“在ANSI C中”,而不是“在x86上的ANSI C”。 - Steve Jessop
我不知道C语言在哈佛架构上的历史或成功情况(而且不同的“哈佛”架构看起来也不同,所以可能在某些架构上可行但并非普遍适用)。我认为你的循环在哈佛架构上并没有任何问题:你将funcptr视为char数据而不是main的代码,而funcptr是一个自动变量,因此即使在哈佛架构上也是数据。我只是争议你的观察“我记得在汇编语言中,没有变量大小指针”是否足以得出任何关于合法、可移植的C语言的结论。 - Steve Jessop
顺便提一下,维基百科脚注中链接的那篇文章谈到了在哈佛架构上实现C语言时存储程序空间数据的困难。据我所知,这并不意味着你不能实现C语言,因为代码空间中的数据并非实现C语言所必需的。只是如果你的只读静态数据、字符串字面量等必须占用宝贵的RAM才能从C语言中作为数据寻址,那就不太好了。因此,C语言在分离寻址方面可能效率低下。C语言最初确实是“设计用于”冯·诺伊曼架构,但据我所知,它并不保证只能在该架构上运行。 - Steve Jessop
(uintptr_t)main 是未定义行为(或者换句话说,依赖于编译器扩展,而这些扩展并未在 C 标准中提到)。 - M.M
显示剩余5条评论

1
将函数指针转换为整数,然后再将其转换为指针以使用“%p”。
#include <stdio.h>

int main() {
    int (*funcptr)() = main;

    printf("%p\n", (void *)(size_t) funcptr);
    printf("%p\n", (void *)(size_t) main);

    return 0;
}

请注意,在某些平台上(例如在“中等”或“紧凑”内存模型下的16位DOS系统中),指向数据和指向函数的指针大小不同。

1
是的,“16位DOS”运行在分段内存架构上。例如,80286将具有4字节的void *和2字节的size_t - Judge Maygarden

-1

我不确定这是否被认为是良好的实践,但它可以在没有循环、联合、转换为非指针类型或除了 string.h 之外的额外依赖的情况下实现效果。

int (*funcptr)() = main;
void* p = NULL;
memcpy(&p, (void**) &funcptr, sizeof(funcptr));
printf("%p", p);

在GCC 7.1.1上使用gcc -ansi -pedantic -Wall -Wstrict-aliasing没有任何警告。


也许应该加上“-Wpedantic”,而不是“-pedantic”,因为我遇到了一个错误。 错误:ISO C 不允许将函数指针转换为对象指针类型 [-Werror=pedantic] - KANJICODER

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