typedef 只做一件小事:给已经存在的类型起另一个名字。它不会创建新类型,不会分配内存,也不会改变值的任何行为。它是一个命名工具,而在 C 中,内置类型名往往很长(unsigned long long、struct Employee、void (*)(int)),一个好名字价值不菲。
基本形式
它的语法读起来就像在变量声明前面加上了 typedef。原本写变量名的位置,换成新的类型名:
unsigned long count; // declares a variable named count
typedef unsigned long ulong; // declares a TYPE named ulong
规则就这么一条,而它能解释你会读到的所有 typedef,包括本页后面那些看起来很奇怪的。
注意 Celsius/Fahrenheit 这一对没有带来什么:类型安全。两者都只是 double,所以在需要摄氏度的地方传入华氏度的值,照样能顺利编译。这些名字是写给人看的,用来说明意图,并不会产生任何检查。
typedef struct 写法
大多数 C 程序之所以会出现 typedef,就是因为它。在 C 中,结构体的类型名包含关键字:
struct Point { int x; int y; };
struct Point p; /* the word "struct" every single time */
typedef 能把它简化:
用前面的规则来读:去掉 typedef,得到的是 struct Point {...} Point;,即声明一个名为 Point 的变量。把 typedef 加回去,Point 就变成了类型名。
带标签还是不带标签
你可以完全省略标签,得到一个只有 typedef 名字的匿名结构体:
typedef struct {
int x;
int y;
} Point; /* no "struct Point" exists - only "Point" */
这样更简洁,对于普通的数据记录,这是常见的写法。但一旦结构体需要提到自身,它就行不通了,因为 typedef 名字要等它自己的声明结束后才能使用:
/* Does NOT compile: "Node" is not a type yet inside its own braces. */
typedef struct {
int value;
Node *next;
} Node;
保留标签,自引用就能工作,因为一看到标签,struct Node 就可以使用了:
实用规则:保留标签。它只多一个词,和 typedef 名字一致所以不会造成混淆,而且为自引用和头文件中的前向声明留了余地。这个链表节点后来能发展成什么,请看结构体与指针一页。
前向声明与不透明类型
因为即使不知道结构体的内容,指向它的指针大小也是已知的,所以头文件可以只交出一个类型,而不暴露它的成员:
/* stack.h */
typedef struct Stack Stack; /* declared, not defined */
Stack *stack_create(void);
void stack_push(Stack *s, int value);
int stack_pop(Stack *s);
void stack_destroy(Stack *s);
完整的 struct Stack { ... }; 放在 stack.c 中,外部任何代码都无法访问它的成员。这种不透明类型模式是 C 库实现封装的方式,标准库中的 FILE 正是如此:你从 fopen 拿到一个 FILE *,却对它内部是什么一无所知。见文件操作。
枚举和联合体的 typedef
同样的写法也适用于另外两种复合类型:
没有 typedef,它们就得写成 enum Color c; 和 union Number n;。它们的实际用途,请看枚举和联合体。
函数指针的 typedef
到了这里,typedef 就不只是图方便,而几乎是必需的了。函数指针的原始语法出了名地难读:
void (*handler)(int, const char *); /* a variable */
void register_cb(void (*cb)(int, const char *)); /* a parameter */
给类型起一次名字,两者就都变得易读了:
再用一次去掉 typedef 的规则:单独的 int (*IntBinaryOp)(int, int); 会声明一个名为 IntBinaryOp 的变量,它指向一个接收两个 int、返回 int 的函数。加上 typedef,它就成了类型名。更多内容见函数指针一页。
typedef 与 #define
对于简单的别名,它们看起来可以互换,其实不然:
#define INT_PTR int *
typedef int *int_ptr;
INT_PTR a, b; /* expands to: int *a, b; -- b is a plain int! */
int_ptr c, d; /* both c and d are int * */
#define 是在编译器看到代码之前进行的盲目文本替换;typedef 是编译器能理解的真正声明,并且作用于列表中的每个名字。类型用 typedef,#define 留给常量和宏。
typedef 帮倒忙的时候
typedef 也可能隐藏读者需要的信息。最典型的例子就是隐藏指针:
typedef struct Node *NodeRef; /* the star disappears */
NodeRef n = get_node();
看到 NodeRef n,读者无法判断 n 是否可能为 NULL、是否需要释放、n 和它的副本是否共享同一个对象,以及成员要用 . 还是 -> 访问。而 struct Node *n 能立刻回答这四个问题。
标准库也是这么做的:它 typedef 了结构体(FILE),而把星号留在使用的地方(FILE *fp)。由此可以得出两条合理的准则:
- 应该 typedef 结构体、联合体、枚举和函数指针:去掉的那些冗余不携带任何信息。
- 不要只为了缩短而 typedef 指针,也不要 typedef 基本类型来伪装它(
typedef int Boolean;会诱使别人往里面存7,应该用<stdbool.h>中的bool,见布尔值)。
标准库自己在 <stdint.h> 中的定宽类型名(uint32_t、int64_t、size_t)全都是 typedef,它们是这个特性最好的证明:名字准确说明了它们是什么,可移植,而且什么也没有隐藏。
常见问题
C 语言中 typedef 的作用是什么?
它给已有的类型起第二个名字。typedef unsigned long ulong; 表示从此 ulong 和 unsigned long 是同一个类型。它不会创建新类型,也不会分配新的存储空间,只是为已有的类型提供一个更短或更有描述性的写法。
C 语言中的 typedef struct 写法是什么?
typedef struct Point { int x; int y; } Point; 在一条语句中同时声明结构体并把类型命名为 Point,这样你就可以写 Point p; 而不是 struct Point p;。当结构体需要引用自身时(比如链表节点),保留标签(struct Point)就很重要。
typedef 和 #define 有什么区别?
typedef 由编译器处理,创建的是真正的类型别名;#define 是编译之前由预处理器完成的文本替换。这个区别在指针上会出问题:#define PTR int* 会让 PTR a, b; 展开成 int* a, b;,只有 a 是指针。typedef int *PTR; 则让两者都是指针。
C 语言中应该用 typedef 定义指针类型吗?
通常不应该。typedef struct Node *NodeRef; 隐藏了这个类型是指针的事实,读者无法判断一个变量是否可能为 NULL、是否需要释放,或者是否要用 ->。标准库的 FILE * 正是出于这个原因才把星号留在外面。要 typedef 结构体,而不是指向它的指针。