【问题标题】:What does it mean to have a struct without a definition?没有定义的结构是什么意思?
【发布时间】:2019-10-07 03:43:29
【问题描述】:

最近在我的系统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;,但是拥有一个裸结构似乎很奇怪,因为您不能使用该结构或将其传递给函数。这只是无操作吗?

【问题讨论】:

  • 在大多数情况下这些用于Opaque pointer,您不需要知道数据的类型或确切的结构,这种类型可以在通过指针传递时由函数使用(指针do有固定类型)。但是知道结构的代码可以使用它。
  • 对,但在这种情况下,声明中或文件中的其他地方没有指针。

标签: c struct forward-declaration


【解决方案1】:

代码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 定义的第三方开发人员有用,他们也许可以克隆库提供的现有对象之一。

【讨论】:

  • 有趣,所以你唯一能用这个声明做的就是获取它的地址?看起来很奇怪。
  • @JoshuaNelson 欢迎来到 C!哈哈。其实并没有那么奇怪。它有一个很好的逻辑。它对于某些类型的利基应用程序非常有用。不透明的指针就是其中之一。
  • 另外,对于加载器与链接器的迂腐正确,+1:P
  • 我认为我真正的问题是“为什么 glibc 的作者会这样做”,只有 glibc 的作者才能回答这个问题。
  • @JoshuaNelson 的一个原因是传递一个指向您想与他们的库一起使用的适当对象的指针,以便他们知道函数的目标。我也会用那个例子更新我的答案。
猜你喜欢
  • 2020-03-20
  • 1970-01-01
  • 2011-12-02
  • 1970-01-01
  • 2011-06-25
  • 1970-01-01
  • 2015-10-13
  • 2020-07-17
  • 1970-01-01
相关资源
最近更新 更多