解读 C 语言中复杂类型的秘诀

C/C++ Feb 13, 2020

C 语言虽然没有强大的现代编程模型,缺少面向对象工具,也没有函数式编程。但是 C 在自定义复杂类型的能力方面,丝毫不逊色于任何其他编程语言。

我们来试水几个复杂类型的定义,看看你能不能准确的识别这是什么类型?

数组

int a[3]

这很简单,是一个具有三个 int 元素的数组

int (a[3])[4]

第一眼能看出来吗? 没错,这就是普通的二维数组,等价于 int a[3][4] 声明,现在你可能已经看出来了,C 在自定义类型上具有意想不到的灵活性。

我们继续……

int *a[3][4]int *(a[3][4])int (*a[3])[4] 三者有什么区别吗?

前两者是相同的,第三个表达式则有所不同,C 语言中的类型解析,会根据运算符的结合顺序而定。

对于 int *a[3][4] 解析来说,会优先根据 [3] 识别出 a  是一个具有 3 个元素的数组,再根据 [4] 识别出 a[3] 每个数组元素是一个具有4个元素的数组(也就是二维数组), 然后通过 * 识别出 a 是一个具有 3x4 个元素的指针数组,最后通过 int 识别出 a 是一个 3x4 个元素的 int 类型指针数组。

对于 int *(a[3][4]) 来说,由于 [] 的优先级比 * 高,所以并不影响 C 解析类型的顺序,所以和前者是等价的。

对于 int (*a[3])[4] 来说,就有点不同了 C 会先解析 [3],识别 a 是一个具有 3个元素的数组,然后是 * (区别就在这里了),这时会将 a 识别为具有三个元素的指针数组,下面再结合 [4],识别 a 是一个具有三个指针元素的数组,每个指针元素指向了一个具有4个元素的数组,最后结合 int,识别出a 是一个具有三个指针元素的数组,切每个元素指向了一个具有4个int 类型元素的数组。访问元素的方式是这样的 int a0 = (*a[2])[2],这样你应该就能彻底明白这到底是个什么类型了吧?

现在相信你应该有所感悟了吧,C 并不是想象中的“如此简单”,相反,C 的类型还是相当丰富的,不然 Objective-C 也不会在 C 基础之上扩展出来了。but,这只是一个开始。如果再结合上函数指针,识别难度会急剧上升。

函数指针

void (*func)(void)void *func(void)

对于前者这就是一个函数指针变量。

C 识别的过程是这样的,按照从方向左到右,优先级从高到低的顺序来,首先从 *func 开始识别,func 是一个指针,然后 (void) func 指针指向一个函数,最后到 void ,func 是一个指向无参数,无返回值的函数指针。

对于后者,这是一个返回任意类型指针的无参函数。

void (*func(void))(int)

这是一个很复杂的类型定义,估计大部分人第一眼就已经晕倒了,反正我第一次看到就被酸爽哽噎了。

还是同样的识别方式:运算符的结合顺序,优先级从高到低。

由于这里的 () 优先级比 * 要高,所以首先识别 (void) ,这时我们知道该变量是一个无参数的函数,下一步 func 识别出 func 是一个无参函数,然后通过 * 识别出 func 是一个无参的函数指针,然后通过 (int) 识别出是一个 int 参数的函数,最后通过 void 识别出 func 是一个无参函数指针,返回的是一个没有返回类型,具有一个int 参数的函数。

第一步扫描得到:
void : 无返回值 , *func(void) : 无参函数返回值是指针类型,(int) :int参数 函数
第二步扫描得到:
func 是一个无参数的函数,返回类型是一个具有 int 参数的无返回值函数指针

总结

C 中只要和指针,函数指针扯上点关系,类型声明就变得复杂起来了,但是只要掌握了识破它的秘密武器:运算符的结合顺序和优先级,也并不是难事。

附上一张 C 语言中运算符的优先级和结合顺序,希望文章对大家有启发,也请指正。

运算符操作数类型结合顺序()NA从左到右^*+-void一元运算符从右到左* /二元运算符从左到右+ -二元运算符从左到右=一元运算符从右到左

Nicholas X.

山穷水复疑无路,柳暗花明又一村