代码struct _IO_FILE_plus; 是名称_IO_FILE_plus 的声明,因此如果编译器看到它在某个地方被使用,它就知道在某个时候会有一个实际描述其成员的定义。
extern 修饰符表示命名的符号是存在于某个其他编译单元中的外部符号。代码如:
extern struct _IO_FILE_plus _IO_2_1_stdin_;
也是符号的声明,在本例中为_IO_2_1_stdin_,告诉编译器该符号是一个已定义并存在于某个其他编译单元(文件)中的外部符号,以及该符号的类型是什么,在这种情况下为struct _IO_FILE_plus。
但是,通常在其他声明中使用struct 声明通常会使用指向struct 的指针,因为struct 的大小及其布局不能仅由诸如struct _IO_FILE_plus; 之类的声明来确定。
但是在这种情况下,因为它是外部的,除非源代码包含一些声明,要求编译器可以使用 struct 的大小和布局,使用这种方式声明的符号是有效的。
因此,如果您有以下陈述之类的来源:
struct _IO_FILE_plus *myIo = malloc(sizeof(struct _IO_FILE_plus));
struct _IO_FILE_plus myIo = _IO_2_1_stdin_; // no pointers here, struct assignment
这些会产生错误,因为编译器需要 struct _IO_FILE_plus 的定义来确定 sizeof() 的结果或在这些语句中为 struct 赋值复制的内存量。
但是,如果您有如下声明:
struct _IO_FILE_plus *myIO = &_IO_2_1_stdin_;
这将编译,因为编译器只需要知道如何找到外部变量的地址并将该地址放入指针变量中。外部变量的地址由加载器在加载应用程序并设置为运行时固定。
如果外部不存在,那么链接时会出现“未解析的外部符号”错误。
API 库示例
这可能有用的一种方法是,如果您有多个由代理对象表示的不同对象或设备,并且您有一个函数库,您希望允许人们为其中的函数选择目标对象或设备。
因此,您所做的是在您的库中将这些对象或代理对象公开为外部对象,但您只需提供声明即可对其内部进行保密。
然后在函数接口中,您需要一个指向要与函数一起使用的适当对象或代理对象的指针。
这种方法的好处在于,有权访问您的库内部的其他方可以提供额外的代理对象,这些代理对象可以与您的库一起使用,但也可以使用他们自己的代理对象。
当struct 定义包含指向您的库将调用以执行第三方知道但您不必这样做的特定于设备的操作的挂钩函数的指针时,此方法尤其有效。钩子函数有一个定义好的接口,其中包含一组预期的结果,而如何完成则取决于钩子函数的提供者。
so库源文件:
struct _IO_FILE_plus {
unsigned char buffer[1024];
int bufptr1;
// … other struct member definitions
int (*hookOne)(struct _IO_FILE_plus *obj); // third party hook function pointer
int (*hookTwo)(struct _IO_FILE_plus *obj); // third party hook function pointer
};
struct _IO_FILE_plus _IO_2_1_stdin_ = { {0}, 0, …. };
struct _IO_FILE_plus _IO_2_1_stdout_ = { {0}, 0, …. };
struct _IO_FILE_plus _IO_2_1_stderr_ = { {0}, 0, …. };
int funcOne (struct _IO_FILE_plus *obj, int aThing)
{
int iResult;
if (obj->hookOne) iResult = obj->hookOne(obj);
// do other funcOne() stuff using the object, obj, provided
return iResult;
}
int funcTwo (struct _IO_FILE_plus *obj, double aThing)
{
int iResult;
if (obj->hookTwo) iResult = obj->hookTwo(obj);
// do other funcTwo() stuff using the object, obj, provided
return iResult;
}
库源文件编译良好,因为编译器具有可用的struct 的完整定义。然后在库提供的头文件中,您有以下语句:
struct _IO_FILE_plus ;
extern struct _IO_FILE_plus _IO_2_1_stdin_ ;
extern struct _IO_FILE_plus _IO_2_1_stdout_ ;
extern struct _IO_FILE_plus _IO_2_1_stderr_ ;
extern int funcOne (struct _IO_FILE_plus *obj, int aThing);
extern int funcTwo (struct _IO_FILE_plus *obj, double aThing);
这些都有效,因为这些源语句都不需要struct 的实际定义可供编译器使用。编译器只需要知道某处定义了这样的符号。
在使用这些的源文件中,您可以有如下语句:
int k = funcOne(&_IO_2_1_stdin_, 5);
同样,这只需要编译器知道符号存在,并且在某些时候该符号的地址将可用。
作为库设计的一部分,很可能有 C 预处理器宏用于进一步隐藏这些管道。所以你可能有这样的宏:
#define DO_FUNCONE(io,iVal) funcOne(&(io), (iVal))
#define DO_FUNCONE_STDIN(iVal) funcOne(&_IO_2_1_stdin_,(iVal))
#define IO_STDIN (&_IO_2_1_stdin)
但是,像下面这样的语句将无法编译,因为编译器将向函数提供 struct 的副本,该函数采用外部值而不是指向它的指针:
int k = doFuncOne (_IO_2_1_stdin_); // compiler error. definition of struct _IO_FILE_plus not available
函数doFuncOne()的函数定义如下:
// compiler error. definition of struct _IO_FILE_plus not available
int doFuncOne (struct _IO_FILE_plus obj) // notice this is struct and not pointer to struct
{
// do some setup then call funcOne().
return funcOne(&obj, 33);
}
但是,对函数 doFuncOne() 的接口的更改将允许它编译:
// following would compile as only declaration is needed by the compiler.
int doFuncOne (struct _IO_FILE_plus *obj) // notice this is now pointer to struct
{
// do some setup then call funcOne().
return funcOne(obj, 33);
}
库可以提供函数funcOne() 的一个版本,比如funcOneStruct(),它允许struct 的参数而不是指向struct 的指针,因为编译器有一个可用的struct 定义编译库的源文件时。但是,使用该库的人将无法使用该功能,因为该库的用户只有struct 的声明可供他们使用,而没有struct 的定义。
这样的函数可能对拥有struct 定义的第三方开发人员有用,他们也许可以克隆库提供的现有对象之一。