11

我想查看真实世界应用程序的源代码以了解良好的编程实践等。所以我选择了 Git 并下载了 1.8.4 版本的源代码。

在随机浏览各种文件后,这两个文件引起了我的注意:strbuf.h strbuf.c

这两个文件显然用这个文档定义了一个 API 。

我有两个问题:

  1. 为什么'strbuf.h'中第16,17,18,19行的函数声明和第6行的全局变量声明为extern?

  2. 为什么“strbuf.h”不包含在 strbuf .c 中?

我作为一个新手程序员总是知道你在 .c 文件中编写函数定义,而函数声明、宏、内联等都写在 .h 文件中,然后在每个想要使用这些的 .c 文件中#included功能等

谁能解释一下?

4

1 回答 1

32

strbuf.c包括cache.hcache.h包括,所以你对问题2(不包括)strbuf.h的前提是错误的:它确实包括它,只是不直接。strbuf.cstrbuf.h

extern应用于函数

函数声明从extern不需要关键字,但它确实有效果:它声明命名函数的标识符(即函数的名称)与任何先前可见的声明具有相同的链接,或者如果没有这样的声明是可见的,则标识符具有外部链接。这个相当混乱的措辞实际上意味着:

static int foo(void); extern int foo(void);

的第二个声明foo也声明了它static,给它内部链接。如果你写:

static int foo(void); int foo(void); /* wrong in 1990s era C */

您首先将其声明为具有内部链接,然后将其声明为具有外部链接,并且在 1999 年之前的 C 版本中,1会产生未定义的行为。因此,从某种意义上说,extern关键字增加了一些安全性(以混淆为代价),因为它可能意味着static必要时。但是你总是可以再写static一次,而且extern不是万能的:

extern int foo(void); static int foo(void); /* ERROR */

这第三种形式仍然是错误的。第一个extern声明没有先前的可见声明,因此foo有外部链接,然后第二个static声明提供foo内部链接,产生未定义的行为。

简而言之,extern函数声明不需要。有些人只是出于风格原因更喜欢它。

(注意:我extern inline在 C99 中省略了,这有点奇怪,并且实现方式各不相同。有关更多详细信息,请参见http://www.greenend.org.uk/rjk/2003/03/inline.html。)

extern应用于变量声明

变量声明中的extern关键字有多种不同的效果。首先,与函数声明一样,它会影响标识符的链接。其次,对于任何函数之外的标识符(两种通常意义上的“全局变量”之一),它会导致声明成为声明,而不是定义,前提是该变量也没有被初始化。

对于函数内部的变量(即,具有“块作用域”),例如somevar

void f(void) {
    extern int somevar;
    ...
}

关键字导致extern标识符具有某种链接(内部或外部)而不是“无链接”(对于自动持续时间局部变量)。在这个过程中,它也会导致变量本身具有静态的持续时间,而不是自动的。(自动持续时间变量从不具有链接,并且始终具有块范围,而不是文件范围。)

与函数声明一样,extern如果存在先前可见的内部链接声明,则链接分配是内部的,否则是外部的。所以这里的x内部f()有内部链接,尽管有extern关键字:

static int x;
void f(void) {
    extern int x; /* note: don't do this */
    ...
}

写这种代码的唯一原因是为了迷惑其他程序员,所以不要这样做。:-)

通常,使用关键字注释“全局”(即文件范围、静态持续时间、外部链接)变量的extern原因是为了防止该特定声明成为定义。当多次定义相同的名称时,使用所谓的“def/ref”模型的 C 编译器会在链接时消化不良。因此,如果file1.cint globalvar;file2.c也说int globalvar;,两者都是定义,代码可能无法编译(尽管大多数类 Unix 系统默认使用所谓的“通用模型”,这无论如何都可以正常工作)。如果您在头文件中声明这样的变量(可能包含在许多不同.c的文件中),请使用extern使该声明“只是一个声明”。

然后,这些文件中的一个,并且只有一个.c可以再次声明变量,省略extern关键字和/或包含初始化程序。或者,有些人更喜欢头文件使用这样的样式:

/* foo.h */
#ifndef EXTERN
# define EXTERN extern
#endif
EXTERN int globalvar;

在这种情况下,这些文件中的一个(并且只有一个).c可以包含以下序列:

#define EXTERN
#include "foo.h"

在这里,因为EXTERN是定义的,所以#ifndef关闭后续#define并且该行EXTERN int globalvar;扩展为just int globalvar;,以便它成为定义而不是声明。就个人而言,我不喜欢这种编码风格,尽管它确实满足了“不要重复自己”的原则。大多数情况下,我发现大写EXTERN具有误导性,而且这种模式对初始化没有帮助。喜欢它的人通常会添加第二个宏来隐藏初始化器:

#ifndef EXTERN
# define EXTERN extern
# define INIT_VAL(x) /*nothing*/
#else
# define INIT_VAL(x) = x
#endif

EXTERN int globalvar INIT_VAL(42);

但是,当要初始化的项目需要一个复合初始化器(例如,struct应该初始化为的 a { 42, 23, 17, "hike!" })时,即使这样也会崩溃。

(注意:我在这里故意掩盖了整个“暂定定义”的东西。没有初始化器的定义只是“暂定定义”,直到翻译单元结束。这允许某些类型的前向引用,否则它们太难了表达。通常不是很重要。)

f包括在定义函数的代码中声明函数的标头f

这总是一个好主意,原因很简单:编译器会将标头中的声明与代码中的定义进行比较。如果两者不匹配(出于任何原因——通常是初始编码中的错误,或者在维护期间未能更新两者之一,但有时仅仅是由于 Cat Walked On Keyboard Syndrome 或类似原因),编译器可以捕获错误在编译时。f()f()


1 1999 年的 C 标准规定,在函数声明中省略extern关键字与在其中使用关键字的含义相同extern。这更容易描述,并且意味着您获得定义(和明智)的行为而不是未定义的行为(因此可能 - 好的可能 - 坏的行为)。

于 2013-08-11T15:32:36.283 回答