【问题标题】:Are these compatible function types in C?这些兼容的函数类型在 C 中吗?
【发布时间】:2014-09-04 19:17:12
【问题描述】:

考虑以下 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 具有 right 指向的类型的所有限定符;

此外,这是一个“约束”,这意味着,如果违反了一致性实现,则必须报告诊断消息。

在我看来,上述程序的赋值违反了这个约束。赋值的两边都是函数指针。那么问题来了,这两种函数类型是否兼容?这在 Sec 中得到了回答。 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:对于某些函数类型 T1,表达式“h1=f”中的 h1 类型是指向 T1 的指针。 T1 到底是什么?

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

Q4:T1 和 T2 兼容类型吗? (请引用标准或其他文件的适当部分来支持答案。)

Q1', Q2', Q3', Q4':现在假设 f 的声明更改为“int f(void) { return 9; }”。再次回答此计划的问题 1-4。

【问题讨论】:

  • 如果我把它放入 clang 我得到:functCheck.cxx:4:6: error: assignmenting to 'int (*)(int)' from in compatible type 'int ()': different number of参数 (1 vs 0) h1 = f; // 为什么允许这样做... ^ ~ 1 个错误生成。
  • @user2950041:这是一个 C 问题。 C 的声明、原型和定义的概念与 C++ 不同。
  • @user2950041:实际上,编译器确实在乎。 Clang 从文件扩展名推断语言。 (例如,您可以将模板放入您的cxx 文件中,并使用cc 进行编译;如果您将文件命名为foo.xyz,它将无法编译)。
  • 并不是我在上面的引用中找到任何支持这一点的东西,但是在 plain old c 中,一个空参数列表意味着您可以提供任意数量的参数。 IE。无效 f() != 无效 f(无效)。我对 C 的深层地牢不太了解 f() == f(...),但我怀疑它,因为 varargs 至少需要第一个参数来挂钩。
  • @user2950041 我终于认为这是一个错误,在 gcc Bugzilla 中提交一个错误怎么样?

标签: c function pointers language-lawyer c11


【解决方案1】:

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

缺陷报告 316 说(强调我的未来):

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

它有一个与你给出的类似的例子:

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

它接着说:

我相信标准的意图是类型由 函数定义仅用于检查兼容性 同一个函数的多个声明;当这里的名字 函数出现在表达式中,它的类型由它的 返回类型并且不包含参数类型的痕迹。然而, 实现解释各不相同。

问题2:上述翻译单元有效吗?

委员会的回答是:

委员会认为 Q1 和 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。

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

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

【讨论】:

  • 在 DR317 中显然是 1.NO, 2.YES;标准明确规定void f() { } 不构成原型。
  • DR316 Q2 与此 SO 帖子提出的问题相同;决议是委员会认为该代码应该被接受(因此通过推断,从 6.7.6.3 引用的文本有缺陷)
  • 谢谢,这肯定解决了这个问题,但我仍然对“意图”感到困惑。在赋值“h1=f”中,h1的类型是什么,f的类型是什么,为什么这两种指针类型的基类型是兼容的?同样,如果我们在 f 的定义中添加“void”,在这种情况下每个人(包括编译器)都同意基类型是不兼容的。
  • @SteveSiegel 我认为正在发生的事情是gccclang 以不同的方式对待旧式K&R 定义。因此,您概述的问题似乎只存在于这些表格中。 void 表单似乎不被视为旧样式。这可能是任意的,缺陷报告不是很详细,可能是故意的。我猜只有 gcc 和 llvm 团队才能完全解释这一点。
  • 我还是不明白赋值表达式中h1和f的类型是什么,即使看了缺陷报告。我在上面的主要问题中添加了具体问题 Q1-Q4 和 Q1'-Q4',希望可以改进此答案以回答这些具体问题。
【解决方案2】:

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

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

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

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

【讨论】:

    【解决方案3】:

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

    例如(使用 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);
    }
    

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

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

    【讨论】:

      【解决方案4】:

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

      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'
      

      【讨论】:

      • 不,()(...) 基本不同,尽管它们很可能都以相同的方式实现。两个函数,一个用()定义,一个用(...)定义,不兼容;调用没有可见原型的可变参数函数具有未定义行为。 (出于历史原因和满足 ABI,大多数 C 编译器都使用相同的调用约定。)
      • 这与函数指针主题没有太大关系(也许是为了说明具有“兼容”类型的函数指针的措辞与实际调用函数的行为不同)
      【解决方案5】:

      尝试在函数声明之前使用 __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);
      }
      

      对于这个例子,你有正常的行为,这与标准没有关联。您声明指向函数的指针,然后分配 this 指针并导致隐式类型转换。我写了它为什么有效。在c中你也可以写

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

      它仍然是标准的一部分,但当然是标准的其他部分。即使您将指向函数的指针分配给指向 int 的指针,也不会发生任何事情。

      【讨论】:

      • -1,标准对 stdcall 和调用约定一无所知。
      • 没错。但答案有什么问题?对不对?
      • 关键是它没有回答这个问题,对我来说这是一个“标准解释”的要求。目前尚不清楚您对此证明了什么,是的,如果您使用特定的非标准扩展,一切都会中断,但问题不是“它是否恰好在 VC++ 中工作”,而是“它是否保证 在纯 C 中工作?它不应该发出警告吗?而对于这些问题,只有引用标准才能给出权威的答案。
      • 请问什么是 PLAIN C?告诉我纯 c 编译器的名称。如果你有 PLAIN C 的编译器,只需在这个编译器上编译项目。如果它被编译,那么它工作正常,如果没有 - 那么这个代码是无效的。我解释了它为什么起作用,而不是如果这是一个标准。您已阅读标准的一部分。如果仍有问题 - 请再次阅读。它是单义的。
      • “Plain C”是标准规定的。根本没有普通的 C 编译器(或者,如果您愿意,没有定义正确的参考实现),这就是为什么不能通过对特定实现执行测试来回答这类问题(这可能是错误的 - 它是在谈论语言的尘土飞扬的角落时并不罕见),尤其是在您测试不相关的东西时。
      猜你喜欢
      • 2020-07-27
      • 1970-01-01
      • 2020-06-17
      • 1970-01-01
      • 2015-03-02
      • 2016-01-19
      • 1970-01-01
      • 2019-09-18
      • 2015-02-03
      相关资源
      最近更新 更多