但这是否意味着File1.c中对Foo函数的内部调用总是在编译期间解析?
是否存在File1.c中的Foo调用可以链接到File2.c的全局Foo函数的情况?
一旦在翻译单元中定义了一个静态函数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定义为未定义行为:
如果在翻译单元内,相同的标识符具有内部和外部链接,则行为未定义。
Foo
,“是否存在File1.c中调用Foo可以链接到File2.c的全局Foo函数的情况?” 静态的Foo
具有内部链接性,而对另一个Foo
的调用必须具有外部链接性,因此您拥有既具有内部链接性又具有外部链接性的Foo
。 - Eric Postpischilfoo
。但如果有代码示例,问题会更清晰明了。 - M.M#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函数?
那是绝对不可能的。尽管通过一些魔法或未定义的行为可能实现了这一点,但在正常的项目中,您应该有信心它永远不会发生。
这里有一个具体的例子:
// 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.c
在static void foo(void);
之后才#include "a2.h"
,则没有错误,因为有一个规则,即如果存在相同标识符的早期声明,则具有既不是static
也不是extern
的函数声明与其链接匹配;但在这种情况下,从a1.c
调用foo()
仍然会找到a1的foo
。如果a2.h
还有一个调用foo()
的宏,那么该宏将无法按预期工作。