C語言將tag視做二等公民類型.C++卻沒這麼友好.本文闡述在C/C++中如何給tag頭等公民類型待遇.node
在編程語言中,標識符是最主要的基本元素,被用來對函數,對象,常量,類型等實體命名。程序員
在C/C++裏,一個標識符是由字母或下劃線開頭後接0個或多個字母,下劃線或數字的字符序列。標識符是大小寫敏感的,因此DAN,Dan,dan是不一樣的標識符。編程
在我對C語言的認知裏,C把結構,聯合和枚舉的標識符當作二等公民看待。一個命名告終構,聯合或枚舉的標識符也命名了一個類型,但你無法在typedef中使用這個標識符來表明那個類型。electron
這個月,我研究了這個表面清楚實際很混亂的奇怪行爲,這一樣也存在於C++中。我將給出一種簡單的編程風格來終止這種混亂。編程語言
C中的tag ide
在C裏,出如今下面的名字:函數
struct s { ... };
是一個tag。一個單獨的tag不是一個類型名。假如是的話,C編譯器應該容許以下的聲明:ui
s x; // error in C s *p; // error in C
實際上C編譯器不容許這樣的聲明。你必須以下聲明:spa
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.