Pytanie "Strlen (s1) - strlen (s2)" nigdy nie jest mniejsze niż zero


Obecnie piszę program w języku C, który wymaga częstych porównań długości łańcuchów, więc napisałem następującą funkcję pomocnika:

int strlonger(char *s1, char *s2) {
    return strlen(s1) - strlen(s2) > 0;
}

Zauważyłem, że funkcja zwraca wartość true, nawet gdy s1 ma mniejszą długość niż s2. Czy ktoś może wyjaśnić to dziwne zachowanie?


76
2018-05-06 22:19


pochodzenie


To jest fortran-66-owski sposób pisania return strlen(s1) > strlen(s2);. - Jonathan Leffler
@TimThomas: Dlaczego oferujecie nagrodę za to pytanie? Mówisz, że nie otrzymała wystarczającej uwagi, ale wydaje się, że jesteś całkiem zadowolony Odpowiedź Alexa Lockwooda. Nie wiem, ile więcej potrzeba, aby wygrać nagrodę! :) - eggyal
To był wypadek, nie wiedziałem, czym jest bounty. -_- Trochę zawstydzające ... - Adrian Monk
Sądzę, że jest to dobre dla Alexa Lockwooda, ponieważ jego świetna odpowiedź zwróci większą uwagę ... więc wszyscy oddaj głos Alex Lockwood! :RE - Adrian Monk
Myślę, że lepiej dla @TimThomasa pozostawić otwartą nagrodę aż do ostatniej dopuszczalnej daty, aby jego pytanie też przyciągnęło uwagę. Nieświadomie stracił swoje 100 punktów reputacyjnych, niech odzyska trochę czasu. - Krishnabhadra


Odpowiedzi:


Zauważyłeś pewne dziwne zachowanie, które pojawia się w C podczas obsługi wyrażeń zawierających zarówno liczby podpisane, jak i niepodpisane.

Gdy wykonywana jest operacja, w której jeden operand jest podpisany, a drugi nie jest podpisany, C domyślnie przekształci podpisany argument w unsigned i wykona operacje, zakładając, że liczby są nieujemne. Ta konwencja często prowadzi do nieintuicyjnego zachowania dla operatorów relacyjnych, takich jak < i >.

Odnośnie funkcji pomocnika, pamiętaj, że od strlen zwraca typ size_t (liczba niepodpisana), różnica i porównanie są obliczane przy użyciu niepodpisanej arytmetyki. Gdy s1 jest krótszy niż s2, różnica strlen(s1) - strlen(s2) powinien być ujemny, ale zamiast tego staje się dużą, niepodpisaną liczbą, która jest większa niż 0. A zatem,

return strlen(s1) - strlen(s2) > 0;

zwraca 1 choćby s1 jest krótszy niż s2. Aby naprawić swoją funkcję, użyj tego kodu:

return strlen(s1) > strlen(s2);

Witamy w cudownym świecie C! :)


Dodatkowe przykłady

Ponieważ to pytanie ostatnio zyskało wiele uwagi, chciałbym przedstawić kilka (prostych) przykładów, aby upewnić się, że rozumiem. Zakładam, że pracujemy na 32-bitowej maszynie, używając reprezentacji uzupełnienia dwójkowego.

Ważną koncepcją, którą należy zrozumieć podczas pracy ze zmiennymi unsigned / signed w C, jest to jeśli w jednym wyrażeniu występuje kombinacja niepodpisanych i podpisanych wielkości, wartości podpisane są niejawnie rzutowane na niepodpisane.

Przykład 1:

Rozważ następujące wyrażenie:

-1 < 0U

Ponieważ drugi operand jest niepodpisany, pierwszym jest niejawnie rzucić niepodpisane, a zatem wyrażenie jest równoważne porównaniu,

4294967295U < 0U

co oczywiście jest fałszywe. Prawdopodobnie nie jest to zachowanie, którego się spodziewałeś.

Przykład # 2:

Rozważmy następujący kod, który próbuje zsumować elementy tablicy a, gdzie liczba elementów jest podana przez parametr length:

int sum_array_elements(int a[], unsigned length) {
    int i;
    int result = 0;

    for (i = 0; i <= length-1; i++) 
        result += a[i];

    return result;
}

Ta funkcja ma na celu wykazanie, jak łatwo mogą powstawać błędy z powodu niejawnego przesyłania z podpisu na niepodpisany. Wydaje się całkiem naturalne przekazywanie parametrów length jako unsigned; w końcu, kto by chciał użyć ujemnej długości? Kryterium zatrzymania i <= length-1 wydaje się również dość intuicyjne. Jednak po uruchomieniu z argumentem length równy 0, połączenie tych dwóch daje nieoczekiwany wynik.

