带括号的三元表达式在 C 中的函数指针声明中返回函数名称的基本原理

Posted

技术标签:

【中文标题】带括号的三元表达式在 C 中的函数指针声明中返回函数名称的基本原理【英文标题】:Rationale for a parenthesized ternary expression returning a function name in a function pointer declaration in C 【发布时间】:2020-03-22 04:24:13 【问题描述】:

在 K&R2 book 的第 119 页的函数指针部分中,函数指针的参数声明如下:

(int (*)(void*,void*))(numeric ? numcmp : strcmp)

numcmpstrcmp 是函数名,numeric 是一个布尔变量,它决定表达式声明的函数指针指向这两个函数中的哪一个。

我不明白这是如何以及为什么起作用的。如果我试图写这个表达式,我的尝试更像是:

int (*(numeric ? numcmp : strcmp))(void*,void*)

我能理解 K&R 构造的最好方法是,第一个括号部分 - (int (*)(void*,void*)) - 作为函数,第二个 - (numeric ? numcmp : strcmp) - 作为函数参数,整个返回函数指针声明。但这样想与我学到的关于 C 的任何东西都没有联系。

我已经阅读了一些关于如何理解 C 中复杂指针表达式的优秀指南。你基本上是从最里面的表达式向外“螺旋”。但是这个让我很困惑,它不符合要求。有人可以解释一下吗?

【问题讨论】:

相关,您可能需要查看本书该页的勘误表。它确实为由于不同的函数指针参数类型以及滥用名称qsort 而导致的可疑转换道歉,因此可能会使人们对同名但参数大不相同的运行时库函数感到困惑。 @WhozCraig Doh,我在第一次阅读这本书时阅读了该勘误表,但完全忘记了它。不过表达没有错,所以还是注意一下。 【参考方案1】:

(int (*)(void*,void*)) 是一个普通的类型转换

如果我们创建一个类型别名

typedef (int (*function_type)(void*,void*));

使用时可能更容易理解:

(function_type) (numeric ? numcmp : strcmp)

简而言之,三元表达式返回一个指向函数的指针,然后将结果(函数指针)强制转换为特定类型。

【讨论】:

谢谢!如果三元组使用函数名称返回指向函数的指针,那么为什么要转换为指向函数的指针呢?我想这需要让整个演员阵容发挥作用。 请注意,使用结果而不将其转换回正确的函数类型是未定义的行为,并且始终在标准 C 中。大概此代码是预标准化的,或者有更多的上下文来制作它工作。 @Theod'Or 要回答我们需要更多上下文。指针是否分配给变量? strcmp 也没有将 void * 作为参数,结果似乎需要这样做。 @Someprogrammerdude 上下文很简单,表达式是函数参数列表中的函数指针声明。我已接受您的回答,因为它显然是正确的,我的问题中提到的勘误表证明了这一点。 你调用了一个指针 typedef function_type ... 啊。函数类型和指针类型不同,我建议要么调用它 function_pointer_type ,要么实际上使用函数 typedef【参考方案2】:

表达式来自 Brian W Kernighan 和 Dennis M Ritchie 的 p119 The C Programming Language, 2nd Edn (1988)。

它只是将两个函数指针(由三元表达式选择)之一转换为公共类型int (*)(void *, void *),以匹配写在 K&R2 的 p120 上的qsort() 函数变体的签名。

但是,IMO,根据 C 标准,那段代码正式违反了“未定义行为”。

C11 [§6.3 转换]

§6.3.2.3 Pointers ¶8

指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应与原始指针比较。如果转换后的指针用于调用类型与引用类型不兼容的函数,则行为未定义。

您可以在§6.2.7 Compatible type and composite type 和§6.7.6.3 Function declarators (including prototypes) ¶15 中查看对兼容类型的要求。

问题中提到的代码是标准 C qsort() 函数的变体的调用。标准函数具有签名:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

在书中的代码中,他们使用了自己的相关函数qsort(),但签名完全不同:

void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *));

现在,qsort() 变体中的代码将使用两个 void * 值调用由 comp 标识的函数。因此,为了避免未定义的行为,作为比较器传递给 qsort() 的函数应该具有签名:

int comparator(void *p1, void *p2);

现在,代码通过比较器使用:

(int (*)(void *, void *))(numeric ? numcmp : strcmp)

