“Strlen (s1) - strlen (s2)” ніколи не менше нуля


77

Зараз я пишу програму на С, яка вимагає частого порівняння довжин рядків, тому я написав таку допоміжну функцію:

Я помітив, що функція повертає true, навіть якщо s1вона має меншу довжину, ніж s2. Хтось може пояснити цю дивну поведінку?


27
Це Фортран-66-іш спосіб писати return strlen(s1) > strlen(s2);.
Джонатан Леффлер,

11
@TimThomas: Чому ви пропонуєте нагороду з цього питання? Ви говорите, що йому не приділено достатньо уваги, але, схоже, ви цілком задоволені відповіддю Алекса Локвуда . Не впевнений, що ще потрібно, щоб виграти нагороду! :)
eggyal

11
Це був нещасний випадок, я не знав, що таке щедрість хаха. -_- Начебто ніяково ...
Адріан Монк,

5
Думаю, це добре для Алекса Локвуда, оскільки його чудова відповідь приверне більше уваги ... так що всі голосують за відповідь Алекса Локвуда !! : D
Адріан Монк,

5
Я думаю, що для @TimThomas краще тримати нагороду відкритою до останньої допустимої дати, щоб його питання теж привернуло увагу .. Він несвідомо втратив свої 100 очок репутації, нехай поверне трохи.
Кришнабхадра

Відповіді:


175

Ви стикалися з якоюсь особливою поведінкою, яка виникає в C при обробці виразів, що містять як знакові, так і беззнакові величини.

Коли виконується операція, де один операнд підписаний, а інший - беззнаковий, C неявно перетворює підписаний аргумент у unsigned і виконує операції, вважаючи, що числа невід’ємні. Ця конвенція часто призводить до неінтуїтивної поведінки реляційних операторів, таких як <і >.

Щодо вашої допоміжної функції, зверніть увагу, що оскільки strlenтип size_tповертань (непідписана величина), різниця та порівняння обчислюються за допомогою беззнакової арифметики. Коли s1коротше ніж s2, різниця strlen(s1) - strlen(s2)повинна бути від’ємною, але замість цього стає великим, непідписаним числом, яке більше ніж 0. Таким чином,

повертається, 1навіть якщо s1коротше ніж s2. Щоб виправити свою функцію, використовуйте замість цього код:

Ласкаво просимо до чудового світу С! :)


Додаткові приклади

Оскільки цьому питанню нещодавно приділялося багато уваги, я хотів би навести кілька (простих) прикладів, лише для того, щоб переконатись, що я доношу ідею до кінця. Я припущу, що ми працюємо з 32-розрядною машиною, використовуючи представлення двох доповнень.

Важливою концепцією, яку слід зрозуміти під час роботи зі змінними без знака / підпису в C, є те, що якщо в одному виразі є поєднання безпідписаних та підписаних величин, підписані значення неявно передаються в unsigned .

Приклад №1:

Розглянемо такий вираз:

Оскільки другий операнд без знака, перший імпліцитно передається беззнаку, і, отже, вираз еквівалентний порівнянню,

що, звичайно, хибно. Ймовірно, це не та поведінка, яку ви очікували.

Приклад №2:

Розглянемо наступний код, який намагається підсумувати елементи масиву a, де кількість елементів задається параметром length:

Ця функція призначена для демонстрації того, наскільки легко можуть виникати помилки внаслідок неявного кастингу з підписаного на безпідписаний. Здається цілком природним передавати параметр lengthяк беззнаковий; зрештою, хто коли-небудь захоче використовувати негативну довжину? Критерій зупинки i <= length-1також здається цілком інтуїтивним. Однак при запуску з аргументом, lengthрівним 0, комбінація цих двох дає несподіваний результат.

Оскільки параметр lengthбез знака, обчислення 0-1виконуються з використанням беззнакової арифметики, що еквівалентно модульному додаванню. Результатом є UMax . <=Порівняння також виконується з допомогою беззнакового порівняння, і оскільки будь-яке число менше або дорівнює Umax , порівняння завжди має місце. Таким чином, код спробує отримати доступ до недопустимих елементів масиву a.