Ponieważ parametr length jest bez znaku, obliczenia 0-1 jest wykonywany przy użyciu niepodpisanej arytmetyki, która jest równoważna dodaniu modułowemu. Rezultat jest wtedy UMax. The <= porównanie jest również wykonywane przy użyciu niepodpisanego porównania, a ponieważ dowolna liczba jest mniejsza lub równa UMax, porównanie zawsze trzyma. W ten sposób kod spróbuje uzyskać dostęp do nieprawidłowych elementów tablicy a.

Kod można naprawić albo przez zadeklarowanie length być int, lub zmieniając test z for pętla być i < length.

Wniosek: Kiedy należy używać niepodpisanych?

Nie chcę tu podawać niczego zbyt kontrowersyjnego, ale oto niektóre z zasad, których często przestrzegam podczas pisania programów w C.

  • NIE NALEŻY używaj tylko dlatego, że liczba jest nieujemna.


174
2018-05-06 22:21



Kolejny świetny przykład pisania mniej tworzy program jeszcze poprawny. - Kerrek SB
@TimThomas Musi rzutować w jedną lub w drugą stronę, a rzucanie bez znaku do podpisu spowoduje utratę informacji, tj. O połowę wartości. - user207421
Ściśle, odejmowanie odbywa się między dwoma size_t wartości, które są gwarantowane unsigned, i unsigned arithmetic wraps modulo odpowiedniej mocy dwóch. Jedyne miejsce, w którym możliwa jest konwersja podpisana / niepodpisana, znajduje się w result > 0 część, gdzie result jest size_t wartość z odejmowania dwóch rozmiarów. - Jonathan Leffler
Tak nie jest odlew, to nawraca się. Termin odlew odnosi się tylko do jawnego operatora obsady, składającego się z nazwy typu nawiasowanego. Operator rzutowania jawnie określa konwersję; konwersja może być jawna lub domniemana. - Keith Thompson
Znajduję ujemne liczby całkowite wystarczająco rzadkie w moim kodzie, że biorę przeciwne podejście i używam unsigned int chyba że jest ku temu konkretny powód. Ma to tę zaletę, że wszystkie operacje są dobrze zdefiniowane (nawet "zawijane"), choć wprawdzie może wymagać opieki, gdy mamy do czynienia z pewnymi nierównościami. - Joshua Green


strlen zwraca a size_t który jest typedef na unsigned rodzaj.

Więc,

(unsigned) 4 - (unsigned) 7 == (unsigned) - 3

Wszystko unsigned wartości są większe lub równe 0. Spróbuj przekonwertować zmienne zwrócone przez strlen do long int.


25
2018-05-06 22:21



ptrdiff_t jest prawidłową przenośną obsadą. Często w długich intach jest 32-bitowa liczba całkowita ze znakiem w systemach 64-bitowych (w systemach 64-bitowych wskaźniki to 64-bitowe). W rzeczywistości zarówno Visual C ++, jak i gcc dla x86 i x86_64 używają 32-bitowych longów. - Mr Fooz
myślałem ptrdiff_t było dla odejmowania wskaźników, a nie odejmowania size_t wartości ... - Mr Lister
Nie ma typu POSIX dla "odejmowania size_t wartości "; C definiuje to jako proste size_t ponieważ jest to typ integralny i typy są zgodne. Można by twierdzić, że tak off_t, ale tak naprawdę jest dla przesunięć plików. Więc najlepsze, co zrobisz, to powód size_t jest wymagany do przechowywania dowolnego indeksu obsługiwanego przez platformę, może również reprezentować dowolną wartość wskaźnika, ponieważ może być używany do indeksowania bajtów z 0. A zatem ptrdiff_t musi być taka sama liczba bitów jak size_t, dzięki czemu po prostu signed wersja size_t. - Mike DeSimone


Alex Lockwood odpowiedź jest najlepszym rozwiązaniem (zwarta, czysta semantyka, itp.).

Czasami ma sens wyraźne przekonwertowanie na podpisaną formę size_t: ptrdiff_t, np.

return ptrdiff_t(strlen(s1)) - ptrdiff_t(strlen(s2)) > 0;

Jeśli to zrobisz, będziesz chciał mieć pewność, że size_t wartość mieści się w ptrdiff_t (który ma mniej bitów mantysy).


1
2018-06-02 00:26