【问题标题】:qsort function compare confused meqsort 函数比较让我困惑
【发布时间】:2018-09-14 18:25:48
【问题描述】:

我看到很多人在 qsort 比较器函数中使用减法。我认为这是错误的,因为在处理这些数字时:int nums[]={-2147483648,1,2,3}; INT_MIN = -2147483648;

int compare (const void * a, const void * b)
{
  return ( *(int*)a - *(int*)b );
}

我写了这个函数来测试:

#include <stdio.h>
#include <limits.h>

int compare (const void * a, const void * b)
{
    return ( *(int*)a - *(int*)b );
}

int main(void)
{
    int a = 1;
    int b = INT_MIN;
    printf("%d %d\n", a,b);
    printf("%d\n",compare((void *)&a,(void *)&b));
    return 0;
}

输出是:

1 -2147483648
-2147483647

但是a &gt; b 所以输出应该是正数。 我见过很多书都是这样写的。我认为这是错误的;处理int类型时应该这样写:

int compare (const void * a, const void * b)
{
    if(*(int *)a < *(int *)b)
        return -1;
    else if(*(int *)a > *(int *)b)
        return 1;
    else 
        return 0;
}

我只是想不通为什么许多书籍和网站以这种误导性的方式写作。 如果您有任何不同的看法,请告诉我。

【问题讨论】:

  • qsort() 的链接是什么,信号整数的溢出是未定义的行为,您期望什么?还有一个INT_MAX1 + INT_MIN 溢出。
  • 我想知道我是不是错了,我认为只是使用 - 比较是错误的,你说的应该是 1+INT_MAX 溢出?
  • 基础数学,1 - (-INT_MIN) == 1 + INT_MIN
  • @Stargateur 你错了,1-INT_MIN = 1+ -INT_MIN = 1 +2147483648 ,因为INT_MAX = 2147483647,然后溢出
  • 你是对的,由于溢出,使用减法进行比较是错误的,要么转换为更大的类型(长),要么使用标准 if/else

标签: c qsort


【解决方案1】:

你是对的,*(int*)a - *(int*)b 存在整数溢出的风险,应该避免作为比较两个 int 值的方法。

它可能是在受控情况下的有效代码,在这种情况下,人们知道值是这样的,因此减法不会溢出。不过一般来说,应该避免。

