typedef 关键字用于自定义数据类型的一个别名,或者称之为「定义了一种新的数据类型」。它可以有效简化定义一个复杂数据类型的代码实现。

定义一种类型的别名

定义一种类型的别名,而不只是简单的宏替换。例如,可以用作同时声明指针类型的 多个对象

1
char* pa, pb;

不符合意图,它只声明了一个指向字符变量的指针 pa 和一个字符变量pb

1
2
typedef char* PCHAR;
PCHAR pa, pb;

符合意图,同时声明了两个指向字符变量的指针。

1
char *pa, *pb;

也符合意图,但相对来说没有用 typedef 的形式直观,尤其在需要大量指针的地方,typedef 的方式更省事。

为结构体类型对象起别名

typedef 可以用在 struct 结构体中,为声明的 struct 结构体类型的对象起别名。

声明 结构体类型:

1
2
3
4
struct tagPOINT {
int x;
int y;
};

一般结构体变量的定义为 struct struct_name obj_name,如:

1
2
struct tagPOINT stCoor;  // 定义一个结构体变量
struct tagPOINT *pstCoor = NULL; // 定义一个结构体指针变量

定义 结构体类型(而非定义结构体变量):

1
2
3
4
typedef struct tagPOINT {
int x;
int y;
} stPOINT, *pstPOINT;

使用 typedefstruct 声明的结构体类型起别名后,定义该结构体类型的变量时,可以不再书写 struct

1
2
3
stPOINT stCoor;  // 定义一个结构体变量
pstPOINT pstCoor = NULL; // 定义一个结构体指针变量
stPOINT *pstCoor; // 定义一个结构体指针变量

上面既有「声明」,又有「定义」,那声明结构体类型、定义结构体类型和定义结构体变量的区别:

  • 声明结构体类型:只是指定了一个结构体的类型,它相当于一个模型,但其中并无具体数据,系统对之也 不分配实际的内存单元

  • 定义结构体类型:使用 typedefstruct 声明的结构体类型起别名后,即定义了一个结构体类型,但此时 未分配内存单元

    • 这里用定义一词,对应了本文最开始所说的,typedef 关键字用于定义了一种新的数据类型。
  • 定义结构体变量:其中有具体的数据,也为变量分配内存单元。

定义与平台无关的类型

例如,定义一个叫 REAL 的浮点类型,在目标平台上,让它表示最高精度类型:

1
typedef long double REAL;

在不支持 long double 的平台上,改为:

1
typedef double REAL;

在连 double 都不支持的平台上,改为:

1
typedef float REAL;

也就是说,当跨平台时,只要修改 typedef 定义本身就行,不用对其他源码做任何修改。标准库就广泛使用了这个技巧,比如 size_t

另外,因为 typedef 是定义了一种类型的别名,而不是简单的字符串替换,所以它比宏来得稳健(虽然有时候用宏也可以完成以上的用途)。

右左原则

理解、简化复杂声明可用的「右左法则」:从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。

1
int (*func)(void *p);

上面的示例中,

  1. 首先,找到变量名 func,外面有一对圆括号,而且左边是一个* 号,这说明 func 是一个指针;
  2. 然后,跳出这个圆括号,先看右边,又遇到圆括号,这说明 (*func) 是一个函数,所以 func 是一个指向这类函数的指针,即函数指针,这类函数具有 void* 类型的形参,返回值类型是int
1
int (*func[5])(int *);

上面的示例中,

  1. 首先,找到变量名 func,右边是一个[] 运算符,说明 func 是具有 5 个元素的数组;
  2. 然后,遇到圆括号了,调转阅读方向,func的左边有一个 *,说明func 的元素是指针;
    • 注意,这里的 * 不是修饰 func,而是修饰func[5] 的,原因是 [] 运算符优先级比 * 高,func先跟 [] 结合;
  3. 最后,跳出这个括号,看右边,又遇到圆括号,说明 func 数组的元素是函数类型的指针,它指向的函数具有 int* 类型的形参,返回值类型为int

