qsort功能比较困惑我

时间:2018-04-05 03:37:05

标签: c qsort

我看到很多人在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 > 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;
}

我无法弄清楚为什么许多书籍和网站都会以这种误导的方式写作。 如果您有任何不同的观点,请告诉我。

5 个答案:

答案 0 :(得分:7)

  

我认为这是错误的

是的,一个简单的减法可能导致return *(int*)a - *(int*)b; // Potential undefined behavior. 溢出,这是未定义的行为,应该避免。

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

一个常见的习惯用法是减去两个整数比较。各种编译器都认识到这一点并创建了高效良好的代码。 enter image description here也是好形式。

return *a - *b;
  

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

For students in range(0,count_students): print(student_name[student] + " ") print(student_result[student] + "\n") 在概念上很容易理解 - 即使它提供了极端值的错误答案 - 通常学习者的代码省略边缘条件来理解这个想法 - &#34;知道&#34;该值将Preserving const-ness

或者考虑never be large的复杂性。

答案 1 :(得分:3)

您的理解绝对正确。这个常见的习语不能用于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

关于你的评论:我无法弄清楚为什么许多书籍和网站以如此误导的方式写作

  • 许多书籍和网站都存在错误,大多数程序也是如此。如果对程序进行明智的测试,许多编程错误会在它们到达生产之前被捕获并被压扁。书中的代码片段未经过测试,虽然它们从未到达 production ,但它们包含的bug确实通过不知情的读者传播,这些读者学习伪造方法和习语。非常糟糕和持久的副作用。

  • 感谢你抓住这个!你在程序员中有一种罕见的技能:你是一个好读者。编写代码的程序员远远多于能够正确读取代码并发现错误的程序员。通过阅读其他人的代码,堆栈溢出或开源项目来实现此技能......并报告错误。

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

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

  • 但请注意,对于小于intsignedunsigned charshort的类型,错误的功能可以正常工作类型确实小于目标平台上的int,而C标准并未强制要求。

  • 事实上,Brian Kernighan和着名的K&amp; R C圣经Dennis Ritchie在 The C Programming Language 中找到了类似的代码。他们在第5章的strcmp()的简单实现中使用了这种方法。本书中的代码已经过时,一直追溯到七十年代末期。虽然它具有实现定义的行为,但它不会在其中任何最稀有的体系结构中调用未定义的行为,其中臭名昭着DeathStation-9000,但它不应该用于比较int值。

答案 2 :(得分:1)

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

它可能是受控情况下的有效代码,其中人们知道值减法不会溢出。但总的来说,应该避免这种情况。

答案 3 :(得分:1)

这么多书的错误之所以可能是万恶之源:K&amp; 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的人编写的代码充满了未定义的行为,那真是难怪。

答案 4 :(得分:0)

首先,它当然是正确的,比较期间的整数可能会给您带来严重的问题。

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

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

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

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

请注意,您看到的 lot 内容并未明确考虑溢出;它可能会更加期待一种更明显的算术&#34;上下文。