qsort:转换比较器函数本身还是比较器函数体中的参数?

jjg*_*jjg 35 c qsort

有几种明显的使用方法qsort:在比较器中强制转换:

int cmp(const void *v1, const void *v2) 
{
    const double *d1 = v1, *d2 = v2;
    ?
}

qsort(p, n, sizeof(double), cmp);
Run Code Online (Sandbox Code Playgroud)

或投射比较器:

int cmp(const double *d1, const double *d2) 
{
    ?
}

qsort(p, n, sizeof(double), (int (*)(const void *, const void *))cmp);
Run Code Online (Sandbox Code Playgroud)

我倾向于使用前者,更多的是出于审美原因。是否有任何技术上的原因偏爱其中一个?

dbu*_*ush 40

您应该避免后一种情况,因为它无效。

两个函数类型要兼容,返回类型必须兼容,对应的参数类型必须兼容。Aconst void *与 a 不兼容,const double *因此函数类型不兼容。通过不兼容的指针类型调用函数会导致未定义的行为

请注意,仅仅因为可以隐式转换两种类型并不意味着它们是兼容的。以const double *and为例const void *,两种类型之间的转换可以在没有强制转换的情况下进行,但是两种类型的表示不必相同。

这意味着 aconst double *传递给函数的方式可能与 a 传递给函数的方式不同 const void *。因此,通过调用类型int (*)(const double*, const double*)为type 的函数,就好像它具有 type 一样int (*)(const void*, const void*),参数可能会以不正确的方式传递。

虽然 x64 和 ARM 系统通常会为所有指针类型使用相同的表示,但您可能会避免使用前者,但仍然无法保证这一点。现代编译器通常会假设未定义的行为不会发生并基于该事实执行优化。

前一种情况是正确的方法,因为函数的签名与qsort函数期望的兼容。

  • 这是一个很好的问题,也是一个很好的答案。它值得好好理解,因为虽然强制转换比较器方法*看起来*一开始是合理的,但如果您考虑编译器将在“qsort”中生成(或*已经*生成)的代码,实际上调用比较器函数,您会看到它正在调用一个带有两个“void *”指针的函数,所以这就是您的比较器函数*必须*的。(如果所有数据指针类型都是“相同的”——当然它们都在当今所有流行的机器上——错误的代码可以工作,但它仍然是错误的。) (10认同)
  • @jjg:看到代码的位数并不表示其符合任何标准或规范。 (7认同)
  • @chux-ReinstateMonica 我不这么认为,因为到 `void *` 的转换不属于默认参数提升。这就是为什么传递给与“%p”格式说明符相对应的“printf”的指针必须显式转换为“void *”。 (7认同)
  • @NateEldredge虽然我从未使用过,但我相信它在字寻址机器上会失败,例如 PR1ME,它有 16 位字指针,但(相当于)18位“char”和“void”指针。[C FAQ 列表](http://c-faq.com/null/machexamp.html) 中有一些关于此的信息。 (4认同)
  • @NateEldredge 在早期的 Cray 上比较 char 肯定会失败,因为 Cray 指针寻址 64 位字并包含额外的内部字段来处理将 8 字节打包为一个字的 char 数据。 (2认同)

Nei*_*eil 12

作为附录,还有另一种调用策略qsort:创建一个中间qsort所需的原型函数,该函数调用启用类型的比较函数。

#include <stdlib.h>
#include <stdio.h>

static int double_cmp(const double *d1, const double *d2)
    { return (*d1 > *d2) - (*d2 > *d1); }

static int double_void_cmp(const void *v1, const void *v2)
    { return double_cmp(v1, v2); }

int main(void) {
    double p[] = { 2.18, 6.28, 3.14, 1.20, 2.72, 0.58, 4.67, 0.0, 1, 1.68 };
    const size_t n = sizeof p / sizeof *p;
    size_t i;
    qsort(p, n, sizeof *p, &double_void_cmp);
    for(i = 0; i < n; i++) printf("%s%.2f", i ? ", " : "", p[i]);
    fputs(".\n", stdout);
    return EXIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)

虽然这有其自身的问题,但可以double_cmp用作其他非qsort事物的比较器。此外,根据我对ISO 9899 6.3.2.3 的解释,它不需要任何强制转换或显式分配,

指向void的指针可以与指向任何不完整或对象类型的指针相互转换。. . 然后再回来。

  • 注意:此技术的编程术语是“thunk”,意思是一个中间函数,它会进行一些细微的调整,以便不兼容的源和目标可以组合在一起 (2认同)

chq*_*lie 8

除了dbush出色的答案之外,还应该注意的是,具有原型的替代比较函数的情况int cmp(const char *s1, const char *s2),例如strcmp不像问题中的那样明确。C 标准规定:

6.2.5 类型

[...] 指向的指针void应具有与指向字符类型的指针相同的表示和对齐要求。类似地,指向兼容类型的限定或非限定版本的指针应具有相同的表示和对齐要求。所有指向结构类型的指针都应具有相同的表示和对齐要求。所有指向联合类型的指针都应具有相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。

因此,函数指针与原型int cmp(const void *v1, const void *v2)int cmp(const char *v1, const char *v2)不兼容的,但调用序列是不太可能是不同的,甚至在那些极为罕见的目标,其中int cmp(const double *v1, const double *v2)将有问题的(早期的Cray系统和缺乏字节寻址的CPU)。


您没有提供比较函数的代码:简单地返回值的差异 ( *d1 - *d2)是一个常见的错误。这不适用于浮点值,也不适用于int值,因为减法可能会溢出。

这是适用于所有数字类型的递增顺序实现:

int cmp(const void *v1, const void *v2) {
    const int *p1 = v1, *p2 = v2;
    return (*p1 > *p2) - (*p1 < *p2);
}
Run Code Online (Sandbox Code Playgroud)

对于浮点类型,可能需要对 NaN 值进行特殊处理:

// sort by increasing values, with NaN after numbers
int cmp(const void *v1, const void *v2) {
    const double *p1 = v1, *p2 = v2;
    if (isnan(*p1)) {
        return isnan(*p2) ? 0 : 1;
    } else
    if (isnan(*p2)) {
        return -1;
    } else {
        return (*p1 > *p2) - (*p1 < *p2);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我喜欢这种“双重”比较处理“NAN”UV - 就像这个[好答案](/sf/answers/3364869091/)。首先把那些“NAN”让开。 (3认同)
  • @pipe:它很可能更多是评论,但是a)由于其长度和包含的代码,它不能很好地作为评论,b)它非常清楚地为该线程的读者提供价值,从而有助于构建偏离已接受的答案。我认为没有理由将其删除为_不是答案_。如果在审查答案时存在自由裁量权的情况,那么这肯定是一个这样的情况;删除它会对未来的读者造成伤害。 (3认同)
  • 这与问题中提出的问题无关,应该是评论或单独的问题。 (2认同)