最近,我在系统stdio.h中遇到了以下代码:

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 struct _IO_FILE *stdin;,但是拥有裸露的结构似乎很奇怪,因为您不能使用该结构或将其传递给函数。这只是一个禁忌症吗?

最佳答案

代码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库示例

一种有用的方法是,如果您有几个不同的对象或代理对象代表的设备,并且您具有一个功能库,希望该功能库允许人们为其中的功能选择目标对象或设备。

因此,您要做的是在库中将这些对象或代理对象公开为外部对象,但仅通过提供声明将它们的内部对象保密。

然后,在功能接口(interface)中,您需要一个指向要与功能一起使用的适当对象或代理对象的指针。

这种方法的好处是,可以访问您的库内部的其他方可以提供其他与您的库一起使用但具有自己的代理对象的代理对象。

struct定义包含指向钩子(Hook)函数的指针时,这种方法特别有用,您的库将调用该钩子(Hook)函数来执行第三方知道但您不必执行的特定于设备的操作。 Hook 函数具有定义好的接口(interface),该接口(interface)带有一组预期结果,该如何完成取决于 Hook 函数的提供者。

因此库源文件:
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()的接口(interface),则可以对其进行编译:
// 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的定义来克隆他们提供的库中现有的对象之一。

10-07 19:46
查看更多