typedef 关键字用于自定义数据类型的一个别名,或者称之为「定义了一种新的数据类型」。它可以有效简化定义一个复杂数据类型的代码实现。
定义一种类型的别名,而不只是简单的宏替换。例如,可以用作同时声明指针类型的 多个对象。
1 | char* pa, pb; |
不符合意图,它只声明了一个指向字符变量的指针 pa 和一个字符变量pb。
1 | typedef char* PCHAR; |
符合意图,同时声明了两个指向字符变量的指针。
1 | char *pa, *pb; |
也符合意图,但相对来说没有用 typedef 的形式直观,尤其在需要大量指针的地方,typedef 的方式更省事。
typedef 可以用在 struct 结构体中,为声明的 struct 结构体类型的对象起别名。
声明 结构体类型:
1 | struct tagPOINT { |
一般结构体变量的定义为 struct struct_name obj_name,如:
1 | struct tagPOINT stCoor; // 定义一个结构体变量 |
定义 结构体类型(而非定义结构体变量):
1 | typedef struct tagPOINT { |
使用 typedef 为 struct 声明的结构体类型起别名后,定义该结构体类型的变量时,可以不再书写 struct:
1 | stPOINT stCoor; // 定义一个结构体变量 |
上面既有「声明」,又有「定义」,那声明结构体类型、定义结构体类型和定义结构体变量的区别:
声明结构体类型:只是指定了一个结构体的类型,它相当于一个模型,但其中并无具体数据,系统对之也 不分配实际的内存单元。
定义结构体类型:使用 typedef 为 struct 声明的结构体类型起别名后,即定义了一个结构体类型,但此时 未分配内存单元。
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); |
上面的示例中,
func,外面有一对圆括号,而且左边是一个* 号,这说明 func 是一个指针;(*func) 是一个函数,所以 func 是一个指向这类函数的指针,即函数指针,这类函数具有 void* 类型的形参,返回值类型是int。1 | int (*func[5])(int *); |
上面的示例中,
func,右边是一个[] 运算符,说明 func 是具有 5 个元素的数组;func的左边有一个 *,说明func 的元素是指针;
* 不是修饰 func,而是修饰func[5] 的,原因是 [] 运算符优先级比 * 高,func先跟 [] 结合;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。
为了简化声明,我们可以:
typedef void (*pFuncParam)(),其中 pFuncParam 为别名一,定义了一个没有入参、返回值为 void 的函数指针;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 |
|
数组指针是指向数组的指针变量,它可以存储数组的地址。通过数组指针可以访问数组的元素。数组指针的声明方式为:data_type (*pointer_var_name)[array_length]。
以下是一个数组指针的示例:
1 |
|
这里 (*ptr) 为什么要解引用?
这是因为,数组变量名 arr 表示的就是数组的首地址,而在 pArr ptr = &arr 中,ptr 表示指向数组首地址的地址(存储数组首地址的地址空间),所以要解引用来获取数组的首地址,才能访问数组中的数据。