【问题标题】:Why can't I cast a function pointer to (void *)?为什么我不能将函数指针强制转换为 (void *)?
【发布时间】:2016-08-07 08:07:54
【问题描述】:

我有一个函数,它接受一个字符串、一个字符串数组和一个指针数组,并在字符串数组中查找字符串,并从指针数组中返回相应的指针。由于我将它用于几个不同的事情,指针数组被声明为 (void *) 的数组,调用者应该知道实际存在什么样的指针(因此它作为返回值返回什么样的指针)。

但是,当我传入一个函数指针数组时,当我使用 -Wpedantic 编译时收到警告:

叮当声:

test.c:40:8: warning: assigning to 'voidfunc' (aka 'void (*)(void)') from 'void *' converts
      between void pointer and function pointer [-Wpedantic]

gcc:

test.c:40:8: warning: ISO C forbids assignment between function pointer and ‘void *’ [-Wpedantic]
   fptr = find_ptr("quux", name_list, (void **)ptr_list,

这是一个测试文件,尽管有警告,但它确实正确打印了“quux”:

#include <stdio.h>
#include <string.h>

void foo(void)
{
  puts("foo");
}

void bar(void)
{
  puts("bar");
}

void quux(void)
{
  puts("quux");
}

typedef void (* voidfunc)(void);

voidfunc ptr_list[] = {foo, bar, quux};

char *name_list[] = {"foo", "bar", "quux"};

void *find_ptr(char *name, char *names[], void *ptrs[], int length)
{
  int i;

  for (i = 0; i < length; i++) {
    if (strcmp(name, names[i]) == 0) {
      return ptrs[i];
    }
  }
  return NULL;
}

int main() {
  voidfunc fptr;

  fptr = find_ptr("quux", name_list, (void **)ptr_list,
                  sizeof(ptr_list) / sizeof(ptr_list[0]));
  fptr();

  return 0;
}

除了不使用-Wpedantic 编译或复制我的find_ptr 函数,一次用于函数指针,一次用于非函数指针之外,有什么方法可以修复警告?有没有更好的方法来实现我想要做的事情?

【问题讨论】:

  • IIRC 的问题是代码指针的大小不一定与数据指针的大小相同。在大多数架构中它们都是。我将把它留给其他人来查找对 C 标准的参考。您可以通过uintptr_t 和返回来获得您想要的结果。
  • 由于@abligh 提到的可能的大小不匹配,将函数指针转换为其他指针类型是未定义的行为有关更多详细信息,请参阅此问题:stackoverflow.com/questions/13696918/…
  • 是什么阻止您将void *ptrs[] 替换为voidfunc * ptrs
  • 你试过数据指针和函数指针的联合吗?
  • 由于您显然不想调用所选函数,而只是返回它,您想通过间接方式解决这个问题,可能传递一个索引数组,引用您所在数组的条目用于这种当前且无法解决的方法。

标签: c pointers function-pointers


【解决方案1】:

您无法修复警告。事实上,在我看来,这应该是一个硬错误,因为将函数指针转换为其他指针是非法的,因为今天有一些架构,这不仅违反了 C 标准,而且是一个实际的错误,会使代码不行。编译器允许它,因为许多架构都可以摆脱它,即使这些程序在其他一些架构上会严重崩溃。但这不仅仅是理论上的标准违规,它还会导致真正的错误。

例如,在 ia64 上,函数指针实际上是(或者至少在我上次查看时曾经是)两个值,这两个值都是跨共享库或程序和共享库进行函数调用所必需的。同样,将函数指针转换和调用函数指针以返回值的函数指向返回 void 的函数的指针,因为您知道无论如何都会忽略返回值,这在 ia64 上也是非法的,因为这可能导致陷阱值泄漏到寄存器中在许多指令之后导致一些不相关的代码崩溃。

不要强制转换函数指针。始终让它们匹配类型。这不仅仅是标准的迂腐,而是一个重要的最佳实践。

【讨论】:

  • 很好的答案,但我有一个问题:在void (*f)() 中存储指向(任何)函数的指针并在拨打电话时将其转换为正确的类型是否安全?
  • @HolyBlackCat 根据我对当今存在的架构的了解,是的。从标准的角度来看,没有。从未来兼容性的角度来看,我宁愿不这样做。从破产时你所在的公司的角度来看,去争取吧。了解您可以逃脱的一个好方法是查看大型项目。例如,如果当编译器对某些事情变得严格时 Linux 内核中断,您可以很确定您会听到它,并且至少会有标志来禁用它。硬件架构也是如此,如今很少有人会构建 Linux 无法运行的 CPU。
  • This answer 与您的相矛盾。特别是,它引用了表明函数指针可以转换为任何其他函数指针类型并再次转换回来的标准。
  • 好吧,我的意思是它与关于转换函数指针的部分相矛盾。如果你完全可以转换它们,那将是有问题的,但你可以使用void (*)() 作为函数指针的一种 void 指针。
  • @technosaurus 不正确-o。在调用它之前,您必须始终将其转换为正确的函数指针类型,无论它看起来“多么接近”。
【解决方案2】:

一种解决方案是添加一个间接级别。这对很多事情都有帮助。与其存储指向函数的指针,不如存储指向struct 的指针,存储指向函数的指针。

typedef struct
{
   void (*ptr)(void);
} Func;

Func vf = { voidfunc };

ptrlist[123] = &vf;

等等

【讨论】:

  • 你不需要struct;你可以只使用一个指向指针的指针。
  • @davmac 是的,但是您可以向结构中添加更多内容并构建一个穷人的闭包。
【解决方案3】:

这是在 C 标准中早已被破坏并且从未修复过的东西——没有可用于指向函数的指针和指向数据的指针的通用指针类型。

在 C89 标准之前,所有的 C 编译器都允许在不同类型的指针之间进行转换,char * 通常用作可能指向任何数据类型或任何函数的通用指针。 C89 添加了void *,但加入了一个子句,即只有对象指针可以转换为void *,而无需定义对象是什么。 POSIX 标准通过强制void * 和函数指针可以安全地来回转换来解决此问题。有很多代码将函数指针转换为void * 并期望它能够正常工作。结果,几乎所有的 C 编译器仍然允许它,并且仍然生成正确的代码,因为任何不这样做的编译器都会被拒绝为不可用。

严格来说,如果你想在 C 语言中拥有一个通用指针,你需要定义一个可以容纳 void *void (*)() 的联合,并使用函数指针的显式转换指向正确的函数指针在调用它之前输入。

【讨论】:

  • 这是在 C 标准中早已被破坏并且从未修复过的东西”有点不准确。这是硬件标准化限制,而不是语言标准限制。
  • 能否请您参考C11中禁止“函数指针和'void *'之间的初始化”的地方?
  • @pmor: 6.3.2.3 专门列出了所有合法的指针转换——函数指针和对象指针之间的转换没有列出,因此未定义。
【解决方案4】:

语言律师的原因是“因为 C 标准没有明确允许它”。 C11 6.3.2.3p1/p8

1. 指向 void 的指针可以转换为或从指向任何对象类型的指针转​​换。指向任何对象类型的指针都可以转换为 指向 void 并再次返回的指针;结果应比较等于 原始指针。

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

请注意,在 C 术语中,函数不是对象,因此没有什么可以让您将指向函数的指针转换为指向 void 的指针,因此行为是 未定义的

void * 的可转换性是一个常见的扩展。 C11 J.5 Common extensions 7:

J.5.7 函数指针转换

1. 指向对象或 void 的指针可以转换为指向函数的指针,从而允许将数据作为函数调用 (6.5.4)。

2. 指向函数的指针可以转换为指向对象的指针或 void,从而允许检查或修改函数(例如,通过调试器)(6.5.4 )。

这是例如 POSIX 所要求的 - POSIX 有一个函数 dlsym,它返回 void *,但实际上它返回指向函数的指针或指向对象的指针,具体取决于解析的符号的类型。


至于为什么会发生这种情况 - 如果实现可以达成一致,C 标准中没有任何内容是未定义或未指定的。但是,在某些平台上,假设 void 指针和函数指针具有相同的宽度确实会使事情变得困难。其中之一是 8086 16 位实模式。


然后用什么代替呢?您仍然可以将 任何函数指针 转换为另一个函数指针,因此您可以在任何地方使用通用函数指针 void (*)(void)。如果您同时需要void * 函数指针,则必须使用结构或联合或分配void * 来指向函数指针,或确保您的代码仅在J. 5.7 已实施;)