Код може бути виправлений, оголосивши lengthйого intабо, змінивши тест forциклу на i < length.

Висновок: Коли слід використовувати без підпису?

Я не хочу стверджувати тут нічого надто суперечливого, але ось деякі правила, яких я часто дотримуюсь, коли пишу програми на мові C.

  • НЕ використовуйте лише тому, що число невід’ємне. Помилки легко зробити, і ці помилки іноді неймовірно тонкі (як показано в Прикладі №2).

  • НЕ використовуйте при виконанні модульної арифметики.

  • НЕ використовуйте, коли використовуєте біти для представлення наборів. Це часто зручно, оскільки дозволяє виконувати логічні зрушення вправо без розширення знака.

Звичайно, можуть бути ситуації, коли ви вирішили піти проти цих "правил". Але найчастіше, дотримуючись цих пропозицій, ваш код стане простішим у роботі та менш схильним до помилок.


46
Ще один прекрасний приклад того, як менше писати, робить програму більш правильною.
Керрек СБ

3
@TimThomas Він повинен транслювати той чи інший спосіб, а кастинг, не підписаний до підписаного, втратить інформацію, тобто половину простору значень.
user207421

7
Строго кажучи, віднімання виконується між двома size_tзначеннями, які гарантовано не мають знака, а беззнакові арифметичні обгортання за модулем відповідної потужності з двох. Єдине місце, де можливе перетворення зі знаком / без знака, - це result > 0частина, де resultє size_tзначення від віднімання двох розмірів.
Джонатан Леффлер

9
Він не кидає , а перетворює . Термін приведення стосується лише явного оператора приведення, що складається з назви типу в дужках. Оператор приведення чітко вказує перетворення; перетворення може бути як явним, так і неявним.
Кіт Томпсон,

2
Я вважаю, що від'ємні цілі числа досить рідкісні в моєму коді, і я використовую протилежний підхід і використовую їх, unsigned intякщо немає певних причин, щоб цього не робити. Це має перевагу в тому, що всі операції є чітко визначеними (навіть "обгортання"), хоча, правда, це може вимагати обережності при вирішенні деяких нерівностей.
Джошуа Грін

25

strlenповертає a, size_tякий є typedefдля unsignedтипу.

Так,

Усі unsignedзначення більше або дорівнюють 0. Спробуйте перетворити змінні, повернуті strlenв long int.


ptrdiff_t - це правильний портативний склад. Загальноприйнятим для long int є 32-розрядне ціле число зі знаком у 64-розрядних системах (у 64-розрядних системах це 64-бітові вказівники). Насправді, як Visual C ++, так і gcc для x86 та x86_64 використовують 32-бітові довжини.
Mr Fooz

3
Я думав, що ptrdiff_tце віднімання покажчиків, а не віднімання size_tзначень ...
Містер Лістер,

4
Не існує типу POSIX для "віднімання size_tзначень"; C визначає це просто, size_tоскільки це цілісний тип, і типи збігаються. Можна стверджувати, що це так off_t, але це насправді стосується зміщення файлів. Тож найкраще, що ви зробите, - це причина того, що оскільки size_tдля зберігання будь-якого індексу, який може обробляти платформа, він також може представляти будь-яке значення покажчика, оскільки воно може використовуватися для індексації байтів з 0. Таким чином, ptrdiff_tмає бути така ж кількість бітів, як і size_t, роблячи це просто signedверсією size_t.
Mike DeSimone

1

Alex Локвуд відповідь є кращим рішенням (компактна, чітка семантикою і т.д.).

Іноді має сенс явно перетворити на підписану форму size_t:, ptrdiff_tнаприклад

Якщо ви зробите це, ви хочете бути впевнені, що size_tзначення відповідає значенню ptrdiff_t(яке має на одну меншу кількість бітів мантиси).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.