do
Litera „x” została utracona w pliku. Napisano program, aby go znaleźć:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE* fp = fopen("desert_file", "r");
char letter;
char missing_letter = argv[1][0];
int found = 0;
printf("Searching file for missing letter %c...\n", missing_letter);
while( (letter = fgetc(fp)) != EOF ) {
if (letter == missing_letter) found = 1;
}
printf("Whole file searched.\n");
fclose(fp);
if (found) {
printf("Hurray, letter lost in the file is finally found!\n");
} else {
printf("Haven't found missing letter...\n");
}
}
Został skompilowany i uruchomić, a na koniec krzyknął:
Hurray, letter lost in the file is finally found!
Przez wiele lat listy były ratowane w ten sposób, dopóki nie pojawił się nowy facet i nie zoptymalizował kodu. Znał typy danych i wiedział, że lepiej jest używać niepodpisanego niż podpisanego dla wartości nieujemnych, ponieważ ma szerszy zakres i zapewnia pewną ochronę przed przepełnieniem. Więc zmienił int na int bez znaku . Znał również ascii wystarczająco dobrze, aby wiedzieć, że zawsze mają one wartość nieujemną. Więc zmienił także char na niepodpisany char . Skompilował kod i wrócił do domu dumny ze swojej dobrej roboty. Program wyglądał następująco:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
FILE* fp = fopen("desert_file", "r");
unsigned char letter;
unsigned char missing_letter = argv[1][0];
unsigned int found = 0;
printf("Searching file for missing letter %c...\n", missing_letter);
while( (letter = fgetc(fp)) != EOF ) {
if (letter == missing_letter) found = 1;
}
printf("Whole file searched.\n");
fclose(fp);
if (found) {
printf("Hurray, letter lost in the file is finally found!\n");
} else {
printf("Haven't found missing letter...\n");
}
}
Wrócił do spustoszenia następnego dnia. Brakowało litery „a” i mimo, że miała znajdować się w „pliku_pustynnym” zawierającym „abc”, program szukał go na zawsze, drukując tylko:
Searching file for missing letter a...
Zwolnili faceta i wycofali się do poprzedniej wersji, pamiętając, że nigdy nie należy optymalizować typów danych w działającym kodzie.
Ale jakiej lekcji powinni się tutaj nauczyć?
Po pierwsze, jeśli spojrzysz na tabelę ascii, zauważysz, że nie ma EOF. Wynika to z faktu, że EOF nie jest znakiem, ale specjalną wartością zwróconą przez fgetc (), która może zwracać znak rozszerzony do int lub -1 oznaczający koniec pliku.
Tak długo, jak używamy podpisanego znaku, wszystko działa dobrze - znak równy 50 jest rozszerzany przez fgetc () na liczbę całkowitą równą 50. Następnie przekształcamy go z powrotem na char i nadal mamy 50. To samo dzieje się z -1 lub dowolnym innym wyjściem pochodzącym z fgetc ().
Ale zobacz, co się stanie, gdy użyjemy niepodpisanego znaku. Zaczynamy od znaku w fgetc (), rozszerz go na int, a następnie chcemy mieć znak bez znaku. Jedynym problemem jest to, że nie możemy zachować -1 w niepodpisanym znaku. Program przechowuje go jako 255, który nie jest już równy EOF.
Zastrzeżenie
Jeśli spojrzysz na sekcję 3.1.2.5 Typy w kopii dokumentacji ANSI C , przekonasz się, że to, czy znak jest podpisany, czy nie, zależy wyłącznie od implementacji. Więc facet prawdopodobnie nie powinien zostać zwolniony, ponieważ znalazł bardzo podstępny błąd czający się w kodzie. Może pojawić się podczas zmiany kompilatora lub przejścia do innej architektury. Zastanawiam się, kto zostałby zwolniony, gdyby błąd wyszedł w takim przypadku;)
PS. Program został zbudowany wokół błędu wymienionego w Paul Assembly Language przez Paula A. Cartera