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
表示指向数组首地址的地址(存储数组首地址的地址空间),所以要解引用来获取数组的首地址,才能访问数组中的数据。