这些兼容的功能类型是否在C?

时间:2014-07-14 19:04:27

标签: c function pointers language-lawyer c11

考虑以下C程序:

int f() { return 9; }
int main() {
  int (*h1)(int);
  h1 = f; // why is this allowed?                                               
  return h1(7);
}

根据C11标准,Sec。 6.5.16.1,在一个简单的指配中,"以下之一应保留",列表中唯一相关的一个如下:

  

左操作数具有原子,限定或非限定指针类型,并且(考虑左值操作数在左值转换后将具有的类型)两个操作数都是指向兼容类型的限定或非限定版本的指针,以及指向的类型。 left包含右边指向的所有类型的限定符;

此外,这是一个"约束",这意味着,符合要求的实现必须在违反诊断消息时报告。

在我看来,在上述程序的赋值中违反了这种约束。赋值的两端都是函数指针。所以问题是,两种功能类型是否兼容?第二节回答了这个问题。 6.7.6.3:

  

对于两个兼容的函数类型,两者都应指定兼容的返回类型.146)此外,参数类型列表(如果两者都存在)应在参数数量和省略号终止符的使用中一致;相应的参数应具有兼容的类型。如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分并且包含空标识符列表,则参数列表不应具有省略号终止符,并且每个参数的类型应为与应用默认参数促销产生的类型兼容。如果一个类型具有参数类型列表而另一个类型由包含(可能为空)标识符列表的函数定义指定,则两者应在参数数量上一致,并且每个原型参数的类型应与类型兼容这是因为默认参数促销应用于相应标识符的类型。

在这种情况下,h1的类型之一具有参数类型列表;另一个,f,没有。因此,上述引文中的最后一句适用:特别是"两者都应在参数数量上达成一致"。显然h1需要一个参数。 f怎么样?以下几点出现在上述内容之前:

  

函数声明符中的空列表是该函数定义的一部分,指定该函数没有参数。

很明显,f取0个参数。因此,这两种类型的参数数量不一致,两种函数类型不兼容,并且赋值违反约束,应该发出诊断。

但是,编译程序时gcc 4.8和Clang都不会发出警告:

tmp$ gcc-mp-4.8 -std=c11 -Wall tmp4.c 
tmp$ cc -std=c11 -Wall tmp4.c 
tmp$

顺便说一句,如果声明了f" int f(void)......",两个编译器都会发出警告,但根据我对上述标准的阅读,这不应该是必要的。

问题:

Q1:分配" h1 = f;"在上面的程序中违反约束"两个操作数都指向兼容类型的合格或非限定版本"?具体做法是:

Q2:表达式中h1的类型" h1 = f"是某些函数类型T1指向T1的指针。究竟是什么T1?

Q3:表达式中f的类型" h1 = f"对于某些函数类型T2,指针指向T2。 T2究竟是什么?

问题4:T1和T2兼容吗? (请引用标准或其他文件的相应部分以支持答案。)

Q1',Q2',Q3',Q4':现在假设f的声明更改为" int f(void){return 9; }&#34 ;.再次回答问题1-4这个程序。

5 个答案:

答案 0 :(得分:8)

这两个缺陷报告解决了您的问题:

缺陷报告316 说(强调我的前进):

  

6.7.5.3#15 中函数类型的兼容性规则没有   定义函数类型何时“由函数定义指定”   包含(可能为空)标识符列表“,[...]

它与你给出的例子类似:

void f(a)int a;{}
void (*h)(int, int, int) = f;

然后继续说:

  

我认为标准的意图是a指定类型   函数定义仅用于检查兼容性   多个相同功能的声明;当这里的名字   函数出现在表达式中,其类型由其确定   返回类型并且不包含参数类型的跟踪。然而,   实施解释各不相同。

     

问题2:上述翻译单位是否有效?

委员会的回答是:

  

委员会相信第一季度和第二季度的答案。 2是的

这是在C99和C11之间,但委员会补充道:

  

我们无意修复旧式规则。然而   本文件中的观察结果似乎是正确的。

到目前为止我可以告诉C99和C11在你在问题中引用的部分没有太大的不同。如果我们进一步研究缺陷报告317 ,我们可以看到它说:

  

我认为C的意图是旧式函数定义   空括号不给函数包含a的类型   原型用于翻译单元的其余部分。例如:

void f(){} 
void g(){if(0)f(1);}
     

