Menu

C 语言 typedef:类型别名、typedef struct 与何时不用

typedef 如何给已有的 C 类型起一个新名字:带标签和不带标签的 typedef struct 写法,函数指针和数组的 typedef,以及 typedef 唯一会帮倒忙的情况:隐藏指针。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 结构体,而不是指向它的指针。

Coddy 编程语言插图

和 Coddy 一起学编程

开始