p106 上的strcmp 函数实现与标准C int strcmp(const char *, const char *) 不太匹配;这是他们自己的次要变体,缺少const 限定符(int strcmp(char *, char *))。但是,p119 上的代码包含<string.h>,因此它可能是使用的标准版本。 thenumcmp`函数的签名如下:

int numcmp(char *, char *);

调用中的强制转换是合法的——您可以将函数指针从一种类型转换为另一种类型(然后再转换回来)。不合法的——在最严格的解释中——是它们的qsort() 变体将调用这些函数,就好像它们的类型是int function(void *, void *) 并且标准说“这是未定义的行为”。

此外,§6.5.15 Conditional operator 表示: 两侧的两个表达式必须满足一系列 6 个条件之一,其中相关的一个是:

两个操作数都是指向兼容类型的合格或不合格版本的指针;

现在,鉴于这两个函数都有签名int function(char *, char *),这没关系。如果strcmp() 是标准的C 版本,那么由于const-qualifiers,它就如履薄冰。

鉴于它是自定义的qsort(),并且两个比较器具有相同的签名,因此使用此签名是合理的:

void qsort(void *lineptr[], int left, int right, int (*comp)(char *, char *));

那么在调用qsort() 时就没有必要强制类型了——函数指针参数很简单:

(numeric ? numcmp : strcmp)

并且qsort() 中的代码不需要更改,因为在 C 语言中会自动将 void * 转换为任何其他类型 — 在本例中为 char *

总结

在实践中,您几乎总是会使用 K&R2 中显示的代码。但严格来说,代码调用了未定义的行为,因为它没有将函数指针转换回它们的原始类型。

如果您使用标准 C qsort(),则应始终传递与签名匹配的比较器:

int comparator(const void *p1, const void *p2);

因此,您不应该在调用qsort() 时需要对函数指针进行强制转换,因为qsort() 将使用该签名来调用您的函数。在您的比较器函数中,代码会将两个 const void * 值转换为正确类型的合适 (const) 指针,并使用这些类型运行比较。

【讨论】:

感谢您的详细分析,提供了 Someprogrammerdude 在 cmets 中要求他回答的上下文。 (我在 K&R2 的第 120 页看到,演员阵容实际上已经解释过了,尽管我错过了这一点。)我对你声称书中的做法并不完全标准感到困惑。如果任何函数参数类型都可以转换为void * 并再次转换回来,那么这样的转换不被认为总是兼容的吗?这就是 p120 的第一段似乎暗示的内容。您的 C11 引用指的是函数指针,而不是函数参数指针。我是不是误会了什么? 问题是在qsort() 函数内部,表达式(*comp)(v1, v2) 调用了一个具有签名int function(void *, void *) 的函数。不幸的是,numcmpstrcmp 都没有那个签名(他们有签名int function(char *, char *)),而且这两个不同的签名不兼容。因此,使用“错误”指针类型调用函数是“未定义行为”,这意味着它可能会按预期工作(并且可能会),或者它可能会以编译器认为需要的任何创造性方式失控。 [...继续...] [...continuation...] 例如,它可能决定“这是我的好磁盘驱动器;我会为你格式化它,因为你允许我这样做”(搜索“鼻恶魔”)。它可能不会,但它可以。如果使用修改后的qsort()int (*comp)(char *, char *) 作为比较器,则在调用(*comp)(v1, v2) 中传递的两个void * 值将自动转换为char *,因为函数指针表示参数的类型为char *并且void * 可以转换为char *(这是一个无操作,因为C11 §6.2.5 ¶25)。 我不愿意批评 K&R 在当时(30 多年前)很正常的事情,但是在那个时间框架内,被认为是好的现代 C 语言已经发生了很大变化。这本书在其他地方是当时的产物——例如,用于读取目录数据的大纲实现根本不适用于现代文件系统(Unix 或 Windows)。 另一个问题(但绝对不是“他们的错”)是它们显示了一些版本的函数getline(),现在在 POSIX (getline()) 中是标准的,但它具有完全不同的调用接口和语义。那真不幸;它使使用代码进行练习的人感到困惑。 (这个qsort() 也是有问题的,因为它与标准 C qsort() 的接口不同,正如我在回答和 cmets 质疑中所指出的那样。这是不可原谅的。)不要误会我的意思 K&R2 很好,但它是旧的,它显示。

以上是关于带括号的三元表达式在 C 中的函数指针声明中返回函数名称的基本原理的主要内容,如果未能解决你的问题,请参考以下文章

调用带返回值得函数

javascript 中的函数

C和指针 第十三章 高级指针话题

语法—函数声明

测试数据类型函数typeof( )的用法

C语言-指针