前面咱们把结构体、共用体、枚举都讲完了。今天说个“辅助型”的知识点——typedef。
这东西不复杂,说白了就一句话:给已有的类型起个新名字。 但你别小看它,实际开发里用好了,代码的可读性能上一个台阶。
咱们先从一个最简单的场景说起。
为啥要给类型起别名?
你写代码的时候,肯定遇到过这种类型名:
unsigned long long int
哇,这一长串,每次写都要敲半天,看着也累。而且你想想,如果这个类型代表的是“文件大小”,那写unsigned long long int fileSize远不如写FileSize fileSize来得清楚。
typedef就是让你给类型起个短名字,或者起个更有意义的名字。
语法特别简单:
typedef 已有类型 新名字;
就这么一行,分号结尾。
基本用法:给基本类型起别名
typedef int Age; // Age就是int typedef float Score; // Score就是float typedef unsigned long long int FileSize; // 长名字变短
以后你就能用这些新名字来定义变量了:
Age myAge = 25; // 等价于 int myAge = 25; Score mathScore = 95.5; // 等价于 float mathScore = 95.5; FileSize size = 1024000; // 等价于 unsigned long long int size = ...
你看,Age myAge是不是比int myAge更有意义?一看就知道这个变量存的是年龄。FileSize size一看就知道是文件大小,比unsigned long long int清楚多了。
给结构体起别名——这是最常用的
这个你其实已经见过了。前面讲结构体的时候,咱们用typedef给结构体起了别名:
typedef struct Student { int id; char name[20]; float score; } Student;
没有typedef的时候,你定义变量得写:
struct Student s1; // 必须写struct
加了typedef之后:
Student s1; // 不用写struct了
省了六个字母,代码也干净了不少。
你仔细观察一下typedef定义结构体的语法——它把Student放在了两个地方。第一个Student是结构体的“标签”(tag),第二个Student是typedef起的别名。你完全可以给别名起个不同的名字:
typedef struct Student { int id; char name[20]; float score; } Stu; // 别名是Stu
那以后就用Stu s1;来定义变量。
但通常大家习惯让标签和别名保持一致,看起来更统一。
给指针起别名
指针的类型写起来也挺啰嗦的:
int *p1, *p2; // 两个int指针
如果你要定义很多指向int的指针,可以用typedef简化:
typedef int* IntPtr; // IntPtr就是int* IntPtr p1, p2; // p1和p2都是int指针
注意:IntPtr p1, p2;和int *p1, *p2;等价,但比int *p1, p2;安全得多。因为int *p1, p2;里p2是int,不是指针,容易看错。用IntPtr就清楚多了,两个都是指针。
给数组起别名
数组类型也能起别名:
typedef int IntArray[10]; // IntArray就是一个长度为10的int数组 IntArray a; // 等价于 int a[10];
IntArray a定义了一个长度为10的int数组。注意这里IntArray是类型,不是变量。a才是变量。
这个用法相对少见,你知道有这回事就行。
给函数指针起别名——这个特别有用
函数指针的声明是出了名的难看:
int (*funcPtr)(int, int);
这玩意儿你每次写都得琢磨半天,一不小心就少写个括号,意思全变了。
用typedef可以给它穿件好看的衣服:
typedef int (*FuncPtr)(int, int); // FuncPtr是类型名
现在你可以直接用它来定义函数指针变量了:
FuncPtr p; // 定义了一个函数指针 p = add; // 让它指向add函数 int result = p(3, 5); // 调用
是不是清爽多了?你不再需要每次写那个绕来绕去的int (*p)(int, int)了。
看个完整的例子:
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } typedef int (*Operation)(int, int); // 起个别名 int calculate(int x, int y, Operation op) { return op(x, y); } int main() { printf("%d\n", calculate(10, 5, add)); // 15 printf("%d\n", calculate(10, 5, sub)); // 5 return 0; }
Operation这个别名一出来,代码可读性立马就不一样了。别人一看Operation op就知道这是个操作函数,不用去猜那个长长的函数指针声明是什么意思。
typedef和#define的区别——这是高频面试题
你可能会想:我用#define也能给类型起别名啊,为啥非得用typedef?
#define IntPtr int* IntPtr p1, p2;
看起来IntPtr就是int*,效果应该一样吧?
不一样。 这里有个大坑。
#define是纯粹的文本替换。IntPtr p1, p2;在预处理之后变成了:
int* p1, p2;
这时候p1是int*,但p2是int!因为*只跟p1结合了,p2前面的int是分开的。
而用typedef就没这个问题:
typedef int* IntPtr; IntPtr p1, p2; // p1和p2都是int*
因为IntPtr是一个完整的类型名,编译器会把p1和p2都当成IntPtr类型。
总结区别:
| typedef | #define | |
|---|---|---|
| 处理阶段 | 编译阶段 | 预处理阶段 |
| 本质 | 类型别名 | 文本替换 |
| 类型检查 | 有 | 无 |
| 作用域 | 遵循C语言作用域 | 从定义处到文件末尾 |
| 指针问题 | 安全 | 有坑 |
所以给类型起别名,优先用typedef,别用#define。
一个完整的例子:让链表代码更清爽
咱们用typedef把链表的代码梳理一下:
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; typedef Node* NodePtr; // 给节点指针起个别名 NodePtr create_node(int value) { NodePtr newNode = (NodePtr)malloc(sizeof(Node)); if (newNode == NULL) { printf("内存分配失败\n"); return NULL; } newNode->data = value; newNode->next = NULL; return newNode; } void print_list(NodePtr head) { NodePtr p = head; while (p != NULL) { printf("%d -> ", p->data); p = p->next; } printf("NULL\n"); } int main() { NodePtr head = create_node(10); head->next = create_node(20); head->next->next = create_node(30); print_list(head); // 释放 NodePtr p = head; while (p != NULL) { NodePtr temp = p; p = p->next; free(temp); } return 0; }
你看,NodePtr这个别名让函数签名和变量声明都清楚了不少。NodePtr head比Node* head更明确地表达了“这是一个指向节点的指针”。
几个常见的坑
坑1:typedef后面忘了分号
typedef int Age // 错误!少了分号 typedef int Age; // 正确
坑2:typedef和#define混着用
typedef int* IntPtr; #define IntPtr2 int*
这俩看起来像,但行为不一样,别混。
坑3:给指针起别名时把*写错位置
typedef int* IntPtr; // 正确:IntPtr是int* typedef int *IntPtr; // 也对,空格无所谓 typedef int (*FuncPtr)(); // 函数指针,括号不能省
坑4:在typedef里定义变量
typedef int Age; // Age是类型名,不是变量 Age = 10; // 错误!Age不是变量,不能赋值 Age myAge = 10; // 正确,用Age定义变量
坑5:以为typedef创建了新类型
typedef只是起了个别名,没有创建新类型。typedef int Age之后,Age和int是完全一样的类型,编译器不区分它们。你写Age a = 10;跟int a = 10;在编译器眼里是一回事。
实际开发中的常见用法
在实际项目里,typedef最常见的用法有这么几种:
1. 给结构体起别名,省掉struct关键字
typedef struct { int x; int y; } Point;
2. 给函数指针起别名,简化回调声明
typedef void (*Callback)(int); void register_callback(Callback cb);
3. 定义平台无关的类型
typedef unsigned int uint32; typedef unsigned char uint8;
4. 简化复杂类型声明
typedef int (*Matrix)[10]; // 指向10个int的数组的指针
总结一下:
typedef就一句话:给类型起别名。
核心要点:
-
语法:
typedef 已有类型 新名字; -
给结构体起别名可以省掉
struct关键字 -
给函数指针起别名可以让代码可读性大幅提升
-
跟
#define不同,typedef有类型检查,更安全 -
它不创建新类型,只是起个新名字
typedef在实际开发中特别常用,尤其是大型项目里。你几乎每打开一个C语言项目的头文件,都能看到一堆typedef。学会它,你就能看懂别人写的代码,也能让自己写的代码更清晰。
回去自己动手,把之前写的结构体、链表代码里的类型都用typedef简化一遍。再把那个函数指针的例子用typedef重写,对比一下可读性。练熟了,typedef就是你写代码时的好帮手。
咱们“用户自己建立数据类型”这一章到这儿就全部讲完了。结构体、共用体、枚举、typedef,这四个工具你都有了。以后你写复杂程序的时候,就能根据实际需要,自己“造”出合适的类型来用。