也可以记住这 2 个模式:

  • type (*x)(....) ——— 函数指针
  • type (*x)[] ——— 数组指针

为复杂的声明定义一个新的简单的别名

typedef 为复杂的声明定义一个新的简单的别名的方法是:在原来的声明里逐步用别名替换一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版。

示例一:函数指针数组

我们知道,int *arr[5] 表示 arr 是一个包含 5 个元素的数组,由于 arr 左边是一个 *,所以数组中的每个元素都是一个指针,是一个 int 类型的指针。

原声明:int *(*a[5])(int, char*),变量名为 a

这个声明中,a 也是一个数组,由于 a 左边是一个 *,所以数组中的元素也是指针,是什么指针呢?是一个返回类型为 int*、参数为(int, char*) 的函数指针。

所以,我们可以定义一个新的函数类型,它接受两个参数:一个是 int 类型,另一个是 char* 类型,返回一个 int 类型的指针,即 typedef int *(*pFunc)(int, char*)

那么,原声明就变成了:

1
pFunc a[5];

这里,变量 a 是一个数组,数组中的每个元素都是一个函数指针,这个函数指针的返回类型为int*、参数为(int, char*)

示例二:函数指针数组

原声明:void (*b[10]) (void (*)()),变量名为 b

这个声明也是一个函数指针数组,只不过不像示例一中的函数的入参是 (int, char*),这个示例中函数的入参是另外一个函数指针 void (*)(),它指向的函数没有入参,返回值类型是 void

为了简化声明,我们可以:

  1. 先替换右边部分括号里的,即 typedef void (*pFuncParam)(),其中 pFuncParam 为别名一,定义了一个没有入参、返回值为 void 的函数指针;
  2. 再替换左边的变量 b,即 typedef void (*pFunc)(pFuncParam)pFunc 为别名二,定义了一个入参为 pFuncParam、返回值为 void 的函数指针。

那么,原声明就变成了:

1
pFunc b[10];

这里,变量 b 是一个数组,数组中的每个元素都是一个函数指针,这个函数指针的返回类型为void,参数为一个指向返回类型为void、没有入参的函数指针。

示例三:数组指针

原声明:double (*e)[9],变量名为 e。这是一个指针,它指向一个有 9 个元素的数组,元素类型为 double。这可以简化为 typedef double (*pArr)[9]

1
pArr e;

这里,变量 e 是一个指针,它指向一个长度为 9 的数组,数组的元素类型是 double

示例一、二与示例三是有差别的,前者是数组(数组中的元素是指针),后者是指针(指针指向的是一个数组)。

函数指针和数组指针示例

函数指针

函数指针是指向函数的指针变量,它可以存储函数的地址。通过函数指针可以在程序运行时动态地调用不同的函数。函数指针的声明方式为:data_type (*pointer_var_name)(param_list)

以下是一个函数指针的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

typedef int (*pFunc)(int, int); // 定义一个函数指针类型

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
pFunc ptr = add; // 指向 add 函数
printf("2 + 3 = %d\n", ptr(2, 3)); // 调用 add 函数

ptr = subtract; // 指向 subtract 函数
printf("5 - 2 = %d\n", ptr(5, 2)); // 调用 subtract 函数

return 0;
}

数组指针

数组指针是指向数组的指针变量,它可以存储数组的地址。通过数组指针可以访问数组的元素。数组指针的声明方式为:data_type (*pointer_var_name)[array_length]

以下是一个数组指针的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

typedef int (*pArr)[5]; // 定义一个数组指针类型

int main() {
int arr[5] = {1, 2, 3, 4, 5};
pArr ptr = &arr; // 指向 arr 数组

for (int i = 0; i < 5; i++) {
printf("%d, %d\n", (*ptr)[i], *((int *)ptr + i)); // 通过数组指针访问数组元素
}

return 0;
}

这里 (*ptr) 为什么要解引用?

这是因为,数组变量名 arr 表示的就是数组的首地址,而在 pArr ptr = &arr 中,ptr 表示指向数组首地址的地址(存储数组首地址的地址空间),所以要解引用来获取数组的首地址,才能访问数组中的数据。