[英]qsort function compare confused me
我看到很多人在qsort比較器函數中使用減法。 我認為這是錯誤的,因為在處理這些數字時: int nums[]={-2147483648,1,2,3}; INT_MIN = -2147483648;
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;
}
我無法弄清楚為什么許多書籍和網站以這種誤導的方式寫作。 如果您有任何不同的觀點,請告訴我。
我認為這是錯誤的
是的,一個簡單的減法可能導致int
溢出,這是未定義的行為 ,應該避免。
return *(int*)a - *(int*)b; // Potential undefined behavior.
一個常見的習語是減去兩個整數比較。 各種編譯器都認識到這一點並創建了高效良好的代碼。 保持const
也是很好的形式。
const int *ca = a;
const int *cb = b;
return (*ca > *cb) - (*ca < *cb);
為什么許多書籍和網站都以這種誤導的方式寫作。
return *a - *b;
在概念上很容易消化 - 即使它提供了極端值的錯誤答案 - 通常學習者代碼省略了邊緣條件來理解 - “知道”價值永遠不會很大 。
或者考慮比較long doubles
與NaN的復雜性。
你的理解絕對正確。 這個常用的習慣用法不能用於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
的值a
和b
。
作者可能看到它使用並認為減法很酷而且很快,值得在印刷中顯示。
但請注意,對於小於int
類型,錯誤的函數可以正常工作: signed
或unsigned
char
和short
,如果這些類型確實小於目標平台上的int
,而C Standard並不強制要求。
事實上,類似的代碼可以在Brian Cnighan和Dennis Ritchie 的C編程語言中找到,它是由其發明者着名的K&R C聖經。 他們在第5章的strcmp()
的簡單實現中使用了這種方法。書中的代碼已經過時,一直追溯到七十年代末。 雖然它具有實現定義的行為,但它不會在除了最臭名昭着的體系結構之外的任何一個中調用未定義的行為,其中臭名昭着的DeathStation-9000 ,但它不應該用於比較int
值。
你是對的, *(int*)a - *(int*)b
帶來整數溢出的風險,作為比較兩個int
值的方法應該避免。
它可能是受控情況下的有效代碼,其中人們知道這些值使得減法不會溢出。 但總的來說,應該避免這種情況。
這么多書錯的原因很可能是萬惡之源: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的人編寫的代碼充滿了未定義的行為,那真是難怪。
首先,在比較期間,整數當然是正確的,這可能會給您帶來嚴重的問題。
另一方面,進行單次減法比通過if / then / else更便宜,並且比較在快速排序中執行O(n ^ 2)次,所以如果這種性能至關重要且我們可以逃脫有了它我們可能想要使用差異。
只要所有值都在小於2 ^ 31的某個范圍內,它就能正常工作,因為它們的差異必須更小。 因此,如果生成要排序的列表的任何內容將保持在十億到十億之間的值,那么您可以使用減法。
請注意,在排序之前檢查值是否在這樣的范圍內是O(n)操作。
另一方面,如果溢出可能發生,你會想要使用你在問題中寫的代碼
請注意,您看到的大量內容並未明確考慮溢出; 只是可能在更明顯是“算術”背景的東西中更可取。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.