функция 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;
}

Я просто не могу понять, почему многие книги и веб-сайты пишут в такой вводящей в заблуждение манере. Если у вас другое мнение, дайте мне знать.


person 52coder    schedule 05.04.2018    source источник
comment
какая связь с qsort(), переполнение целочисленного сигнала - неопределенное поведение, чего вы ожидали? тоже есть INT_MAX и 1 + INT_MIN переполнение.   -  person Stargateur    schedule 05.04.2018
comment
Я хочу знать, ошибся ли я, я думаю, просто используйте - в сравнении неверно, то, что вы говорите, должно быть переполнением 1 + INT_MAX?   -  person 52coder    schedule 05.04.2018
comment
базовая математика, 1 - (-INT_MIN) == 1 + INT_MIN   -  person Stargateur    schedule 05.04.2018
comment
@Stargateur вы только что ошиблись, 1-INT_MIN = 1+ -INT_MIN = 1 +2147483648, так как INT_MAX = 2147483647, тогда переполнение   -  person 52coder    schedule 05.04.2018
comment
вы правы, использование вычитания для сравнения неверно из-за переполнения, либо приведите к большему типу (длинному), либо используйте стандартный if / else   -  person Iłya Bursov    schedule 05.04.2018
comment
@ 52coder ?????????????????????????????????????????????? ???????????????   -  person Stargateur    schedule 05.04.2018
comment
@Stargateur: 1 + INT_MIN не должно переполняться. 1 + INT_MAX переполняется. INT_MIN - 1 переполняется.   -  person Jonathan Leffler    schedule 05.04.2018
comment
@JonathanLeffler, о, я испортил свое уравнение ... позор мне ^^ кое-как я развиваюсь от INT_MIN до -2147483648, но я сохраняю INT_MIN по какой-то непонятной причине   -  person Stargateur    schedule 05.04.2018
comment
Я видел много книг, которые так пишут - ошибка, кажется, широко распространена, вот пример в коде gcc.   -  person yugr    schedule 05.04.2018


Ответы (5)


Я думаю это неправильно

Да, простое вычитание может привести к 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.

person chux - Reinstate Monica    schedule 05.04.2018
comment
В качестве дополнительного бонуса вычитание целого числа сравнений дает аккуратные значения -1, 0 и 1. - person Antti Haapala; 05.04.2018
comment
Чтобы избежать потенциальных предупреждений компилятора (которые должны быть включены для обнаружения других проблем), вы должны сохранить постоянство аргументов указателя: return (*(const int*)a > *(const int*)b) - (*(const int*)a < *(const int*)b); - person chqrlie; 05.04.2018
comment
@chqrlie Согласен с const-ностью. - person chux - Reinstate Monica; 05.04.2018
comment
Может быть проще использовать: int ca = *(const int *)a; int cb = *(const int *)b; return (ca > cb) - (ca < cb);, теряя больше дефектов. Я сомневаюсь, что есть большая разница, когда оптимизирующий компилятор работает с кодом, но мне это кажется проще. - person Jonathan Leffler; 14.04.2018
comment
@JonathanLeffler Согласен. Я использовал const int *ca = a;, так как думал, что это более пошаговая иллюстрация для OP. Он избегает явного приведения типов. - person chux - Reinstate Monica; 14.04.2018

Ваше понимание абсолютно правильное. Эту общую идиому нельзя использовать для значений 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);
}

Обратите внимание, что этот подход работает для всех числовых типов, но вернет 0 для значений с плавающей запятой NaN.

Что касается вашего замечания: Я просто не могу понять, почему многие книги и веб-сайты пишут в такой вводящей в заблуждение манере:

  • Многие книги и веб-сайты содержат ошибки, как и большинство программ. Многие программные ошибки обнаруживаются и устраняются еще до того, как они попадут в рабочую среду, если программа протестирована с умом. Фрагменты кода в книгах не тестируются, и хотя они никогда не достигают рабочей версии, содержащиеся в них ошибки вирусно распространяются через ничего не подозревающих читателей, которые изучают поддельные методы и идиомы. Очень плохой и продолжительный побочный эффект.

  • Престижность вам за то, что вы это поймали! У вас есть редкий среди программистов навык: вы хороший читатель. Программистов, пишущих код, гораздо больше, чем тех, кто умеет правильно читать код и видеть ошибки. Оттачивайте этот навык, читая чужой код, при переполнении стека или из проектов с открытым исходным кодом ... И сообщайте об ошибках.

  • Метод вычитания широко используется, я видел его во многих местах, как и вы, и он действительно работает для большинства пар значений. Эта ошибка может оставаться незамеченной на долгие годы. Подобная проблема была скрыта в zlib на протяжении десятилетий: int m = (a + b) / 2; вызывает роковое целочисленное переполнение для больших int значений a и b.

  • Автор, вероятно, видел, что это используется, и подумал, что вычитание было крутым и быстрым, и его стоит показать в печати.

  • Обратите внимание, однако, что ошибочная функция работает правильно для типов меньше, чем int: signed или unsigned char и short, если эти типы действительно меньше, чем int на целевой платформе, что стандарт C не требует.

  • Действительно, похожий код можно найти в языке программирования C Брайана. Керниган и Деннис Ричи, знаменитая Библия K&R C, созданная ее изобретателями. Они используют этот подход в упрощенной реализации strcmp() в главе 5. Код в книге датирован концом семидесятых. Хотя он имеет поведение, определяемое реализацией, он не вызывает неопределенного поведения ни в одной, кроме самых редких архитектур, среди которых печально известный DeathStation-9000, но его нельзя использовать для сравнения int значений.

