C语言中静态函数的优先级

9
假设您有一个名为File1.c的文件,并在其中有一个静态函数Foo,该函数在File1.c中被调用。另外,在另一个文件(File2.c)中,您有另一个不是静态的Foo函数。我理解静态函数在其声明所在的文件之外是不可见的,实际上对于链接器是不可见的。
但这是否意味着File1.c中对Foo函数的内部调用总是在编译期间解析?
是否存在File1.c中的Foo调用可以链接到File2.c的全局Foo函数的情况?

3
好的,我会尽力进行翻译。请问您需要什么样的帮助? 第一个问题的答案是“是”,第二个问题的答案是“不”。 - bruno
请查看以下链接,了解C语言中静态函数的详细信息: https://www.geeksforgeeks.org/what-are-static-functions-in-c/ - Luis Dipak
我认为静态符号对于链接器来说是不可见的。我可能错了。 - rsonx
@Frank 链接器不考虑静态符号,它会绕过它们,即使它们对于调试目的可见。 - bruno
C标准规定:“如果对象或函数的文件作用域标识符的声明包含static存储类别说明符,则该标识符具有内部链接22)”。 - Jean-François Fabre
显示剩余4条评论
3个回答

6

总结

一旦在翻译单元中定义了一个静态函数foo,则除非该函数被命名为foo的非函数(如对象或类型定义)所隐藏,否则在翻译单元的其余部分中,foo将指向该函数。它不会链接到名为foo的外部函数。

通过如下所述的声明调整,理论上标识符可能在该翻译单元中同名的静态声明后引用其他翻译单元中的函数。不幸的是,由于C标准中的C 2018 6.2.2 7,该行为未被定义:

如果在一个翻译单元中,相同的标识符同时具有内部和外部链接,则其行为未定义。

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

详细信息

C的作用域和链接规则回答了这些问题。

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

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

由于标识符foo在任何函数之外声明,它具有文件作用域(C 2018 6.2.1 4)。这意味着标识符foo在余下的File1.c中可见并指定该函数定义。此外,由于使用了static,它具有内部链接(6.2.2 3)。
但是,作用域有一个例外。对于其他作用域内部的作用域(例如,在文件或块内定义函数或块内部的块),相同标识符的声明可以隐藏外部声明。因此,让我们考虑在块内重新声明foo
为了引用在File1.c之外定义的foo,我们需要声明foo具有外部链接性,以便将此新的foo链接到外部定义的foo。在C语言中是否有一种方法可以做到这一点?
如果我们尝试在块内声明extern int foo(int x);,则会应用6.2.2 4:
对于在先前声明可见的作用域中使用存储类别说明符extern声明的标识符,如果先前的声明指定了内部或外部链接,则稍后声明的标识符的链接与先前声明中指定的链接相同。
因此,此声明仅重新声明相同的foo
如果我们不使用extern声明它,使用int foo(int x);,则会应用6.2.2 5:
如果函数标识符的声明没有存储类别说明符,则其链接将确定为如果使用存储类别说明符extern进行了声明一样。
因此,似乎我们无法声明一个不同的foo,无论是否使用extern。但是,等等,我们还有一个技巧。我们可以通过使用没有链接的声明来隐藏指定内部或外部链接的先前声明。要获得没有链接的声明,我们可以声明一个没有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);出现的地方,具有内部链接的foo的先前声明不可见,因此6.2.2 4中引用的第一个条件不适用,而6.2.2 4的其余部分如下:

如果没有可见的先前声明,或者如果先前的声明未指定链接,则标识符具有外部链接。

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

如果在翻译单元内,相同的标识符具有内部和外部链接,则行为未定义。


非常好的回答!我唯一的建议是在你说“6.2.2 4不适用”的地方引用6.2.2p4的其余部分。 - Ian Abbott
OP的情况并不一定包括“相同的标识符同时具有内部和外部链接”的情况。(但他确实问道“是否存在这种情况”,所以您的情况将是一个案例) - M.M
@M.M:你还有什么其他方法可以在File1.c中拥有一个静态函数Foo,“是否存在File1.c中调用Foo可以链接到File2.c的全局Foo函数的情况?” 静态的Foo具有内部链接性,而对另一个Foo的调用必须具有外部链接性,因此您拥有既具有内部链接性又具有外部链接性的Foo - Eric Postpischil
@EricPostpischil 我认为 OP 并不是故意尝试从 File1 调用 File2.c 中的 foo。但如果有代码示例,问题会更清晰明了。 - M.M

4
这是否意味着File1.c中对Foo函数的内部调用总是在编译期间解析?并非如此。就大多数情况而言,语言本身并不关心它的规则是如何执行的,只要首先执行就行了。
我们可以通过快速测试来检查给定工具链(我这里使用的是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()都在符号表中,但标志不同。

我们还可以查看汇编代码:

objdump -d test.o

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

需要注意的是,调用foo函数的代码还未被链接(指向0占位符)。因此我们可以自信地说,在这种情况下解析会在链接时发生。

是否存在一些情况,File1.c中的Foo函数调用会链接到File2.c的全局Foo函数?

那是绝对不可能的。尽管通过一些魔法或未定义的行为可能实现了这一点,但在正常的项目中,您应该有信心它永远不会发生。


Linux/Unix 命令 nm 易于显示差异,T 代表 bar_,_t 代表 foo_(_U 代表 _printf_),你的答案中可以加入这些内容。 - bruno
@bruno 谢谢您的建议,这样确实更好。 - user4442671

0

这里有一个具体的例子:

// 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"开头。这里很可能会出现编译错误,但是其他答案展示了如何通过使用像块作用域函数声明这样的恶意结构来产生无声的未定义行为。

还有可能出现良好定义但不受意图控制的行为。如果a1.cstatic void foo(void);之后才#include "a2.h",则没有错误,因为有一个规则,即如果存在相同标识符的早期声明,则具有既不是static也不是extern的函数声明与其链接匹配;但在这种情况下,从a1.c调用foo()仍然会找到a1的foo。如果a2.h还有一个调用foo()的宏,那么该宏将无法按预期工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接