我最近了解到:
int a;
文件范围内的是变量声明,默认情况下具有外部链接。
因此,我可以使用它来实现诸如弱符号/功能链接之类的东西:
cat >lib.c <<'EOF'
#include "lib.h"
#include <stdio.h>
#include <stdint.h>
// This is declaration
// It will be initialized to NULL in case no definition is found
void (* const lib_callback_pnt)(int);
void lib_callback_default(int a)
{
printf("%s %d\n", __func__, a);
}
void lib_call(int a)
{
printf("%s calling %p\n", __func__,
// this is not really portable
(void*)(uintptr_t)(intmax_t)lib_callback_pnt
);
// call callback
void (* const lib_callback_to_call)(int) =
lib_callback_pnt == NULL
? lib_callback_default
: lib_callback_pnt;
lib_callback_to_call(a);
}
EOF
cat >lib.h <<'EOF'
#ifndef LIB_H_
#define LIB_H_
extern void (* const lib_callback_pnt)(int);
void lib_callback_default(int a);
void lib_call(int a);
#endif
EOF
cat >main1.c <<EOF
#include "lib.h"
int main() {
lib_call(42);
}
EOF
cat >main2.c <<'EOF'
#include "lib.h"
#include <stdio.h>
static void my_lib_callback(int a)
{
printf("Hah! Overwritten lib callback!\n");
}
// this is definition
void (* const lib_callback_pnt)(int) = my_lib_callback;
int main() {
lib_call(42);
}
EOF
cat >Makefile <<'EOF'
CC=gcc
CFLAGS=-Wall -Wextra -pedantic -std=c11
all:
$(CC) $(CFLAGS) lib.c main1.c -o main1
$(CC) $(CFLAGS) lib.c main2.c -o main2
EOF
在lib.c
库中,我声明了一个函数指针void (* const lib_callback_pnt)(int)
,它用作回调。函数指针未在lib.c
中初始化,并且将默认初始化为NULL(导致静态存储持续时间)。
然后我有两个程序或用户应用程序,分别是main1.c
和main2.c
。
main1.c
只需调用库函数即可调用回调-回调未在任何地方初始化,因此默认情况下初始化为NULL-我可以在库中进行比较并适当地调用默认回调/ select动作。
然而main2.c
声明了带有初始化的函数指针lib_callback_pnt
-这是一个定义。在所有源文件中,此变量只有一个定义,因此链接器不会抱怨多个符号定义。当我们调用库时,指针已初始化,因此用户应用程序main2
已成功覆盖了回调。
我们可以编译:
$ make
gcc -Wall -Wextra -pedantic -std=c11 lib.c main1.c -o main1
gcc -Wall -Wextra -pedantic -std=c11 lib.c main2.c -o main2
然后致电:
$ ./main1
lib_call calling (nil)
lib_callback_default 42
$ ./main2
lib_call calling 0x5627c07871cf
Hah! overwritten lib callback!
问题:
答案 0 :(得分:2)
您的lib.c实际上确实定义了lib_callback_pnt
。 C11在6.9.2p2说:
具有文件范围的对象(没有初始化程序),存储类说明符或存储类说明符
static
的对象的标识符声明构成临时定义 。如果翻译单元包含一个或多个标识符的临时定义,并且翻译单元不包含该标识符的外部定义,则该行为就好像该翻译单元包含该标识符的文件范围声明,且复合类型为转换单元末尾的值,初始值设定为0。
因此lib.c中lib_callback_pnt
的声明是一个临时定义。由于该转换单元不包含任何其他lib_callback_pnt
显式定义它的声明,因此该行为应与使用“ = 0
”对其进行初始化的真实定义相同。
默认情况下,带有ELF输出的gcc显然不完全符合此要求。
在我的Linux系统上,如果我gcc -c lib.c; nm lib.o | grep lib_callback_pnt
,我得到:
0000000000000008 C lib_callback_pnt
我的man nm
文档解释说,“ C”表示“公共符号”:
“ C”该符号是常见的。通用符号是未初始化的数据。链接时,可能会出现多个具有相同名称的通用符号。如果符号定义在任何地方,则将通用符号视为未定义的引用。
因此您的方法确实适用于gcc / ELF,但根据C标准,它是不正确的,因此您不能指望它可以与其他编译器一起使用。
答案 1 :(得分:1)
在lib.c
中,void (* const lib_callback_pnt)(int);
是对象(在此情况下为指针)的标识符的声明,该对象的文件范围没有初始化程序且没有存储类说明符。然后C 2018 6.9.2 2告诉我们:
具有文件范围且没有初始化程序,没有存储类说明符或具有存储类说明符 static 的对象的标识符声明构成临时定义< / em>。如果翻译单元包含一个或多个标识符的临时定义,并且翻译单元不包含该标识符的外部定义,则该行为就好像该翻译单元包含该标识符的文件范围声明,且复合类型为转换单元末尾的值,初始值设定为0。
这告诉我们在标准C中,lib_callback_pnt
被初始化为零,即使它是在其他转换单元中定义的也是如此。而且,它的行为就好像它具有一个初始化程序一样,这意味着它是在lib.c
中定义的。
此外,在lib_callback_pnt
中定义了main2.c
时,这违反了C 2018 6.9 5:
如果在表达式中使用了用外部链接声明的标识符(不是结果为整数常量的 sizeof 或 _Alignof 运算符的操作数的一部分) ,在整个程序中的某个位置,标识符应该只有一个外部定义;否则,最多只能有一个。
Unix中有一种传统,允许在一个翻译单元中声明int foo;
,而在另一翻译单元中声明int foo = 1;
。从技术上讲,这违反了C标准,但在编译器和链接器中通常可以使用。