Przechowywanie znaku EOF (End of File) w typie char

11

Przeczytałem książkę Dennisa Ritchiego The C Programming Language, której intnależy użyć, aby zmienna mogła przechowywać EOF - aby była wystarczająco duża, aby mogła przechowywać wartość EOF - nie char. Ale następujący kod działa dobrze:

#include<stdio.h> 

main()  { 
  char c; 
  c=getchar(); 
  while(c!=EOF)  { 
    putchar(c); 
    c=getchar(); 
  } 
} 

Gdy nie ma już danych wejściowych, getcharzwraca EOF. W powyższym programie zmienna ctypu char jest w stanie ją z powodzeniem zatrzymać.

Dlaczego to działa? Zgodnie z wyjaśnieniem w powyższej książce kod nie powinien działać.

użytkownik1369975
źródło
5
Ten kod prawdopodobnie zawiedzie, jeśli przeczytasz znak o wartości 0xff. Przechowywanie wynik getchar()w introzwiązuje tego problemu. Twoje pytanie jest zasadniczo takie samo, jak pytanie 12.1 w często zadawanych pytaniach na temat comp.lang.c , które jest doskonałym źródłem informacji. (Również main()powinno być int main(void)i nie zaszkodzi dodać return 0;przed zamknięciem }.)
Keith Thompson
1
@delnan: Powiązany artykuł nie ma racji, jak Unix traktuje Control-D. Nie zamyka strumienia wejściowego; powoduje jedynie, że wszelkie funkcje fread (), które blokują konsolę, natychmiast zwracają wszelkie nieprzeczytane dane. Wiele programów interpretuje zerowy bajt zwracany przez fread () jako wskazujący EOF, ale plik faktycznie pozostanie otwarty i będzie w stanie dostarczyć więcej danych wejściowych.
supercat,

Odpowiedzi:

11

Twój kod wydaje się działać, ponieważ przypadkowe konwersje typów przypadkowo robią to, co należy.

getchar()zwraca an intz wartością, która albo pasuje do zakresu, unsigned charalbo jest EOF(co musi być ujemne, zwykle wynosi -1). Zauważ, że EOFsam nie jest postacią, ale sygnałem, że nie ma już dostępnych znaków.

Podczas przechowywania wynik z getchar()IN c, istnieją dwie możliwości. Albo typ charmoże reprezentować wartość, w którym to przypadku jest to wartość c. Lub typ char nie może reprezentować wartości. W takim przypadku nie jest określone, co się stanie. Procesory Intela po prostu odcinają wysokie bity, które nie pasują do nowego typu (skutecznie zmniejszając wartość modulo 256 dla char), ale nie powinieneś na tym polegać.

Następnym krokiem jest porównanie cz EOF. Jak EOFjest int, czostanie przekonwertowany na intrównież, zachowując wartość przechowywaną w c. Jeśli cmożna zapisać wartość EOF, wówczas porównanie się powiedzie, ale jeśli niec można zapisać wartości, porównanie się nie powiedzie, ponieważ nastąpiła nieodwracalna utrata informacji podczas konwersji na typ .EOFchar

Wygląda na to, że Twój kompilator zdecydował się na charpodpisanie typu, a wartość na EOFtyle mała, aby zmieściła się char. Gdyby charbyły niepodpisane (lub gdybyś użył unsigned char), test nie powiódłby się, ponieważ unsigned charnie można utrzymać wartości EOF.


Pamiętaj również, że jest drugi problem z twoim kodem. Ponieważ EOFnie jest postacią samą w sobie, ale wymusza się na niej chartyp, istnieje bardzo prawdopodobieństwo, że zostanie źle zinterpretowana jako istota, EOFa dla połowy możliwych znaków nie jest zdefiniowane, czy zostaną poprawnie przetworzone.

Bart van Ingen Schenau
źródło
Zmuszanie do wpisania charwartości spoza zakresu CHAR_MIN.. CHAR_MAXbędą wymagane jest albo uzyskując wartość implementacji określone, otrzymując wzór bitowy które definiuje realizacji jako reprezentacji pułapki lub spowodować sygnał realizacji określone. W większości przypadków implementacje musiałyby zostać poddane dodatkowej pracy, aby zrobić coś innego niż redukcja dwóch uzupełnień. Jeśli ludzie w Komitecie ds. Standardów popierają pomysł, że należy zachęcać kompilatory do wdrażania zachowań zgodnych z zachowaniem większości innych kompilatorów, jeśli nie ma powodów, aby robić inaczej ...
supercat
... uznałbym taki przymus za wiarygodny (nie mówiąc, że kod nie powinien dokumentować jego intencji, ale (signed char)xnależy to uznać za jaśniejsze i tak bezpieczne jak ((unsigned char)x ^ CHAR_MAX+1))-(CHAR_MAX+1).) W tej chwili nie widzę żadnego prawdopodobieństwa kompilatory wdrażające wszelkie inne zachowania zgodne z dzisiejszą normą; jedynym niebezpieczeństwem byłoby to, że Standard mógłby zostać zmieniony, aby przerwać zachowanie w domniemanym interesie „optymalizacji”.
supercat,
@ superupat: Standard jest napisany w taki sposób, że żaden kompilator nie musi wytwarzać kodu, którego zachowanie nie jest naturalnie obsługiwane przez procesor, na który jest on kierowany. Większość niezdefiniowanych zachowań istnieje, ponieważ (w momencie pisania standardu) nie wszystkie procesory zachowywały się konsekwentnie. Ponieważ kompilatory stają się coraz bardziej dojrzałe, autorzy kompilatorów zaczęli wykorzystywać niezdefiniowane zachowanie do bardziej agresywnych optymalizacji.
Bart van Ingen Schenau
Historycznie intencją Standardu było głównie to, co opisujesz, chociaż Standard opisuje pewne zachowania wystarczająco szczegółowo, aby wymagać od kompilatorów dla niektórych popularnych platform wygenerowania większej ilości kodu niż byłoby to wymagane w luźniejszej specyfikacji. Przymus typu int i=129; signed char c=i;jest jednym z takich zachowań. Stosunkowo niewiele procesorów ma instrukcję, która byłaby crówna, igdy byłaby w zakresie od -127 do +127 i dawałaby dowolne spójne odwzorowanie innych wartości ina wartości z zakresu od -128 do +127, które różniłyby się od redukcji uzupełnienia dwóch, lub. ..
supercat,
... konsekwentnie podnosi sygnał w takich przypadkach. Ponieważ standard wymaga, aby implementacje albo generowały spójne mapowanie, albo konsekwentnie zwiększały sygnał, jedynymi platformami, na których standard pozostawiałby miejsce na coś innego niż redukcję uzupełnienia dwóch, byłyby rzeczy takie jak procesory DSP ze sprzętem arytmetycznym nasycania. Co do historycznej podstawy niezdefiniowanego zachowania, powiedziałbym, że problem nie dotyczy tylko platform sprzętowych. Nawet na platformie gdzie przelewowy zachowywać się w sposób bardzo konsekwentny, to może być przydatne pułapkę go kompilatora ...
SuperCat