C/C++中tag和type

C语言将tag视作二等公民类型.C++却没这么友好.本文阐述在C/C++中如何给tag头等公民类型待遇.

 在编程语言中,标识符是最主要的基本元素,被用来对函数,对象,常量,类型等实体命名。

在C/C++里,一个标识符是由字母或下划线开头后接0个或多个字母,下划线或数字的字符序列。标识符是大小写敏感的,所以DAN,Dan,dan是不同的标识符。

 在我对C语言的认知里,C把结构,联合和枚举的标识符当做二等公民看待。一个命名了结构,联合或枚举的标识符也命名了一个类型,但你没法在typedef中使用这个标识符来代表那个类型。

 这个月,我研究了这个表面清楚实际很混乱的奇怪行为,这同样也存在于C++中。我将给出一种简单的编程风格来终止这种混乱。

C中的tag

 在C里,出现在下面的名字:

struct s
  {
  ...
  };

 是一个tag。一个单独的tag不是一个类型名。假如是的话,C编译器应该允许如下的声明:

s x;  	// error in C
s *p;  	// error in C

实际上C编译器不允许这样的声明。你必须如下声明:

struct s x;  // OK
struct s *p;  // OK

struct后跟s的组合,被称作详细类型说明符(elaborated type specifier).

联合和枚举的名字也是tag而并非类型。例如:

enum day { Sunday, Monday, ... };
...
day today;  	// error
enum day tomorrow;	// OK

这里,enum day就是一个详细类型说明符。

基本上,C不允许在同一作用域中出现重名的个体。例如同一作用域中的如下声明:

int status(); 	// function
int status;  	// object

编译器将指出后一个声明错误。但是C对待tag却不同于其它标识符。C编译器将tag保存在一个符号表中,在概念上(而非物理上)与符号表中其它的标识符分离。因此,C程序员可以在同一作用域中拥有同名的tag和标识符。

例如,C编译器允许:

int status();		// function
enum status { ... };	// enumeration

在同一作用域中。甚至下面的也OK:

struct s s;

这定义了一个类型是struct s的对象s。这样的声明不是好的习惯,可这是C。。。

tag和typedef

很多程序员倾向于把结构体tag当做类型名,所以他们用typedef为tag定义别名。例如:

struct s
  {
  ...
  };
typedef struct s T;

允许你使用T来代替struct s:

T x;	// OK
T *p;	// OK

程序不能定义一个类型为T名字也为T的对象(或函数或枚举常量),例如:

T T;  // error

这很好。

注意:我在gcc 4.4.7上实验发现,T T;这样的定义是可以的。

结构,联合或枚举定义中的tag名是可选的。很多程序员将结构定义合并到typedef中并分配一个tag:

typedef struct
  {
  ...
  } T;

这没问题,除非结构体成员中引用了结构体本身:

struct list_node
  {
  ...
  struct list_node *next;
  };

定义了结构体list_node,其中包含指向另一个list_node的指针。如果你用typedef定义结构体并省略tag,例如:

typedef struct
  {
  ...
  list_node *next; // error
  } list_node;

编译器会报错,因为成员变量next的定义在list_node被声明之前就引用list_node了。

对于引用自身的结构体,我们除了声明一个结构体的tag外别无他法。如果你希望之后使用typedef的名字,你必须同时声明tag和typedef。很多程序员遵循K&R建议的命名习惯。在他们初版的书中,他们为tag选了一个短小,有点隐晦的标识符,并为typedef选了一个长的大写的标识符。如下:

typedef struct tnode
  {
  ...
  struct tnode *left;
  struct tnode *right;
  } TREENODE;

在书的第二版里,他们把TREENode改为Treenode。

我始终不能理解为什么他们为tag和typedef使用不同的名字,使用一个名字也没有不妥:

typedef struct tree_node tree_node;

更进一步,你可以在结构体定义之前而不是之后,提前声明结构体:

typedef struct tree_node tree_node;
struct tree_node
  {
  ...
  tree_node *left;
  tree_node *right;
  };