问题1:这样的函数定义是否为函数提供了类型   包括翻译单元其余部分的原型?

     

问题2:上述翻译单位是否有效?

委员会的答复是:

  

问题#1的答案是否定的,问题#2的答案是肯定的。有   但是,如果执行了函数调用,则不会发生约束违规   它会有不确定的行为。见6.5.2.2; p6。

这似乎取决于以下事实:函数定义是否定义了类型或原型是不明确的,因此意味着没有兼容性检查要求。这最初是旧式函数定义的意图,委员会不会进一步澄清,因为它已被弃用。

委员会指出,仅仅因为翻译单位有效并不意味着没有未定义的行为。

答案 1 :(得分:2)

从历史上看,C编译器通常以保证额外参数将被忽略的方式处理参数传递,并且还只要求程序传递参数实际使用,从而允许例如

int foo(a,b) int a,b;
{
  if (a)
    printf("%d",b);
  else
    printf("Unspecified");
}

可以通过foo(1,123);foo(0);安全地调用,而不必在后一种情况下指定第二个参数。即使在普通调用约定不支持这种保证的平台(例如经典Macintosh)上,C编译器通常默认使用支持它的调用约定。

标准清楚地表明编译器不是必需来支持这种用法,但要求实现禁止它们不仅会破坏现有代码,而且还会使这些实现无法生成代码与预标准C中的代码一样高效(因为必须更改应用程序代码以传递无用的参数,然后编译器必须生成代码)。实现此类用法未定义行为减轻了支持它的任何义务的实现,同时仍然允许实现在方便时支持它。

答案 2 :(得分:1)

不是您问题的直接答案,但编译器只是在调用函数之前生成用于将值推入堆栈的程序集。

例如(使用VS-2013编译器):

mov         esi,esp
push        7
call        dword ptr [h1]

如果在此函数中添加局部变量,则可以使用其地址来查找调用函数时传递的值。

例如(使用VS-2013编译器):

int f()
{
    int a = 0;
    int* p1 = &a + 4; // *p1 == 1
    int* p2 = &a + 5; // *p2 == 2
    int* p3 = &a + 6; // *p3 == 3
    return a;
}

int main()
{
    int(*h1)(int);
    h1 = f;
    return h1(1,2,3);
}

因此,实质上,使用附加参数调用函数是完全安全的,因为在将程序计数器设置为函数的地址(在可执行映像的代码部分中)之前,它们只是被压入堆栈。

当然,可以声称它可能会导致堆栈溢出,但在任何情况下都会发生(即使传递的参数数量与声明的参数数量相同)。

答案 3 :(得分:-1)

对于没有声明参数的函数,编译器不会推断出参数/参数类型。以下代码基本相同:

int f()
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

我认为这与支持变量长度参数的基本方式有关,而且()基本上与(...)相同。仔细查看生成的目标代码,可以看出f()的参数仍然被推送到用于调用函数的寄存器上,但由于它们在函数定义中被引用,因此它们不会在函数内部使用。如果你想声明一个不支持参数的参数,那么写它就更合适了:

int f(void)
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

此代码无法在GCC中编译以导致以下错误:

In function 'main':
error: too many arguments to function 'f'

答案 4 :(得分:-4)

尝试在函数声明之前使用__stdcall - 它不会编译 原因是函数调用默认为__cdecl。这意味着(除了其他功能)调用者在调用后清除堆栈。因此,调用者函数可以推动堆栈所需的一切,因为它知道它推动了什么,并将以正确的方式清除堆栈 __stdcall意味着(除了其他东西)被调用者清理堆栈。所以参数的数量必须匹配 ...标志告诉编译器,参数的数量各不相同。如果声明为__stdcall,那么它将自动替换为__cdecl,您仍然可以使用任意数量的参数。

这就是编译器发出警告但不会停止的原因。

实例
错误:堆栈已损坏。

#include <stdio.h>

void __stdcall allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

作品

#include <stdio.h>

void allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

对于此示例,您具有正常行为,该行为未与标准互连。你声明了指向函数的指针,然后分配这个指针,它会导致隐式类型转换。我写了为什么它有效。在c中你也可以写

int main() {
  int *p;
  p = (int (*)(void))f; // why is this allowed?      
  ((int (*)())p)();
  return ((int (*)())p)(7);
}

它仍然是标准的一部分,但当然是标准的其他部分。即使你将指向函数的指针分配给指向int的指针,也没有任何反应。