这是一个通用函数指针,它是危险的吗?

时间:2013-12-28 00:45:11

标签: c pointers

学习并弄乱函数指针,我注意到了一种初始化void函数指针并转换它们的方法。然而,虽然我没有收到任何警告或错误,无论是使用GCC还是VS的编译器,我都想知道这样做是危险的还是坏的做法,因为我没有看到这种方法经常在互联网。此外,我们称这个泛型函数指针吗?

#include <stdio.h>
#include <stdint.h>
#include <conio.h>

#define PAUSE (_getch())

uint16_t add(const uint16_t x, const uint16_t y) {
    return x + y;
}

char chr(uint8_t test) {
    return (char)test;
}

int main(void) {

    void(*test)() = (void*)add;

    const uint16_t x = 1, y = 1;
    uint16_t value = ((uint16_t(*)())test)(x, y);

    test = (void*)chr;

    printf("%d\n", add(x, y));                    // 2
    printf("%d\n", value);                        // 2
    printf("%c\n", ((char(*)())test)(100));       // d

    PAUSE;
    return 0;
}

4 个答案:

答案 0 :(得分:8)

  

这是一个通用函数指针

不,如果我不是非常错误,那么在C中就没有“通用函数指针”这样的东西。

  

并且危险吗?

是的,确实如此。这很邪恶。


您需要了解一些事项。首先,除非您正在运行符合POSIX的系统,否则

void(*test)() = (void*)add;

错误。 void *是指针指向 对象 类型,因此,它与函数指针。 (至少在标准C中没有 - 正如我所提到的,POSIX也要求它与函数指针兼容。)

第二件事是void (*fp)()void (*fp)(void)不同。前一个声明允许fp获取任意类型的任意数量的参数,当编译器看到对函数(指针)的第一次调用时,将推断出参数的数量及其类型。

另一个重要的方面是保证函数指针可以相互转换(AFAIK表明它们具有相同的表示和对齐要求)。这意味着任何函数指针都可以分配给任何函数的(地址)(在适当的强制转换之后),只要你不通过指向不兼容类型的指针调用函数。当且仅当您在调用指针之前将指针强制转换回原始类型时,行为才是明确定义的。

所以,如果你想要一个“通用”函数指针,你可以写一些类似

的东西
typedef void (*fn_ptr)(void);

然后您可以将任何指向函数的指针分配给类型为fn_ptr的对象。您需要注意的是,在调用函数时,再次转换为正确的类型,如:

int add(int a, int b);

fn_ptr fp = (fn_ptr)add; // legal
fp(); // WRONG!
int x = ((int (*)(int, int))fp)(1, 2); // good

答案 1 :(得分:5)

这里有两个严重的问题:

  1. 从函数指针到对象指针(例如void *)的强制转换会触发未定义的行为:原则上,它可能会使系统崩溃(尽管在实践中有许多系统可以正常工作)。而不是void *,最好为此目的使用函数指针类型。
  2. 你欺骗编译器在不知不觉中将int传递给期望uint8_t的函数。这也是未定义的行为,而且非常危险。由于编译器不知道它正在这样做,它甚至不能采取最基本的必要步骤来避免粉碎堆栈 - 你真的在这里赌博。类似地,这有点微妙,但你欺骗编译器将两个int - s传递给一个期望两个uint16_t - s的函数。
  3. 还有两个较小的问题:

    1. 函数指针类型的表示法 - 例如,在演员表中 - 令人困惑。我认为最好使用typedef:typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar)
    2. 调用具有与实际函数不同的签名的函数指针是未定义的行为。您可以通过仔细编码来避免这种情况 - 比您当前的代码更加谨慎 - 但这很棘手且风险很大。

答案 2 :(得分:2)

你可能会破坏调用堆栈,具体取决于调用约定,特别是谁在进行清理:http://en.wikipedia.org/wiki/X86_calling_conventions使用被调用者清理时,编译器无法知道你在堆栈上传递了多少变量在清理时,所以传递错误数量的参数或错误大小的参数将最终破坏调用堆栈。

在x64上,每个人都使用调用者清理,因此在这方面你是安全的。然而,参数值通常是一团糟。在您的示例中,在x64上,它们将是当时相应寄存器中的任何内容。

答案 3 :(得分:2)

C11§6.3.2.3(8)说:

  

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应该等于原始指针。如果转换的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

并且§6.7.6.3(15)说关于兼容的函数类型:

  

[...]如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分且包含空标识符列表,则参数列表不应具有省略号终止符每个参数的类型应与应用默认参数促销产生的类型兼容。 [...]

所以,如果你有addchr来获取int个参数(一个int的宽度至少为16位),那就没问题了(如果你没有指向void *)的函数指针,但实际上它是UB。