C语言允许用户使用 typedef 关键字自定义数据类型的别名,比如基本类型名称、数组类型名称、指针类型,以及用户自定义的结构型名称、共用型名称、枚举型名称等。在这里只对结构体类型,数组类型,以及指针类型进行说明。
1. typedef结构体
typedef struct list_node_struct
{
int data;
struct list_node_struct* next;
} list_node;
在C语言中使用typedef定义一个结构体的别名,一般是为了在编码时少写一些代码,比如这里,就可以使用list_node node;来代替struct list_node_struct node;这样冗长的代码。
但是在结构体定义的内部,仍需使用struct关键字声明指向结构体自己指针,这是因为此时typedef语句还没有建立完全,list_node暂时还不存在。另外,在结构体的内部,struct list_node_struct也还没完全建立,属于不完整的类型。C语言不允许使用不完整的类型,因为不完整类型的占用空间大小是未知的;但C语言允许声明不完整类型的指针,比如上述的struct list_node_struct* next。这是因为C语言中所有数据类型的指针所占用的存储空间大小都是固定的,所以即使一个指针指向不完整的类型,但它本身却仍是一个完整的类型。于是在这里,在定义一个list_node时C语言编译器就不会为了该申请多少内存而苦恼。
2. typedef数组
typedef list_node list_node_array[]
typedef list_node list_node_t[1];
typedef list_node list_node_array_5[5];
可能会疑惑为什么C语言不能使用typedef list_node[] list_node_array来定义数组类型,因为C语言定义一个数组的语法就是<数据类型> <数组名称>[<数组长度>],而typedef的官方定义是:
Typedef does not work like typedef [type] [new name]. The [new name] part does not always come at the end
You should look at it this way: if [some declaration] [declares] a variable, typedef [same declaration] would define a type.
翻译过来就是:任何声明变量的语句前面加上typedef之后,原来是变量的都变成一种类型,不管这个声明中的标识符号出现在中间还是最后。换句话说,typedef借用了声明变量的语法,使原本的变量名变为了一种类型。不过在C++中可以使用using list_node_t = list_node[1]这样的语法来为“长度为1的list_node数组”取别名,这是因为C++支持list_node* node = new list_node[1]这样的语法。
另外,有部分人认为,定义长度为1的数组只相当于定义了一个变量,失去了数组定义多变量和方便使用并表示变量这一优势,因此毫无实际意义。其实不然,现在假设有一个函数,用于初始化一个list_node,容易想到的版本是:
void init_list_node(list_node* node)
{
if(node != NULL)
{
node->data = 0;
node->next = NULL;
}
}
那么,我们可以使用以下方式来调用这个函数:
int main()
{
list_node node1;
init_list_node(&node1); /* 第1种方式 */
list_node* node2 = (list_node*)malloc(sizeof(list_node));
init_list_node(node2); /* 第2种方式 */
...
free(node2);
return 0;
}
但是,如果改用长度为1的list_node数组类型list_node_t进行变量定义,就可以采用如下编码方式调用init_list_node函数:
int main()
{
list_node_t node;
init_list_node(node);
return 0;
}
由于node是数组名,它代表了该数组首元素的地址,所以node的数据类型兼容init_list_node的参数列表,于是就可以省去了&的编写,提升了代码的可读性。并且由于node存储于栈上,所以不需要操心堆内存的释放问题。
不过使用长度为1的数组有一个明显的缺陷就是数组名称是个常量,它不能被改变。于是下面的语句就不能通过编译:
int main()
{
list_node_t a;
list_node node;
list_node* b = &node;
list_node* c = a; /* 允许 */
a = b; /* 不允许 */
b = a; /* 允许 */
}
另外,长度为1的数组还被用于C99之前的柔性数组成员的替代方案,见[5]。
3. typedef指针
typedef list_node* list_node_ptr;
typedef char* (*write_func)(char*, int, int);
对于第一条语句,可能会觉得使用typedef定义一个新的别名意义不大。但是在复杂指针类型的变量声明中,使用typedef将会非常有意义,比如以下代码:
char* (*write_funcs[5])(char*, int, int);
相当于:
write_func write_funcs[5];
哪种方式更优不言而喻。
不过,要小心使用typedef带来的陷阱:
typedef char* string;
int strcmp(const string, const string);
在上述代码中const string是否相当于const char*呢?答案是否定的。typedef不同于宏定义,它不是简单的字符串替换。所以即使string是char*的别名,编译器也仍会认为string是一个非指针类型。而给一个非指针类型加上const限定符,相当于限制变量本身是只读的。因此,此时相当于定义了一个指针常量,即char* const。
4. typedef是一个存储类关键字
还需要特别注意的是,虽然 typedef 并不真正影响对象的存储特性,但在语法上它还是一个存储类的关键字,就像 auto、extern、static 和 register 等关键字一样。因此,像下面这种声明方式是不可行的:
typedef static int static_i32;
不可行的原因是不能声明多个存储类关键字,由于 typedef 已经占据了存储类关键字的位置,因此,在 typedef 声明中就不能够再使用 static 或任何其他存储类关键字了。当然,编译器也会报错,如在 MSVC 中的报错信息为“不能指定多个存储类”。
参考资料
[3] 不完整类型
[4] Incomplete types