【讨论】:

    【解决方案2】:

    首先,比较过程中的整数可能会给您带来严重的问题,这当然是正确的。

    另一方面,做单次减法比通过 if/then/else 更便宜,并且在快速排序中比较需要执行 O(n^2) 次,所以如果这种排序对性能至关重要并且我们可以侥幸逃脱我们可能想要使用差异。

    只要所有值都在小于 2^31 的某个范围内,它就可以正常工作,因为它们的差异必须更小。因此,如果生成您想要排序的列表的任何东西都将值保持在十亿到负十亿之间,那么您可以使用减法。

    请注意,在排序之前检查值是否在这样的范围内是一个 O(n) 操作。

    另一方面,如果有可能发生溢出,您可能希望使用类似于您在问题中编写的代码

    请注意,您看到的 lots 内容并未明确考虑溢出;只是在更明显的“算术”上下文中,这可能更令人期待。

    【讨论】:

    • O(n^2) 是最坏的情况并且很少见。平均值为 O(n log n)。
    【解决方案3】:

    我认为是错误的

    是的,简单的减法会导致int 溢出,这是未定义的行为,应该避免。

    return *(int*)a - *(int*)b;  // Potential undefined behavior.
    

    一个常见的习惯是减去两个整数比较。各种编译器都认识到这一点并创建了高效的行为良好的代码。 Preserving const-ness也是不错的形式。

    const int *ca = a;
    const int *cb = b;
    return (*ca > *cb) - (*ca < *cb);
    

    为什么许多书籍和网站以这种误导性的方式写作。

    return *a - *b; 在概念上很容易理解——即使它提供了带有极端值的错误答案——通常学习者代码会省略边缘条件来理解这个想法——“知道”值将never be large

    或者考虑comparing long doubles with regard to NaN 的复杂性。

    【讨论】:

    • 作为一个额外的好处,减去整数比较得到整洁的值-101
    • 为了避免潜在的编译器警告(应该启用它以检测其他问题),您应该保留指针参数的常量:return (*(const int*)a &gt; *(const int*)b) - (*(const int*)a &lt; *(const int*)b);
    • @chqrlie 同意const-ness。
    • 使用起来可能会更简单:int ca = *(const int *)a; int cb = *(const int *)b; return (ca &gt; cb) - (ca &lt; cb);,从而减少了更多的错误。我怀疑当优化编译器用代码完成时是否有很大的不同,但对我来说看起来更简单。
    • @JonathanLeffler 同意。我使用了const int *ca = a;,因为我认为它对 OP 来说更具有分步说明性。它确实避免了显式转换。
    【解决方案4】:

    你的理解是完全正确的。此常用习语不能用于int 值。

    您提出的解决方案可以正常工作,尽管使用局部变量会更具可读性以避免如此多的强制转换:

    int compare(const void *a, const void *b) {
        const int *aa = a;
        const int *bb = b;
        if (*aa < *bb)
            return -1;
        else if (*aa > *bb)
            return 1;
        else 
            return 0;
    }
    

    请注意,无论有没有这些局部变量,现代编译器都会生成相同的代码:总是更喜欢更易读的形式。

    通常使用具有相同精确结果的更紧凑的解决方案,尽管有点难以理解:

    int compare(const void *a, const void *b) {
        const int *aa = a;
        const int *bb = b;
        return (*aa > *bb) - (*aa < *bb);
    }
    

    请注意,此方法适用于所有数字类型,但对于 NaN 浮点值将返回 0

    至于你的评论:我只是想不通为什么许多书籍和网站都以这种误导性的方式写作

    • 许多书籍和网站都包含错误,大多数程序也是如此。如果程序经过明智的测试,许多编程错误在投入生产之前就会被捕获并消除。书中的代码片段未经测试,尽管它们从未达到生产,但它们包含的错误确实会通过学习虚假方法和习语的毫无戒心的读者进行病毒式传播。一个非常糟糕和持久的副作用。

    • 感谢你捕捉到这个!你有一个程序员中难得的技能:你是一个优秀的读者。编写代码的程序员远多于能够正确阅读代码并发现错误的程序员。通过阅读其他人的代码、堆栈溢出或开源项目来磨练这项技能......并报告错误。

    • 减法方法很常用,我在很多地方都看到过,像你一样,它确实适用于大多数值对。这个错误可能会被忽视很久。类似的问题在 zlib 中潜伏了几十年:int m = (a + b) / 2; 导致 int 的大值 ab 导致致命的整数溢出。

    • 作者可能看到它使用了,认为减法很酷而且速度很快,值得在印刷品中展示。

    • 但是请注意,如果这些类型确实小于int目标平台,C 标准没有强制要求。

    • 在 Brian Kernighan 和 Dennis Ritchie 的The C Programming Language 中确实可以找到类似的代码,这本著名的 K&R C 的发明者的圣经。他们在第 5 章中对strcmp() 的简单实现中使用了这种方法。书中的代码已经过时,可以追溯到七十年代后期。尽管它具有实现定义的行为,但它不会在任何架构中调用未定义的行为,除了最罕见的架构,其中包括臭名昭著的 DeathStation-9000,但它不应用于比较 int 值。

    【讨论】:

      【解决方案5】:

      这么多书错的原因很可能是万恶之源:K&R 书。在第 5.5 章中,他们尝试教授如何实现 strcmp

      int strcmp(char *s, char *t)
      {
        int i;
        for (i = 0; s[i] == t[i]; i++)
          if (s[i] == '\0')
            return 0;
        return s[i] - t[i];
      }
      

      此代码有问题,因为char 具有实现定义的签名。忽略这一点,并忽略它们未能像标准 C 版本中那样使用 const 正确性,否则代码可以正常工作,部分原因是它依赖于隐式类型提升到 int (这很难看),部分原因是它们假设为 7 位 ASCII,最坏的情况0 - 127 不能下溢。

      在书的后面,5.11,他们尝试教如何使用qsort

      qsort((void**) lineptr, 0, nlines-1,
        (int (*)(void*,void*))(numeric ? numcmp : strcmp));
      

      忽略此代码调用未定义行为的事实,因为strcmp 与函数指针int (*)(void*, void*) 不兼容,他们教导使用strcmp 中的上述方法。

      但是,看看他们的numcmp 函数,它看起来像这样:

      /* numcmp: compare s1 and s2 numerically */
      int numcmp(char *s1, char *s2)
      {
        double v1, v2;
        v1 = atof(s1);
        v2 = atof(s2);
        if (v1 < v2)
          return -1;
        else if (v1 > v2)
          return 1;
        else
          return 0;
      }
      

      如果atof 发现无效字符(例如., 很可能的语言环境问题),则忽略此代码将崩溃并烧毁的事实,他们实际上设法教授了正确的方法写这样一个比较函数。由于这个函数使用的是浮点数,所以真的没有别的写法了。

      现在有人可能想提出一个int 版本。如果他们基于strcmp 实现而不是浮点实现来做,他们会得到错误。

      总体而言,仅通过在这本曾经的经典书籍中翻几页,我们已经发现了大约 3-4 个依赖于未定义行为的案例和 1 个依赖于实现定义的行为的案例。所以难怪从这本书中学习 C 的人编写的代码中充满了未定义的行为。

      【讨论】:

      • 请注意,strcmp() 的此实现没有未定义的行为(除非 sizeof(int) == 1char 已签名,这是一种病态且罕见的情况)。 char默认无符号是正确的,如果char默认有符号可以通过将最后一条语句改为return (unsigned char)s[i] - (unsigned char)t[i];来修复
      • @chqrlie 实际上,整数提升使得返回语句int 的类型与类型无关。好吧,这不是 UB,只是写得不好的代码,依赖于隐式促销。
      • 是的,返回类型没问题。即使使用(unsigned char) 强制转换,每个值都被提升为int,并且差值被计算为int 并按原样返回。 strcmp 在 C 标准中指定为根据 unsigned char 值比较字符时的一致实现需要强制转换。
      • @chqrlie 标准在哪里说在strcmp 中,字符是根据无符号字符值进行比较的?并且演员不会这样做,因为提到的整数提升到(签名)int
      • 7.24.4 比较函数 比较函数memcmpstrcmpstrncmp返回的非零值的符号由第一对字符(都解释为unsigned char)的值之间的差异符号,它们在被比较的对象中不同。 此外,强制转换完全符合需要。 char 值转换为 unsigned char,然后提升为 int
      猜你喜欢
      • 2014-10-10
      • 1970-01-01
      • 2018-03-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多