C中静态函数的优先级

Priority of static functions in C

假设您有一个包含静态函数 Foo 的文件 File1.c,并且该函数是在 File1.c 中调用的。此外,在另一个文件 (File2.c) 中,您有另一个非静态的 Foo 函数。我知道静态函数在声明它的文件之外是不可见的,实际上对于链接器来说是不可见的。

但这是否意味着 File1.c 中 Foo 函数的内部调用总是在编译期间解决?

是否存在 File1.c 中的 Foo 调用可以链接到 File2.c 的全局 Foo 函数的情况?

does that mean that internal calling of Foo function in File1.c is always resolved during compilation?

不一定。在大多数情况下,语言本身并不关心它的规则是如何执行的,只关心它们首先被执行。

我们可以通过快速测试检查给定的工具链(在我的例子中 linux/gcc)如何选择这样做:

从一个简单的文件开始 (test.c):

#include <stdio.h>

static void foo() {
    printf("hello");
}

void bar() {
    foo();
}

然后编译并检查生成的目标文件:

gcc -c -o test.o test.cpp
nm test.o

0000000000000018 T bar
0000000000000000 t foo
                 U _GLOBAL_OFFSET_TABLE_
                 U printf

我们看到 foo()bar() 都在符号 table 中,但标志不同。

我们也可以看看汇编:

objdump -d test.o

0000000000000018 <bar>:
  18:   55                      push   %rbp
  19:   48 89 e5                mov    %rsp,%rbp
  1c:   b8 00 00 00 00          mov    [=12=]x0,%eax
  21:   e8 da ff ff ff          callq  0 <foo>
  26:   90                      nop
  27:   5d                      pop    %rbp
  28:   c3                      retq  

并看到对 foo 的调用尚未被link编辑(指向 0 占位符)。所以我们可以自信地说,在这种情况下,解决可以而且确实会在 link 时间发生。

Are there cases where Foo calling in File1.c can be linked to the global Foo function of File2.c?

这是一个直接的否定。通过骇人听闻的魔术或未定义的行为可能会发生这种情况,但在正常项目中,您应该确信它永远不会发生。

总结

在翻译单元中定义静态函数 foo 后,foo 会在翻译单元的其余部分引用该函数,但它可以被 [=78= 隐藏](例如对象或类型定义)命名为 foo 的部分翻译单元。它不会 link 到名为 foo.

的外部函数

通过修改下面解释的声明,一个标识符 理论上可以 指代一个在 static 同名声明之后来自另一个翻译单元的函数翻译单位。不幸的是,由于 C 2018 6.2.2 7:

,该行为未由 C 标准定义

If, within a translation unit, the same identifier appears with both internal and external linkage, the behavior is undefined.

这意味着您不能单独依赖 C 标准来确保此行为,但 C 实现可以将其定义为扩展。

详情

这些问题由 C 的作用域和 linkage 规则回答。

假设在File1.c中我们有一个函数的静态定义:

static int foo(int x) { return x*x; }

由于标识符 foo 是在任何函数之外声明的,因此它具有文件范围 (C 2018 6.2.1 4)。这意味着标识符 foo 是可见的,并为 File1.c 的其余部分指定此函数定义。此外,由于使用了 static,它具有内部 linkage (6.2.2 3).

范围存在异常。对于其他范围内的范围,例如在文件内定义函数的块 { … } 或块内的块,相同标识符的声明可以隐藏外部声明。因此,让我们考虑在块内重新声明 foo

为了引用 File1.c 之外定义的 foo,我们需要用外部 linkage 声明 foo,这样这个新的 foo 可以 link 编辑为外部定义的 foo。有没有办法在 C 中做到这一点?

如果我们尝试在块内声明 extern int foo(int x);,则 6.2.2 4 适用:

For an identifier declared with the storage-class specifier extern in a scope in which a prior declaration of that identifier is visible, if the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration.

所以这个声明只会重新声明相同的 foo

如果我们在没有extern的情况下声明它,使用int foo(int x);,6.2.2 5 适用:

If the declaration of an identifier for a function has no storage-class specifier, its linkage is determined exactly as if it were declared with the storage-class specifier extern.

所以,似乎我们不能在有或没有 extern 的情况下声明不同的 foo。但是,等等,我们还有一个技巧。我们可以通过使用没有 linkage 的声明隐藏它来使指定内部或外部 linkage 的先前声明不可见。要获得没有 linkage 的声明,我们可以声明一个没有 extern:

的对象(而不是函数)
#include <stdio.h>

static int foo(int x) { return x*x; }

void bar(void)
{
    int foo; // Not used except to hide the function foo.
    {
        extern int foo(int x);
        printf("%d\n", foo(3));
    }
}

因为在 extern int foo(int x); 出现的地方,内部 linkage 的 foo 的先前声明不可见,上面引用的 6.2.2 4 中的第一个条件不适用,和 6.2.2 4 的其余部分:

If no prior declaration is visible, or if the prior declaration specifies no linkage, then the identifier has external linkage.

这是“合法的”C 代码。不幸的是,它未被 6.2.2 7:

定义

If, within a translation unit, the same identifier appears with both internal and external linkage, the behavior is undefined.

这是一个具体的例子:

// a1.c
static void foo(void) { }
void bar(void) { foo(); }

// a2.c
void bar(void);
void foo(void) { bar(); }
int main(void) { foo(); }

在这个例子中代码是正确的:

  • a1.c 具有使用内部链接和一个匹配定义声明的标识符 foo
  • a2.c 具有使用外部链接和一个匹配定义声明的标识符 foo

如果您试图让 a1.c 包含 a2 的 foo 的声明,您会 运行 遇到问题的领域。

例如:假设 a2.h 有内容 void foo(void);,而 a1.c#include "a2.h" 开头。这里可能存在编译错误,但是其他答案显示了如何通过使用诸如 block-scope 函数声明之类的邪恶构造来实现无声的未定义行为。

也可能出现 well-defined 但意外的行为。如果 a1.c#include "a2.h" 之后 static void foo(void); 那么就没有错误,因为有一个规则,函数声明既不 static extern 也不匹配相同标识符的较早声明的链接(如果存在的话);但在那种情况下,从 a1.c 调用 foo() 仍然会找到 a1 的 foo。如果 a2.h 也有一个名为 foo() 的宏,那么该宏将不会按预期运行。