如果你学过多门现代编程语言, 比如 C#, F#, Golang, Rust 这些, 那你一定知道, 目前越来越多的新语言正使用着一种叫做 “类型后置” 的变量与函数声明风格.
何为类型后置
在 C# 中, 声明一个 int 类型, 名为 count 的变量, 其写法如下:
int count;
其中, 类型在前, 名称在后, 这就是类型前置. 而在 Golang 中, 同样的声明就变成了这样:
var count int
此时, 名称在前, 类型灾后, 这就是类型后置. 其它很多种语言都是类型后置, 例如 F#, Rust, Kotlin:
let count : int = 0
let count : i32
var count : Int
不属于类型前置或后置的 C 语言
有很多人都将 C 语言误认为是类型前置, 毕竟 C# 和 Java 这类 “C 族” 语言中, 都是类型前置, 但其实 C 并不是.
在 C 语言中, 声明一个整数型 count 的写法如下:
int count;
怪异的数组
但是, 如果要声明一个整形数组, 语法就开始怪异了起来:
int numbers[10];
在上述代码中, 表示 “数组” 的中括号, 并不在类型旁边, 甚至都不在名称的前面. 比起 “类型”, 这里的中括号更像是对 numbers 这个变量的一种 “修饰”.
而在 C# 中, 声明一个 int 数组, 却是实实在在的类型前置:
int[] numbers;
这里不讨论 C# 的 “本地数组”
怪异的指针
很多人在 C 语言中声明指针类型变量, 都喜欢这么写:
int* pointer;
但其实这并不算最正确的写法. 假如你希望声明两个指针, 如果你像下面这样写的话, 会怎样?
int* pointer1, pointer2;
你会发现, pointer2 根本不是一个指针, 而是一个普普通通的 int. 而上述语法的最正确写法其实是:
int *pointer1, pointer2;
你应该发现了, 表示指针的 “星号” 并没有和 int 结合成一个整体, 并作为后面两个变量的类型, 而是对 pointer1
进行修饰, 使它成为一个指针. 至于 pointer2
, 它没有被修饰, 自然不是指针.
函数指针
C 语言中, 函数指针应该是让很多人都感到头痛的一个东西.
简单指针
下面是一个简单的, 传入两个 int, 传出一个 int 的函数指针:
int (*func)(int, int);
为什么这里 *func
需要使用括号括住呢? 假如我们去掉他, 会发生什么?
int *func(int, int);
它变成了一个函数声明! 这里其实可以理解为, 名称 func
没有优先与星号结合, 而是和后面的括号结合, 变成了函数声明. 之后再和型号结合, 表示此函数返回一个指针. 而最开始的 int, 正是指针的类型.
其实, 在声明函数指针的时候, 我们还可以为参数命名. 与其说 “还可以为参数命名”, 不如说不给参数命名, 其实是省略:
int (*func)(int a, int b);
而删去参数 a
b
这两个名称, 就成了我们经常见到的, 最为精简的函数指针声明.
参数为函数指针的函数指针
那么假如, 我需要声明一个函数指针 func2, 它传入一个参数为两个 int, 返回值为 int 的函数指针, 还传入两个 int, 该怎么写? 大概是这样:
int (*func2)(函数指针, int, int);
然后, 这个函数指针的第一个参数所需要的函数指针, 如果要声明一个这样的变量, 应该是这样:
int (*param1)(int, int);
最后, 我们直接将 param1
直接塞到 func2
的参数列表中, 变成这样:
// int (*func2)(函数指针, int, int);
// 塞入 int (*param1)(int, int);
int (*func2)(int (*param1)(int, int), int, int);
省略掉参数名称 param1
, 就变成了这样:
int (*func2)(int (*)(int, int), int, int);
至此, 声明完成.
返回值为普通指针的函数指针
假如我需要声明一个函数指针, 它传入两个 int, 并返回一个 int 指针, 其声明方式也非常简单.
首先写一个传入两个 int, 传出一个 int 的函数指针:
int (*func)(int, int);
然后使用 * 对声明进行修饰, 表示函数指针的返回值是指针:
int *(*func)(int, int);
声明完毕
返回值为函数指针的函数指针
假如我需要声明一个函数指针, 它传入两个 int, 返回一个函数指针. 返回的函数指针需要传入两个 int, 并返回一个 int.
根据上述说明, 我们可以按照非常简单明了的方式进行编写:
首先, 最终的返回值, 也就是返回的函数指针的返回值, 是 int
int
我们需要声明的是一个指针:
int *func
它是一个函数指针, 并需要传入两个 int:
int (*func)(int, int)
这个函数指针, 返回的也是一个指针:
int *(*func)(int, int)
返回的函数指针, 需要传入两个 int:
int (*(*func)(int, int))(int, int);
至此, 声明完成.
参数和返回值均为函数指针
在上面我们已经有了返回值为函数指针的函数指针了:
int (*(*func)(int, int))(int, int);
我们完全可以把任何一个函数指针的声明加到参数中, 这样它的参数就多了一个函数指针:
int (*(*func)(int (*)(int, int), int, int))(int, int);
当然, 你也可以往返回的函数指针的参数中加函数指针:
int (*(*func)(int (*)(int, int), int, int))(int (*)(int, int), int, int);
现在这个东西看起来已经很复杂了, 其实正常使用根本不会声明这么复杂的函数指针, 就算需要声明复杂的函数指针, 也会借助 typedef
来声明.
类型声明
类型声明其实就是在变量声明前面加 typedef
关键字, 使其变成了类型声明.
简单类型声明
例如, 我们声明一个 int 类型的变量, 名为 count:
int count;
现在给它加上 typedef
关键字:
typedef int count;
现在, count 就表示一个 int.
函数指针类型声明
同理, 如果是函数指针:
int (*func_int_int_int)(int, int);
这里, func_int_int_int 表示一个传入两个 int, 传出一个 int 的函数指针变量
现在给它加上 typedef
关键字:
typedef int (*func_int_int_int)(int, int);
于是 func_int_int_int 就变成传入两个 int, 传出一个 int 的函数指针了.
如果我们需要声明其他的, 传入两个 int, 传出一个 int 的函数指针变量, 当然可以借助刚刚声明好的类型:
func_int_int_int fp1;
func_int_int_int fp2;
也就是说, 变量声明语句, 加上 typedef
关键字, 原来是什么类型的变量, 就变成了什么类型的别名.
函数声明
现在, 我们知道了, 在 C 语言中, 定义复杂类型变量, 其实就是对变量进行不断修饰的一个过程. 那么, 函数呢?
假如我要声明一个无参数, 返回值为整形指针的函数, 应该怎么写? int* func()
还是 int *func()
?
那么最后的答案是… 后者. 至于为什么, 只需要看了这个声明, 你就知道:
int(*func()) {
return 0;
}
尽管我给 *func()
加上了括号, 但是它仍然是一个正确的函数定义. 这意味着, 星号在没有使用括号的情况下, 就是与后面的部分结合的. 而既然它并不是与前面的 int 类型结合, 那在写的时候, 当然也不应该将星号和 int 写在一起.
写在最后
在偶然的机会下, 我和朋友聊到了上面提到的这些东西, 并在之后写下了本篇文章.
毕竟我 C/C++ 的熟练程度其实是远不如 C# 的. 在最初学 C 语言, 看到 C 语言声明各种复杂类型变量的时候, 我的内心是懵逼的. 直到后来, 对编译原理的了解加深了, 再后来, 偶然重新捡起 C/C++ 的这些东西, 才恍然大悟.
其实到现在, 我也有很多疑惑. C 语言的语法解析器, 到底是如何设计, 才会导致函数指针变量声明需要加括号的这种行为呢?
*func()
在不加括号的情况下, func 会向后结合括号, 而没有向前结合星号. 在我看来, 这属实是很奇怪的语法. 不过, 我现在对于语法解析器了解的也不多, 不懂这种行为, 也实属正常.