简体   繁体   English

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

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

Learning and messing up with function pointers, I noticed a way to initialize void function pointers and cast them. 学习并弄乱了函数指针,我注意到了一种初始化void函数指针并转换它们的方法。 Yet, although I don't receive any warning or error, either with GCC or VS's compiler, I wanted to know whether it was dangerous or a bad practice to do this as I don't see this way of initializing function pointers often on the Internet. 然而,虽然我没有收到任何警告或错误,无论是使用GCC还是VS的编译器,我都想知道这样做是危险的还是不好的做法,因为我没有看到这种方法经常初始化函数指针互联网。 Moreover, do we call this generic function pointer? 而且,我们称之为通用函数指针吗?

#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;
}

Is this a generic function pointer 这是一个通用的函数指针

No, if I'm not terribly mistaken, there's no such thing as a "generic function pointer" in C. 不,如果我不是非常错误,那么在C中就没有“泛型函数指针”这样的东西。

and is it dangerous? 这是危险的吗?

Yes, it is. 是的。 It is evil. 这是邪恶的。


There are a couple of things you need to know. 您需要了解一些事项。 First, unless you are running a system that conforms to POSIX, 首先,除非您运行的系统符合POSIX,

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

is wrong. 错的。 void * is a pointer-to- object type, and as such, it is not compatible with function pointers. void *是指向对象的指针类型,因此它与函数指针不兼容。 (At least not in standard C -- as I mentioned, POSIX requires it to be compatible with function pointers too.) (至少在标准C中没有 - 正如我所提到的,POSIX要求它也与函数指针兼容。)

The second thing is that void (*fp)() and void (*fp)(void) are different. 第二件事是void (*fp)()void (*fp)(void)是不同的。 The former declaration permits fp to take any number of parameters of any type, and the number of arguments and their types will be inferred when the compiler sees the first call to the function (pointer). 前一个声明允许fp采用任意类型的任意数量的参数,并且当编译器看到对函数(指针)的第一次调用时,将推断出参数的数量及其类型。

Another important aspect is that function pointers are guaranteed to be convertible across each other (AFAIK this manifests in them having the same representation and alignment requirements). 另一个重要的方面是保证函数指针可以相互转换(AFAIK这表明它们具有相同的表示和对齐要求)。 This means that any function pointer can be assigned to (the address of) any function (after an appropriate cast), so long as you do not call a function through a pointer to an incompatible type. 这意味着任何函数指针都可以分配给任何函数的(地址)(在适当的强制转换之后),只要你不通过指向不兼容类型的指针调用函数。 The behavior is well-defined if and only if you cast the pointer back to the original type before calling it. 当且仅当您在调用指针之前将指针强制转换回原始类型时,行为才是明确定义的。

So, if you want a "generic" function pointer, you can just write something like 所以,如果你想要一个“通用”函数指针,你可以写一些像

typedef void (*fn_ptr)(void);

and then you could assign any pointer to function to an object of type fn_ptr . 然后你可以将任何指向函数的指针fn_ptr类型的对象。 What you have to pay attention to is, again, the conversion to the right type when invoking the function, as in: 您需要注意的是,在调用函数时,再次转换为正确的类型,如:

int add(int a, int b);

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

There are two serious problems here: 这里有两个严重的问题:

  1. A cast from a function pointer to an object pointer (such as void * ) triggers undefined behavior: in principle, it could crash your system (though in practice there are many systems where it will work fine). 从函数指针到对象指针(例如void * )的转换会触发未定义的行为:原则上,它可能会使系统崩溃(尽管在实践中有许多系统可以正常工作)。 Instead of void * , it's better to use a function-pointer type for this purpose. 而不是void * ,最好为此目的使用函数指针类型。
  2. You're tricking the compiler into unknowingly passing an int to a function expecting a uint8_t . 你欺骗了编译器在不知不觉中传递一个int以期待的功能uint8_t That's also undefined behavior, and it's very dangerous. 这也是未定义的行为,而且非常危险。 Since the compiler doesn't know that it's doing this, it can't even take the most basic necessary steps to avoid smashing the stack — you're really gambling here. 由于编译器不知道它正在这样做,它甚至不能采取最基本的必要步骤来避免粉碎堆栈 - 你真的在这里赌博。 Similarly, this is a bit more subtle, but you're also tricking the compiler into passing two int -s into a function expecting two uint16_t -s. 类似地,这有点微妙,但你欺骗编译器将两个int -s传递给一个期望两个uint16_t -s的函数。

And two lesser problems: 还有两个较小的问题:

  1. The notation for function pointer types on their own — eg, in a cast — is confusing. 函数指针类型的表示法(例如,在演员表中)是令人困惑的。 I think it's better to use a typedef: typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar) 我认为最好使用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) . typedef void (*any_func_ptr)(); any_func_ptr foo = (any_func_ptr)(bar)
  2. It's undefined behavior to call a function pointer with a different signature than the actual function has. 调用具有与实际函数不同的签名的函数指针是未定义的行为。 You can avoid that with careful coding — more careful than your current code — but it's tricky and risky. 您可以通过仔细编码来避免这种情况 - 比您当前的代码更加谨慎 - 但这很棘手且风险很大。

You may corrupt the call stack with this, depending on the calling convention, specifically who's doing the cleanup: http://en.wikipedia.org/wiki/X86_calling_conventions With the callee cleanup, the compiler has no way of knowing how many variables you have passed on the stack at the point of cleanup, so passing the wrong number of parameters or parameters of the wrong size will end corrupting the call stack. 您可能会使用此方法来破坏调用堆栈,具体取决于调用约定,特别是谁正在进行清理: http//en.wikipedia.org/wiki/X86_calling_conventions对于被调用者清理,编译器无法知道您有多少变量已经在清理点上传递了堆栈,因此传递错误数量的参数或错误大小的参数将终止破坏调用堆栈。

On x64, everyone uses the caller cleanup, so you're safe in this regard. 在x64上,每个人都使用调用者清理,因此在这方面你是安全的。 The parameter values, however, will in general be a mess. 然而,参数值通常是一团糟。 In your example, on x64, they will be whatever was in the corresponding registers at the time. 在您的示例中,在x64上,它们将是当时相应寄存器中的任何内容。

C11 §6.3.2.3 (8) says: C11§6.3.2.3(8)说:

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; 指向一种类型的函数的指针可以被转换为指向另一种类型的函数的指针并且再次返回; the result shall compare equal to the original pointer. 结果应该等于原始指针。 If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined. 如果转换的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

And §6.7.6.3 (15) says about compatible types of functions: §6.7.6.3(15)说关于兼容的函数类型:

[…] If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. [...]如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分并且包含空标识符列表,则参数列表不应具有省略号终止符和类型每个参数应与应用默认参数促销产生的类型兼容。 […] [...]

So, if you had add and chr to take int arguments (an int has at least a width of 16 bit) that would be OK (if you didn't cast the function pointer to void * ), but, as it is, it is UB. 所以,如果你有addchr来获取int参数(一个int的宽度至少为16位),那就没问题(如果你没有将函数指针强制转换为void * ),但实际上,它是是UB。

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

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