person chqrlie    schedule 05.04.2018

Вы правы, *(int*)a - *(int*)b представляет риск целочисленного переполнения, и его следует избегать как метода сравнения двух int значений.

Возможно, это может быть действительный код в контролируемой ситуации, когда известно, что значения таковы, что при вычитании не произойдет переполнения. Однако в целом этого следует избегать.

person Eric Postpischil    schedule 05.04.2018

Причина, по которой так много неправильных книг, вероятно, является корнем всего зла: книгой 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, код в остальном работает, частично потому, что он полагается на неявное продвижение типа до 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 по этой книге, пишут код, полный неопределенного поведения.

person Lundin    schedule 05.04.2018
comment
Обратите внимание, что эта реализация strcmp() не имеет неопределенного поведения (если sizeof(int) == 1 и char не подписаны, что является патологическим и редким явлением). Правильно, если char по умолчанию без знака, и это можно исправить, если char подписано по умолчанию, изменив последний оператор на return (unsigned char)s[i] - (unsigned char)t[i]; - person chqrlie; 08.04.2018
comment
@chqrlie Фактически, целочисленное продвижение делает тип оператора return int независимо от типа. Хорошо, это не UB, а просто плохо написанный код с опорой на неявные рекламные акции. - person Lundin; 09.04.2018
comment
Да, с возвращаемым типом все в порядке. Даже с преобразованием (unsigned char) каждое значение повышается до int, а разница вычисляется как int и возвращается как таковая. Приведения необходимы для соответствующей реализации strcmp, которая указана в стандарте C для сравнения символов на основе их значений unsigned char. - person chqrlie; 10.04.2018
comment
@chqrlie Где в стандарте указано, что в strcmp символы сравниваются на основе значений символов без знака? И приведение этого не делает из-за упомянутого целочисленного повышения до (подписанного) int. - person Lundin; 10.04.2018
comment
7.24.4 Функции сравнения Знак ненулевого значения, возвращаемого функциями сравнения memcmp, strcmp и strncmp, определяется знаком разницы между значениями первой пары символов ( оба интерпретируются как unsigned char), которые различаются сравниваемыми объектами. Более того, приведение типов делает именно то, что необходимо. Значения char преобразуются в unsigned char, а затем повышаются до int. - person chqrlie; 10.04.2018
comment
@chqrlie Хорошо, я не знал об этом ограничении. Тогда действительно версия K&R не соответствует требованиям. Текст, который вы цитируете из стандарта, вероятно, так же виноват, как и K&R, но, конечно, стандарт изначально мог позаимствовать идею от K&R. - person Lundin; 10.04.2018

Во-первых, конечно, правильно, что целое число во время сравнения может создать для вас серьезные проблемы.

С другой стороны, выполнение одного вычитания дешевле, чем выполнение if / then / else, и сравнение выполняется O (n ^ 2) раз в быстрой сортировке, поэтому, если эта сортировка критична к производительности, и мы можем уйти с его помощью мы можем захотеть использовать разницу.

Он будет работать нормально, пока все значения находятся в некотором диапазоне размеров меньше 2 ^ 31, потому что тогда их различия должны быть меньше. Итак, если все, что генерирует список, который вы хотите отсортировать, будет содержать значения от миллиарда до минус одного миллиарда, вы можете использовать вычитание.

Обратите внимание, что проверка того, что значения находятся в таком диапазоне до сортировки, является операцией O (n).

С другой стороны, если есть вероятность, что может произойти переполнение, вы захотите использовать что-то вроде кода, который вы написали в своем вопросе.

Обратите внимание, что много материала, который вы видите, явно не учитывает переполнение; просто, возможно, этого более ожидаемо в чем-то более очевидном «арифметическом» контексте.

person Daniel McLaury    schedule 05.04.2018
comment
O (n ^ 2) - худший случай и редкость. Среднее значение O (n log n). - person Eric Postpischil; 05.04.2018