void (*)() 也被一些消息来源推荐,但现在它似乎在最新的 GCC 中触发了警告,因为它没有原型。

【讨论】:

  • 8086 16 位模式存在两种不同大小的指针的问题,但这两种大小同样适用于对象指针和函数指针。因此,对于使用 nearfar 指针限定符的编译器,near 对象指针的宽度与 near 函数指针的宽度相同,而 far 指针的宽度同样总是相同(尽管不同于 @ 987654335@)
  • @ChrisDodd Turbo C 例如有几个内存模型medium内存模型意味着所有对象指针都是@ 987654336@,代码指针(函数)为far,相反的模型compact表示所有对象指针为far,函数指针为near
【解决方案5】:

通过一些修改,您可以避免指针对话:

#include <stdio.h>
#include <string.h>

void foo(void)
{
    puts("foo");
}

void bar(void)
{
    puts("bar");
}

void quux(void)
{
    puts("quux");
}

typedef void (* voidfunc)(void);

voidfunc ptr_list[] = {foo, bar, quux};

char *name_list[] = {"foo", "bar", "quux"};

voidfunc find_ptr(char *name, char *names[], voidfunc ptrs[], int length)
{
    int i;

    for (i = 0; i < length; i++) {
        if (strcmp(name, names[i]) == 0) {
            return ptrs[i];
        }
    }
    return NULL;
}