你无需在声明成员变量时使用关键字struct,诸如left和right之类,引用的是另外一个tree_nodes。

C++中的tag

C++中类的语法是结构和联合语法的扩展。实际上,C++把结构和联合当做类的特殊情况,我也是。

尽管C++标准没有管它们叫tag,类的名字实际上非常像tag。例如,你可以声明class string的对象如下:

class string s;

当然,很少(如果有的话),C++程序员真的这么做。

C++把用户定义类型设计为看起来很像内建类型。内建类型的声明如下:

int n;

不必使用关键字class,所以前面使用了class关键字的s的声明会让提醒你这不是一个内建类型。为此,C++允许你省略class关键字,像使用类型名一样直接使用类名,如:

string s;

再次,C++标准从没使用过tag这个词。在C++中,类和枚举名就是类型名。尽管,有若干条规则用来选出这些类型名并区别对待。我发现继续把类名和枚举名看做tag会更容易理解。

如果你愿意,你可以想象C++自动为每一个tag生成了一个typedef。例如,当编译器遇到了如下的类定义:

class gadget
  {
  ...
  };

会自动生成一个typedef,如同程序定义一样:

typedef class gadget gadget;

不幸的是,这并非完全准确。C++不能为结构,联合或枚举生成这样的typedef,它并不能兼容C的指令。

例如,假设C程序声明了一个函数和一个结构体名字都是query:

int query();
struct query;

再次,这可能是坏的编程行为,但仍然可能真实发生,比如不同作者在不同的文件中定义了同名的函数和结构体。不管怎样,query都能代表函数而struct query代表结构体。

如果C++真的自动为tag生成typedef,那么当你在C++中编译上面这个程序,编译器应该生成:

typedef struct query query;

很不幸,这个类型名会与函数名冲突,程序无法编译通过。

在C++中,tag表现得就像typedef一个名字,除非程序在相同作用域声明了一个跟tag同名的对象,函数,或枚举。在这种情况下,对象,函数或枚举名屏蔽了tag名,程序要想引用tag名只能在tag名前面追加关键字class,struct,union或enum。因此,一个包含如下语句的C程序:

int query();
struct query;

跟在C++下编译的程序行为一致。再次,query代表函数,详细类型说明符struct query代表类型。

防止不小心让非类型名屏蔽tag的方法是有意用一个typedef屏蔽掉tag名。每一次你声明一个tag,你应该也定义一个同名typedef来重命名tag,就像:

typedef class gadget gadget;
class gadget
  {
  ...
  };

这样,如果同一作用域中有同名的函数,对象或枚举常量,你会得到一个编译器的明显错误信息,而不是只能在运行时才能追踪到的隐蔽bug。

推荐准则

我的建议是,采用同一风格来转换tag为typedef。

对每一个tag,在同一作用域中定义一个同名的typedef作为tag的别名。

这种风格在C和C++中都能正常运作。

对每一个class,你可以把typedef定义在类定义前面或后面(C++中的class包括结构和联合)。把typedef放在类定义之前可以让你让你在类中使用typedef的定义,所以这是我推荐的。

对每一个枚举,你必须把typedef放在枚举定义之后,如:

enum day { Sunday, Monday, ... };
typedef enum day day;  // OK

放在前面会触发编译时错误:

typedef enum day day;  // error
enum day { Sunday, Monday, ... };

应当承认,tag名被屏蔽印发错误的进率看起来很小。你可能永远不会遇到这样的问题。但是一旦这样的错误出现就很致命,所以你还是应该使用typedef不管这种错误发生几率有多小。

我不能理解为什么有人允许在同一作用域中让函数或对象名屏蔽类名。这种屏蔽规则在C中是一个失误,本不应该在C++中继续存在。你需要付出更多的编程努力来避免这个问题。

本文作者:Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. He served for many years as secretary of the C++ standards committee. With Thomas Plum, he wrote C++ Programming Guidelines. You can write to him at dsaks@wittengberg.edu.

 

posted @ 2015-05-29 17:05  sirlipeng  阅读(3555)  评论(0编辑  收藏  举报