我看到很多人在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;
}
我无法弄清楚为什么许多书籍和网站都会以这种误导的方式写作。 如果您有任何不同的观点,请告诉我。
答案 0 :(得分:7)
我认为这是错误的
是的,一个简单的减法可能导致return *(int*)a - *(int*)b; // Potential undefined behavior.
溢出,这是未定义的行为,应该避免。
const int *ca = a;
const int *cb = b;
return (*ca > *cb) - (*ca < *cb);
一个常见的习惯用法是减去两个整数比较。各种编译器都认识到这一点并创建了高效良好的代码。 也是好形式。
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;
导致int
和a
的{{1}}值的b
值的命运整数溢出。
作者可能看到它使用并认为减法是酷而且速度快,值得在印刷品中显示。
但请注意,对于小于int
:signed
或unsigned
char
和short
的类型,错误的功能可以正常工作类型确实小于目标平台上的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;上下文。