2145 字
11 分钟
并非类型前置, 有趣的 C 语言变量声明, 函数声明与函数指针声明
2024-10-07

如果你学过多门现代编程语言, 比如 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 会向后结合括号, 而没有向前结合星号. 在我看来, 这属实是很奇怪的语法. 不过, 我现在对于语法解析器了解的也不多, 不懂这种行为, 也实属正常.

并非类型前置, 有趣的 C 语言变量声明, 函数声明与函数指针声明
https://slimenull.com/posts/202410070406/
作者
SlimeNull
发布于
2024-10-07
许可协议
CC BY-NC-SA 4.0