繁体   English   中英

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

[英]Is this a generic function pointer and is it dangerous?

学习并弄乱了函数指针,我注意到了一种初始化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;
}

这是一个通用的函数指针

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

还有两个较小的问题:

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

您可能会使用此方法来破坏调用堆栈,具体取决于调用约定,特别是谁正在进行清理: http//en.wikipedia.org/wiki/X86_calling_conventions对于被调用者清理,编译器无法知道您有多少变量已经在清理点上传递了堆栈,因此传递错误数量的参数或错误大小的参数将终止破坏调用堆栈。

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

C11§6.3.2.3(8)说:

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM