分配给具有不同签名的函数的C函数指针

时间:2016-05-11 20:21:47

标签: c gcc

在某些库代码中,有一种为事件设置回调的模式,其中回调可能会收到一个参数。回调本身可能不会对参数执行任何操作,或者参数可能是0,但它会被传递。将其分解为基础知识,如下所示:

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

void callback_1(char *data) {
  printf("Length of data: %d\n", strlen(data));
}

void callback_2() {
  printf("No parameters used\n");
}

typedef void (*Callback)(char *);

int main(void) {
    Callback callback;

    callback = callback_1;
    callback("test");

    callback = callback_2;
    callback("test");

    return 0;
}

这在GCC 4.9.3(32位)上编译并运行,没有任何意外。

一个好的回调函数有一个像callback_1这样的签名,但是如果它没有被使用,我偶尔会忘记data参数。虽然我知道C并不总是类型安全的(特别是关于void指针),但我预计会发出警告,因为如果我提供了一个不匹配的参数,例如int data,我会收到有关不兼容类型的警告。如果Callback的typedef不接受参数,如果回调函数在签名中有一个参数,我会收到编译错误。

在C中是否有一种方法可以获得警告,其中函数指针被分配给签名缺少参数的函数?如果回调缺少参数,堆栈上会发生什么?是否可能会在回调中错过参数?

2 个答案:

答案 0 :(得分:3)

这是因为您将callback2定义为:

void callback_2()

空括号表示它需要未指定的个参数。因此,它有资格被分配到Callback类型。

如果您将定义更改为:

void callback_2(void)

这显式指定该函数接受0个参数,并且您将获得“从不兼容的指针类型分配”警告。

为了正确捕获这个条件,使用-Wstrict-prototypes-Wall -Wextra进行编译,如果声明或定义一个带有空参数列表的函数,你将获得以下内容:

warning: function declaration isn’t a prototype

答案 1 :(得分:1)

您的代码不仅具有未定义的行为,还包含已弃用的功能。

6.7.5.3/14

  

标识符列表仅声明参数的标识符   功能。函数声明符中的空列表,它是a的一部分   该函数的定义指定该函数没有   参数。函数声明符中的空列表不是其中的一部分   该函数的定义指定没有关于的信息   提供参数的数量或类型。

注意区别。 void f();是没有定义的声明者。 void f() {}是一个定义的声明者。

6.5.2.2/2:

  

如果表示被调用函数的表达式具有类型   包括一个原型,参数的数量应该是一致的   参数数量。每个参数都应具有一个类型   value可以分配给具有非限定版本的对象   其相应参数的类型。

6.11.6

  

使用带有空括号的函数声明符(不是   prototype-format参数类型声明符)是一个过时的   特征

void f()void f(void)是兼容类型,但由于f()定义了不带参数的函数,因此使用参数调用它是未定义的行为。

足够好的迂腐,那么究竟发生了什么? C中没有名称错位,因此链接器只能看到函数的名称。 GCC和Clang都发出以完全相同的方式调用函数的代码。他们将指向函数的指针推入堆栈,参数(&#34; test&#34;字符串)然后他们调用它。这里真的没有什么可疑的。这是我objdump所得到的:

00000000004006b3 <callback_2>:
  ...

我只包含此内容以显示callback_2的地址。此示例中callback_1的地址为400686。

首先是一些一般的堆栈内容:

  4006c4:   55                      push   rbp
  4006c5:   48 89 e5                mov    rbp,rsp
  4006c8:   48 83 ec 10             sub    rsp,0x10

我们存储callback_one的地址:

  4006cc:   48 c7 45 f8 86 06 40    mov    QWORD PTR [rbp-0x8],0x400686
  4006d3:   00
  4006d4:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]

然后我们的&#34;测试&#34;串。使用objdump -s -j .rodata表示我们的字符串的地址位于索引7,地址4007b0。

 4007b0 73207573 65640074 65737400           s used.test. 
                                             1234567

4007b0 + 7是4007b7。

  4006d8:   bf b7 07 40 00          mov    edi,0x4007b7

致电callback_one

  4006dd:   ff d0                   call   rax

重复callback_two

  4006df:   48 c7 45 f8 b3 06 40    mov    QWORD PTR [rbp-0x8],0x4006b3
  4006e6:   00 
  4006e7:   48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  4006eb:   bf b7 07 40 00          mov    edi,0x4007b7
  4006f0:   ff d0                   call   rax

那么为什么你没有得到任何警告?嗯,从技术上讲,你并没有违反任何语言规则。未定义的行为不需要可诊断。这个故事的寓意是:如果你认为代码需要警告,就不要写它。但C是一种棘手的语言。只要您了解语言,警告以及您的编译器正在做什么就应该没问题。