int main() {
    voidfunc fptr;

    fptr = find_ptr("quux", name_list, ptr_list,
                    sizeof(ptr_list) / sizeof(ptr_list[0]));
    fptr();

    return 0;
}

【讨论】:

  • 我不想将指针数组参数声明为函数指针,因为我也将相同的函数用于其他事情。
【解决方案6】:

正如在其他答案中指出的那样,您不应该被允许将函数指针分配给对象指针,例如 void*。但是您可以安全地将函数指针分配给任何函数指针。在 C++ 中使用 reinterpret_cast

我举个例子:

typedef void(*pFun)(void);
double increase(double a){return a+1.0;}

pFun ptrToFunc = reinterpret_cast<void(*)(void)>(increase);

平原

pFun ptrToFunc = increase;

不能在多个编译器上编译。

【讨论】:

  • 为什么不使用更简单的 C 风格转换,特别是考虑到这是一个 C 问题? pFun ptrToFunc = (void(*)(void))increase;?
【解决方案7】:

我正在回答这个老问题,因为现有答案中似乎缺少一种可能的解决方案。

编译器禁止转换的原因是sizeof(void(*)(void)) 可以不同于sizeof(void*)。我们可以使函数更通用,以便它可以处理任何大小的条目:

void *find_item(char *name, char *names[], void *items, int item_size, int item_count)
{
  int i;

  for (i = 0; i < item_count; i++) {
    if (strcmp(name, names[i]) == 0) {
      return (char*)items + i * item_size;
    }
  }
  return NULL;
}

int main() {
  voidfunc fptr;

  fptr = *(voidfunc*)find_item("quux", name_list, ptr_list,
                              sizeof(ptr_list[0]),
                              sizeof(ptr_list) / sizeof(ptr_list[0]));
  fptr();

  return 0;
}

现在find_entry() 函数根本不需要直接处理该项目。相反,它只返回一个指向数组的指针,调用者可以在取消引用之前将其转换为指向函数指针的指针。

(上面的代码 sn-p 假定来自原始问题的定义。您也可以在此处查看完整代码:try it online!

【讨论】:

  • 编译器禁止转换的原因是sizeof(void(*)(void))可以和sizeof(void*)不同这是一个的原因。这不是的原因。 另一个原因阅读@Art's answer to this very question。不仅如此,这个答案充其量是令人困惑的,甚至可能是严格的混叠违规,而且肯定是不完整的。 voidfunc 是什么?如何填充列表以便您的 find_item() 可以工作? *(voidfunc*)find_item 确实看起来像是潜在的严格别名违规。
  • @AndrewHenle 这实际上并没有在任何地方强制转换函数指针。它是将指针转换为函数指针,而函数指针本身只是数据。据我所知,使用 void* 和 char* 的指针运算不会以任何方式违反严格的别名。他们甚至没有在这里被取消引用。当我只修改一个函数时,我觉得从原始问题中复制粘贴所有代码是没有意义的 - 你可以在问题中找到 voidfunc 等的 typedef。
  • 这是一个指向函数指针的指针 你不能在严格符合 C 代码中做到这一点。阅读6.3.2.3 Pointers。您所提议的内容未包含在 任何 八段中,因此超出了标准 C 语言的范围。
  • @AndrewHenle 你提出了一个有效的观点;我不明白为什么标准似乎不允许这样做,所以我把它作为一个单独的问题提出了 stackoverflow.com/questions/53033813/… 。我想这归结为函数指针是否是“对象类型”(函数本身当然不是对象,但指针可能是)。
  • @AndrewHenle “甚至可能是严格的别名违规”。 C : “嘿,这里有一些严格的别名规则,如果你违反了这些规则,UB,perf 原因。”。开发人员:“嘿编译器,你会在违规时出错吗?”。编译器:“...”。开发人员:“...对???”。
猜你喜欢
  • 1970-01-01
  • 2013-06-03
  • 1970-01-01
  • 2018-03-05
  • 2012-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-22
相关资源
最近更新 更多