C 中实际上有四个命名空间(尽管这取决于特定的计数方式,有些包括宏名称作为第五个空间,我认为这是考虑它们的有效方式):
-
goto 标签
- 标签(
struct、union 和 enum)
- 结构或联合类型的实际成员(每种类型一个,因此您可以将其视为“多”而不是“一个”命名空间)
- 所有其他(“普通”)标识符,例如函数和变量名称以及通过
typedef 与其他类型同义的名称。
虽然(理论上)应该可以为 struct 和 union 使用单独的空格,例如,C 没有,所以:
struct foo; union foo; /* ERROR */
无效。然而:
struct foo { int a, b; };
struct bar { char b; double a; };
很好,表明两种不同的struct 类型的成员位于不同的命名空间中(所以这再次使上面的“4 个命名空间”的计数变得可疑:-)。
除此之外,C 有一些适度(在某些方面是不必要的)复杂但在实践中相当可行的结构类型如何工作的规则。
每个struct 创建一个新类型除非它引用回现有类型。 struct 关键字后面可以跟一个标识符,或者只是一个左大括号{。如果只有一个左大括号,struct 会创建一个新类型:
struct { ... } X; /* variable X has a unique type */
如果存在标识符,编译器必须查看(单个)标记名称空间以查看该名称是否已定义。如果没有,struct 定义一个新类型:
struct blart { ... } X; /* variable X has type <struct newname>, a new type */
如果标识符是已经存在,通常这指的是现有的类型:
struct blart Y; /* variable Y has the same type as variable X */
不过,有一个特殊的例外。如果你在一个新的范围内(例如在函数的开头),一个“空声明”——struct 关键字,后跟一个标识符,后跟一个分号——“清除”之前的可见类型:
void func(void) {
struct blart; /* get rid of any existing "struct blart" */
struct blart { char *a; int b; } v;
这里v 有一个new 类型,即使struct blart 已经在func 之外定义。
(这种“空洞声明”技巧在混淆代码竞赛中最有用。:-))
如果您不是在一个新的范围内,一个空洞的声明用于声明该类型存在的目的。这主要用于解决不同的问题,我稍后会介绍。
struct blart;
这里struct blart 提醒您(和编译器)现在有一个名为“struct blart”的类型。此类型只是声明,这意味着如果struct blart 尚未定义,则结构类型是“不完整的”。如果struct blart 已定义,则此类型已定义(并且“完整”)。所以:
struct blart { double blartness; };
定义它,然后任何更早或更晚的struct blarts 引用相同的类型。
这就是为什么这种声明很有用。在 C 中,标识符的任何声明都有范围。有四种可能的作用域:“文件”、“块”、“原型”和“函数”。最后一个(函数范围)专门用于goto 标签,因此我们可以从这里开始忽略它。这留下了文件、块和原型范围。文件范围是大多数人认为的“全局”的技术术语,与“本地”的“块范围”形成对比:
struct blart { double blartness } X; /* file scope */
void func(void) {
struct slart { int i; } v; /* block scope */
...
}
这里struct blart 具有文件范围(与“全局”变量X 一样),struct slart 具有块范围(与“本地”变量v 一样)。
当块结束时,struct slart 消失。你不能再用名字来引用它;后来的struct slart 创建了一个新的不同类型,与后来的int v; 创建一个新的v 完全相同,并且不在函数func 内的块范围内引用v。
唉,设计原始 C 标准的委员会(有充分的理由)在函数原型内部增加了一个范围,与这些规则的交互相当糟糕。如果你写一个函数原型:
void proto(char *name, int value);
标识符(name 和 value)在右括号后消失,正如您所期望的那样 - 您不希望这会创建一个名为 name 的块范围变量。不幸的是,struct 也是如此:
void proto2(struct ziggy *stardust);
名称stardust 消失了,但struct ziggy 也消失了。如果struct ziggy 没有更早出现,那么在原型中创建的新的、不完整的类型现在已经从人类的所有范围内移除。它永远无法完成。好的 C 编译器会在此处打印警告。
解决方案是在编写原型之前声明结构(无论是否完整 [*]):
struct ziggy; /* hey compiler: "struct ziggy" has file scope */
void proto2(struct ziggy *stardust);
这一次,struct ziggy 有一个已经存在的可见声明可供引用,因此它使用现有类型。
[* 例如,在头文件中,您通常不知道 定义 struct 的头文件是否已包含,但您可以声明构造自己,然后定义使用指向它的指针的原型。]
现在,至于typedef...
typedef 关键字在语法上是一个存储类说明符,如 register 和 auto,但它的行为很奇怪。它在编译器中设置了一个标志,上面写着:“将变量声明更改为类型名称别名”。
如果你写:
typedef int TX, TY[3], *TZ;
您(和编译器)可以理解这一点的方式是从 删除typedef 关键字开始。结果需要在语法上有效,它是:
int TX, TY[3], *TZ;
这将声明三个变量:
-
TX 具有类型 int
-
TY 的类型为“int 的数组 3”
-
TZ 的类型为“指向int 的指针”
现在您(和编译器)将typedef 放回原处,并将“has”更改为“is another name for”:
-
TX 是类型 int 的另一个名称
-
TY 是“int 的数组 3”的另一个名称
-
TZ 是“指向int 的指针”的另一个名称
typedef 关键字与 struct 类型的工作方式完全相同。 struct 关键字创建了新类型;然后typedef 将变量声明从“具有类型...”更改为“是类型...的另一个名称”。所以:
typedef struct ca ca_t;
首先创建新类型,或者像往常一样引用现有类型struct ca。然后,不是将变量 ca_t 声明为具有类型 struct ca,而是将名称声明为类型 struct ca 的另一个名称。
如果省略结构标记名称,则只剩下两个有效的句法模式:
typedef struct; /* note: this is pointless */
或:
typedef struct { char *top_coat; int top_hat; } zz_t, *zz_p_t;
在这里,struct { 创建了一个新类型(请记住,我们在开头说过这种方式!),然后在结束 } 之后,将声明变量的标识符现在创建类型别名。 同样,该类型实际上是由 struct 关键字创建的(尽管这一次几乎无关紧要;typedef-names 现在是引用该类型的唯一方法)。
(第一个毫无意义的模式之所以如此,是因为没有大括号,您粘贴在 中的第一个标识符是 struct-tag:
typedef struct tag; /* (still pointless) */
因此您毕竟没有省略标签!)
至于最后一个问题,关于语法错误,这里的问题是 C 被设计为一种“单程”语言,您(和编译器)在这种语言中,您(和编译器)永远不必看得很远就能找出什么是什么.当你尝试这样的事情时:
typedef struct list {
...
List *next; /* ERROR */
} List;
您给编译器的内容太多,无法一次消化。它首先(实际上)忽略typedef 关键字,除了设置更改变量声明方式的标志。这给你留下了:
struct list {
...
List *next; /* ERROR */
}
名称List 根本不可用。尝试使用List *next; 不起作用。最终编译器会到达“变量声明”(并且因为设置了标志,所以将其更改为类型别名),但到那时为时已晚;错误已经发生。
解决方案与函数原型相同:您需要“前向声明”。前向声明会给你一个不完整的类型,直到你完成定义 struct list 部分,但这没关系:C 允许你在许多位置使用不完整的类型,包括当你想要声明一个指针时,包括 @987654415 @ 别名创建。所以:
typedef struct list List; /* incomplete type "struct list" */
struct list { /* begin completing "struct list" */
...
List *next; /* use incomplete "struct list", through the type-alias */
}; /* this "}" completes the type "struct list" */
与到处写struct list 相比,这收获相对较小(它节省了一些打字,但那又怎样?好吧,我们中的一些人遭受了一些腕管/ RSI 问题:-)。
[注意:这最后一段会引起争议……它总是如此。]
事实上,如果你将struct 替换为type,C 代码对于“强类型语言”爱好者来说会变得更好。而不是可怕的 [%],弱酱:
typedef int distance; /* distance is measured in discrete units */
typedef double temperature; /* temperatures are fractional */
他们会写:
#define TYPE struct
TYPE distance;
TYPE temperature;
这些是不完整的类型,确实是不透明的。要使用距离值创建或销毁或确实执行任何操作,您必须调用一个函数(并且——无论如何,对于大多数变量;外部标识符有一些例外——使用指针,唉):
TYPE distance *x = new_distance(initial_value);
increase_distance(x, increment);
use_distance(x);
destroy_distance(x);
没有人会写:
*x += 14; /* 3 inches in a dram, 14 ounces in a foot */
它根本无法编译。
那些对其类型系统的束缚和约束较少的人可以通过完成类型来放松约束:
TYPE distance { int v; };
TYPE temperature { double v; };
当然,现在“骗子”可以做到:
TYPE distance x = { 0 };
x.v += 14; /* 735.5 watts in a horsepower */
(嗯,至少最后一条评论是正确的)。
[% 没那么糟糕,我想。有些人似乎不同意。]