Dlaczego jest scanf
źle?
Głównym problemem jest to, że scanf
nigdy nie miał on na celu radzenia sobie z wkładem użytkownika. Jest przeznaczony do stosowania z „idealnie” sformatowanymi danymi. Cytuję słowo „doskonale”, ponieważ nie jest to do końca prawda. Ale nie jest przeznaczony do analizowania danych, które są tak niewiarygodne jak dane wejściowe użytkownika. Z natury wkład użytkownika nie jest przewidywalny. Użytkownicy źle rozumieją instrukcje, literówki, przypadkowo naciśnij enter przed ich wykonaniem itp. Można rozsądnie zapytać, dlaczego funkcja, której nie należy używać do wprowadzania danych przez użytkownika stdin
. Jeśli jesteś doświadczonym użytkownikiem * nix, wyjaśnienie nie będzie niespodzianką, ale może dezorientować użytkowników systemu Windows. W systemach * nix bardzo często buduje się programy działające za pomocą pipingu,stdout
stdin
drugiego. W ten sposób możesz upewnić się, że dane wyjściowe i dane wejściowe są przewidywalne. W tych okolicznościach scanf
faktycznie działa dobrze. Ale pracując z nieprzewidywalnymi danymi wejściowymi, ryzykujesz różnego rodzaju kłopoty.
Dlaczego więc nie ma łatwych w użyciu standardowych funkcji wprowadzania danych przez użytkownika? Można się tylko zgadywać, ale zakładam, że starzy hakerzy C po prostu uważali, że istniejące funkcje są wystarczająco dobre, nawet jeśli są bardzo niezręczne. Ponadto, patrząc na typowe aplikacje terminalowe, bardzo rzadko odczytują one dane wejściowe użytkownika stdin
. Najczęściej przekazujesz wszystkie dane wejściowe użytkownika jako argumenty wiersza poleceń. Jasne, są wyjątki, ale w przypadku większości aplikacji wkład użytkownika jest bardzo drobny.
Więc co możesz zrobić?
Mój ulubiony jest fgets
w połączeniu z sscanf
. Kiedyś napisałem odpowiedź na ten temat, ale ponownie opublikuję cały kod. Oto przykład z przyzwoitym (ale nie doskonałym) sprawdzaniem i analizowaniem błędów. Jest wystarczająco dobry do celów debugowania.
Uwaga
Nie lubię szczególnie prosić użytkownika o wprowadzenie dwóch różnych rzeczy w jednym wierszu. Robię to tylko wtedy, gdy należą do siebie w naturalny sposób. Jak na przykład, printf("Enter the price in the format <dollars>.<cent>: ")
a następnie użyj sscanf(buffer "%d.%d", &dollar, ¢)
. Nigdy bym czegoś takiego nie zrobił printf("Enter height and base of the triangle: ")
. Głównym celem użycia fgets
poniżej jest hermetyzacja danych wejściowych, aby upewnić się, że jedno wejście nie wpływa na następne.
#define bsize 100
void error_function(const char *buffer, int no_conversions) {
fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
fprintf(stderr, "%d successful conversions", no_conversions);
exit(EXIT_FAILURE);
}
char c, buffer[bsize];
int x,y;
float f, g;
int r;
printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);
printf("You entered %d %d %f %c\n", x, y, f, c);
Jeśli wykonasz wiele z nich, mogę polecić utworzenie opakowania, które zawsze będzie opróżniać:
int printfflush (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
fflush(stdout);
va_end (arg);
return done;
}```
Takie postępowanie wyeliminuje powszechny problem, którym jest końcowy znak nowej linii, który może zepsuć się z danymi wejściowymi gniazda. Ale ma inny problem, a mianowicie, jeśli linia jest dłuższa niż bsize
. Możesz to sprawdzić za pomocą if(buffer[strlen(buffer)-1] != '\n')
. Jeśli chcesz usunąć nowy wiersz, możesz to zrobić za pomocą buffer[strcspn(buffer, "\n")] = 0
.
Zasadniczo radziłbym nie oczekiwać, że użytkownik wprowadzi dane wejściowe w dziwnym formacie, który należy przeanalizować z różnymi zmiennymi. Jeśli chcesz przypisać zmienne height
i width
, nie pytaj o oba jednocześnie. Pozwól użytkownikowi nacisnąć klawisz Enter między nimi. Podejście to jest bardzo naturalne w pewnym sensie. Nigdy nie otrzymasz danych wejściowych, stdin
dopóki nie naciśniesz Enter, więc dlaczego nie zawsze przeczytać całą linię? Oczywiście może to nadal prowadzić do problemów, jeśli linia jest dłuższa niż bufor. Czy pamiętam, że wspomniałem, że dane wejściowe użytkownika są nieporadne w C? :)
Aby uniknąć problemów z liniami dłuższymi niż bufor, możesz użyć funkcji, która automatycznie przydziela bufor o odpowiednim rozmiarze, możesz użyć getline()
. Wadą jest to, że będziesz musiał free
później uzyskać wynik.
Przyspieszenie gry
Jeśli poważnie myślisz o tworzeniu programów w C przy pomocy danych wejściowych, polecam zajrzeć do biblioteki takiej jak ncurses
. Ponieważ wtedy prawdopodobnie chcesz również tworzyć aplikacje z pewną grafiką terminali. Niestety, jeśli to zrobisz, stracisz trochę przenośności, ale daje to znacznie lepszą kontrolę nad danymi wejściowymi użytkownika. Na przykład umożliwia natychmiastowe odczytanie naciśnięcia klawisza zamiast oczekiwania na naciśnięcie klawisza Enter.
(r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
nie wykrywa tak źle końcowego tekstu nieliczbowego.fgets()
z"1 2 junk"
,if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {
nie zgłasza nic złego wejścia mimo że ma „śmieci”.scanf
jest przeznaczony do użycia z idealnie sformatowanymi danymi Ale nawet to nieprawda. Oprócz problemu z „śmieciami”, o którym wspomniał @chux, istnieje również fakt, że format podobny"%d %d %d"
chętnie odczytuje dane wejściowe z jednego, dwóch lub trzech wierszy (lub nawet więcej, jeśli występują przeszkadzające puste wiersze), że nie ma sposób wymuszenia (powiedzmy) wejścia dwuwierszowego poprzez wykonanie czegoś podobnego"%d\n%d %d"
itp.scanf
może być odpowiedni dla sformatowanego wejścia strumieniowego , ale w ogóle nie jest dobry dla niczego opartego na linii.scanf
jest niesamowity, gdy wiesz , że Twój wkład jest zawsze dobrze skonstruowany i dobrze wychowany. Inaczej...IMO, oto największe problemy z
scanf
:Ryzyko przepełnienia bufora - jeśli nie określisz szerokości pola dla specyfikatorów
%s
i%[
specyfikatorów konwersji, ryzykujesz przepełnieniem bufora (próba odczytania większej ilości danych wejściowych, niż rozmiar bufora ma pomieścić). Niestety, nie ma dobrego sposobu na określenie tego jako argumentu (tak jak w przypadkuprintf
) - musisz albo zakodować go na stałe w ramach specyfikatora konwersji, albo wykonać kilka makr shenaniganów.Akceptuje dane wejściowe, które powinny zostać odrzucone - jeśli czytasz dane wejściowe za pomocą
%d
specyfikatora konwersji i wpisujesz coś w stylu12w4
, można oczekiwać,scanf
że dane wejściowe zostaną odrzucone, ale tak nie jest - pomyślnie konwertuje i przypisuje12
, pozostawiającw4
w strumieniu wejściowym zepsuć następny odczyt.Czego więc powinieneś użyć?
Zwykle zalecam czytanie wszystkich interaktywnych danych wejściowych jako tekstu
fgets
- pozwala określić maksymalną liczbę znaków do odczytania na raz, dzięki czemu można łatwo zapobiec przepełnieniu bufora:Jedną z dziwnych
fgets
rzeczy jest to, że zapisze końcowy znak nowej linii w buforze, jeśli jest miejsce, dzięki czemu możesz łatwo sprawdzić, czy ktoś wpisał więcej danych wejściowych niż się spodziewałeś:Sposób, w jaki sobie z tym poradzisz, zależy od ciebie - możesz odrzucić cały wkład z ręki i przykuć wszelkie pozostałe dane za pomocą
getchar
:Lub możesz przetworzyć dane wejściowe, które masz do tej pory i przeczytać ponownie. To zależy od problemu, który próbujesz rozwiązać.
Aby tokenizować dane wejściowe (podzielić je na podstawie jednego lub więcej ograniczników), możesz użyć
strtok
, ale uważaj -strtok
modyfikuje dane wejściowe (zastępuje ograniczniki ciągiem znaków) i nie możesz zachować ich stanu (tzn. Możesz „ t częściowo tokenizuj jeden ciąg, a następnie zacznij tokenizować inny ciąg, a następnie wybierz miejsce, w którym przerwałeś oryginalny ciąg). Istnieje wariant,strtok_s
który zachowuje stan tokenizera, ale AFAIK jego implementacja jest opcjonalna (musisz sprawdzić, czy__STDC_LIB_EXT1__
jest zdefiniowany, aby zobaczyć, czy jest dostępny).Po tokenizowaniu danych wejściowych, jeśli chcesz przekonwertować ciągi na liczby (tj.
"1234"
=>1234
), Masz opcje.strtol
istrtod
przekonwertuje ciąg znaków liczb całkowitych i liczb rzeczywistych na odpowiadające im typy. Pozwalają również uchwycić12w4
problem, o którym wspomniałem powyżej - jednym z ich argumentów jest wskaźnik do pierwszego znaku nie przekonwertowanego w ciągu:źródło
%*[%\n]
Co jest przydatne w przypadku długich linii w dalszej części odpowiedzi).snprintf()
),.isspace()
- akceptuje znaki niepodpisane reprezentowane jakoint
, więc musisz rzucić,unsigned char
aby uniknąć UB na platformach, na którychchar
jest podpisany.W tej odpowiedzi założę, że czytasz i interpretujesz linie tekstu . Być może monitujesz użytkownika, który coś pisze i naciska klawisz RETURN. A może czytasz wiersze tekstu strukturalnego z jakiegoś pliku danych.
Ponieważ czytasz wiersze tekstu, warto uporządkować kod wokół funkcji bibliotecznej, która odczytuje, no cóż, wiersz tekstu. Standardowa funkcja jest
fgets()
, chociaż istnieją inne (w tymgetline
). A następnie następnym krokiem jest jakoś zinterpretować ten wiersz tekstu.Oto podstawowy przepis na dzwonienie w
fgets
celu odczytania wiersza tekstu:To po prostu czyta jeden wiersz tekstu i drukuje go z powrotem. Jak napisano, ma kilka ograniczeń, do których dojdziemy za chwilę. Ma także bardzo dobrą funkcję: liczba 512, którą przekazaliśmy jako drugi argument,
fgets
to rozmiar tablicyline
, wfgets
której czytamy. Ten fakt - że możemy powiedzieć,fgets
ile można odczytać - oznacza, że możemy być pewni, żefgets
nie przepełni tablicy, wczytując w nią zbyt wiele.Teraz wiemy, jak odczytać wiersz tekstu, ale co, jeśli naprawdę chcielibyśmy odczytać liczbę całkowitą, liczbę zmiennoprzecinkową, pojedynczy znak lub pojedyncze słowo? (To znaczy, co jeśli
scanf
wezwanie staramy się poprawić używał formacie specyfikator jak%d
,%f
,%c
, lub%s
?)Łatwo jest zinterpretować wiersz tekstu - ciąg znaków - jak dowolną z tych rzeczy. Aby przekonwertować ciąg na liczbę całkowitą, najprostszym (choć niedoskonałym) sposobem jest wywołanie
atoi()
. Aby przekonwertować na liczbę zmiennoprzecinkową, istniejeatof()
. (I są też lepsze sposoby, jak zobaczymy za chwilę.) Oto bardzo prosty przykład:Jeśli chcesz, aby użytkownik wpisał pojedynczy znak (być może
y
lubn
jako odpowiedź tak / nie), możesz dosłownie złapać pierwszy znak linii, na przykład:(To oczywiście ignoruje możliwość wpisania przez użytkownika odpowiedzi wieloznakowej; po cichu ignoruje wszelkie dodatkowe znaki, które zostały wpisane).
Wreszcie, jeśli chcesz, aby użytkownik wpisał ciąg znaków zdecydowanie nie zawierający białych znaków, jeśli chcesz traktować wiersz wejściowy
ponieważ po łańcuchu
"hello"
następuje coś innego (co zrobiłbyscanf
format%s
), cóż, w takim przypadku trochę sfałszowałem, w końcu nie jest tak łatwo ponownie zinterpretować linię w ten sposób, więc odpowiedź na to część pytania będzie musiała chwilę poczekać.Ale najpierw chcę wrócić do trzech rzeczy, które pominąłem.
(1) Dzwoniliśmy
czytać do tablicy
line
, a gdzie 512 jest rozmiarem tablicy,line
więcfgets
wie, żeby jej nie przepełnić. Ale aby upewnić się, że 512 jest prawidłową liczbą (szczególnie, aby sprawdzić, czy ktoś nie poprawił programu, aby zmienić rozmiar), musisz przeczytać wszystko, gdziekolwiekline
zadeklarowano. Jest to uciążliwe, więc istnieją dwa znacznie lepsze sposoby synchronizacji rozmiarów. Możesz (a) użyć preprocesora, aby utworzyć nazwę dla rozmiaru:Lub (b) użyj
sizeof
operatora C :(2) Drugi problem polega na tym, że nie sprawdzaliśmy błędów. Podczas odczytywania danych wejściowych należy zawsze sprawdzać możliwość wystąpienia błędu. Jeśli z jakiegokolwiek powodu
fgets
nie może odczytać wiersza tekstu, o który go prosiłeś, oznacza to, zwracając wskaźnik zerowy. Więc powinniśmy robić takie rzeczyWreszcie istnieje problem polegający na tym, że aby odczytać wiersz tekstu,
fgets
odczytuje znaki i wypełnia je do tablicy, dopóki nie znajdzie\n
znaku kończącego linię, a także wypełnia\n
znak w tablicy . Możesz to zobaczyć, jeśli nieznacznie zmodyfikujesz nasz wcześniejszy przykład:Jeśli uruchomię to i po wyświetleniu monitu napiszę „Steve”, zostanie wydrukowane
To
"
w drugiej linii jest dlatego, że napis, który odczytał i wydrukował, był w rzeczywistości"Steve\n"
.Czasami ta dodatkowa nowa linia nie ma znaczenia (na przykład, kiedy zadzwoniliśmy
atoi
lubatof
, ponieważ oba ignorują wszelkie dodatkowe dane nienumeryczne po liczbie), ale czasami ma to duże znaczenie. Tak często będziemy chcieli usunąć tę nową linię. Można to zrobić na kilka sposobów, które omówię za chwilę. (Wiem, że dużo to mówiłem. Ale wrócę do tych wszystkich rzeczy, obiecuję.)W tym momencie możesz myśleć: „Myślałem, że powiedziałeś, że
scanf
to nie jest dobre, a ten inny sposób byłby o wiele lepszy. Alefgets
zaczyna wyglądać jak uciążliwość. Dzwonieniescanf
było takie proste ! Czy mogę nadal tego używać? „Jasne, możesz nadal używać
scanf
, jeśli chcesz. (I w przypadku naprawdę prostych rzeczy, pod pewnymi względami jest to prostsze.) Ale proszę, nie przychodź do mnie płakać, gdy zawodzi cię z powodu jednego z 17 dziwactw i słabości, lub przechodzi w nieskończoną pętlę z powodu wejścia nie spodziewałem się lub gdy nie możesz dowiedzieć się, jak go użyć, aby zrobić coś bardziej skomplikowanego. I spójrzmy nafgets
rzeczywiste niedogodności:Zawsze musisz określić rozmiar tablicy. Cóż, oczywiście, wcale nie jest to uciążliwe - jest to cecha, ponieważ przepełnienie bufora jest naprawdę złą rzeczą.
Musisz sprawdzić wartość zwracaną. W rzeczywistości jest to pranie, ponieważ aby używać
scanf
poprawnie, musisz również sprawdzić jego wartość zwrotną.Musisz rozebrać
\n
plecy. Przyznaję, że to prawdziwa uciążliwość. Chciałbym, żeby istniała standardowa funkcja, na którą mógłbym cię wskazać, która nie miała tak małego problemu. (Proszę, niech nikt nie poruszagets
.) Ale w porównaniu doscanf's
17 różnych niedogodności, wezmę tę jedną niedogodnośćfgets
każdego dnia.Więc jak nie masz paska, który przełamane? Trzy drogi:
(a) Oczywisty sposób:
(b) Tricky i kompaktowy sposób:
Niestety ten nie zawsze działa.
(c) Kolejny zwarty i nieco niejasny sposób:
A teraz, gdy to już nie przeszkadza, możemy wrócić do innej rzeczy, którą pominąłem: niedoskonałości
atoi()
iatof()
. Problem polega na tym, że nie dają one żadnej użytecznej wskazówki na sukces lub porażkę: po cichu ignorują końcowe dane nieliczbowe i po cichu zwracają 0, jeśli w ogóle nie ma danych numerycznych. Preferowane alternatywy - które mają również pewne inne zalety - tostrtol
istrtod
.strtol
pozwala również użyć bazy innej niż 10, co oznacza, że możesz uzyskać efekt (między innymi)%o
lub za%x
pomocąscanf
. Ale pokazanie, jak prawidłowo korzystać z tych funkcji, jest historią samą w sobie i byłoby zbyt dużym rozproszeniem od tego, co już zamienia się w dość rozdrobnioną narrację, więc nie powiem już więcej o nich.Reszta głównej narracji dotyczy danych wejściowych, które możesz próbować przetworzyć, które są bardziej skomplikowane niż tylko pojedyncza liczba lub znak. Co jeśli chcesz odczytać wiersz zawierający dwie cyfry lub wiele słów oddzielonych spacjami lub określoną interpunkcję ramkową? To właśnie tam rzeczy stają się interesujące i gdzie prawdopodobnie komplikują się, jeśli próbujesz robić rzeczy za pomocą
scanf
, i gdzie jest znacznie więcej opcji teraz, gdy czytasz jedną linię tekstu za pomocąfgets
, chociaż pełna historia wszystkich tych opcji prawdopodobnie może wypełnić książkę, więc będziemy mogli jedynie zarysować powierzchnię tutaj.Moją ulubioną techniką jest podzielenie linii na „słowa” oddzielone spacjami, a następnie zrobienie czegoś z każdym „słowem”. Jedną z głównych standardowych funkcji służących do tego jest
strtok
(która ma również swoje problemy i która ocenia całą osobną dyskusję). Moje własne preferencje to dedykowana funkcja do konstruowania tablicy wskaźników dla każdego rozbitego „słowa”, funkcja opisana w tych notatkach kursowych . W każdym razie, gdy masz już „słowa”, możesz dalej przetwarzać każde, być może przy użyciu tych samych funkcjiatoi
/atof
/strtol
/strtod
, które już sprawdziliśmy.Paradoksalnie, mimo że spędziliśmy tutaj sporo czasu i wysiłku, zastanawiając się, jak się odejść
scanf
, innym dobrym sposobem radzenia sobie z wierszem tekstu, który właśnie czytaliśmy,fgets
jest przekazanie gosscanf
. W ten sposób uzyskujesz większość zaletscanf
, ale bez większości wad.Jeśli twoja składnia wejściowa jest szczególnie skomplikowana, może być właściwe użycie biblioteki „regexp” do jej przeanalizowania.
Wreszcie możesz użyć dowolnych rozwiązań analizy ad hoc, które Ci odpowiadają. Możesz poruszać się po linii po znaku za pomocą
char *
wskaźnika sprawdzającego znaki, których oczekujesz. Możesz także wyszukiwać określone znaki za pomocą funkcji takich jakstrchr
lubstrrchr
, lubstrspn
lubstrcspn
lubstrpbrk
. Lub możesz parsować / konwertować i pomijać grupy znaków cyfrowych za pomocą funkcjistrtol
lubstrtod
, które pomijaliśmy wcześniej.Można oczywiście powiedzieć o wiele więcej, ale mam nadzieję, że wprowadzenie to sprawi, że zaczniesz.
źródło
sizeof (line)
a nie po prostusizeof line
? Ten pierwszy sprawia, że wygląda jakline
nazwa typu!sscanf
jako silnika konwersji, ale zbieranie (i ewentualnie masowanie) danych wejściowych za pomocą innego narzędzia. Ale może warto wspomniećgetline
w kontekście taht.fscanf
prawdziwych niedogodnościach”, masz na myślifgets
? Irytujące # 3 naprawdę mnie denerwuje, szczególnie biorąc pod uwagę, żescanf
zwraca niepotrzebny wskaźnik do bufora zamiast zwracać liczbę wprowadzanych znaków (co sprawiłoby, że usunięcie nowej linii byłoby znacznie czystsze).sizeof
stylu. Dla mnie pamiętanie, kiedy jesteś w domu, jest łatwe: myślę, że jestem(type)
jak obsada bez wartości (ponieważ interesuje nas tylko ten typ). Jeszcze jedno: mówisz, żestrtok(line, "\n")
to nie zawsze działa, ale nie jest oczywiste, kiedy nie. Zgaduję, że myślisz o przypadku, w którym linia była dłuższa niż bufor, więc nie mamy nowej linii istrtok()
zwraca wartość null? Szkoda,fgets()
że nie zwraca bardziej użytecznej wartości, więc możemy wiedzieć, czy nowa linia jest, czy nie.Zamiast
scanf(some_format, ...)
rozważyćfgets()
zsscanf(buffer, some_format_and %n, ...)
Za pomocą tego
" %n"
kodu można po prostu wykryć, czy cały format został pomyślnie zeskanowany i czy na końcu nie było żadnych niepotrzebnych śmieci.źródło
Określmy wymagania dotyczące analizowania jako:
prawidłowe dane wejściowe muszą zostać zaakceptowane (i przekonwertowane na inną formę)
nieprawidłowe dane wejściowe należy odrzucić
gdy jakiekolwiek dane wejściowe zostaną odrzucone, konieczne jest przekazanie użytkownikowi opisowego komunikatu wyjaśniającego (w jasnym języku „zrozumiałym dla zwykłych ludzi, którzy nie są programistami”), dlaczego został odrzucony (aby ludzie mogli dowiedzieć się, jak to naprawić problem)
Aby wszystko było bardzo proste, rozważmy przeanalizowanie pojedynczej prostej liczby całkowitej dziesiętnej (wpisanej przez użytkownika) i nic więcej. Możliwe przyczyny odrzucenia danych przez użytkownika to:
Zdefiniujmy również poprawnie „dane wejściowe zawierały niedopuszczalne znaki”; i powiedz, że:
5” będzie traktowane jak „5”)
Na podstawie tego możemy ustalić, że potrzebne są następujące komunikaty o błędach:
Z tego punktu widać, że odpowiednia funkcja do konwersji łańcucha na liczbę całkowitą musiałaby rozróżniać bardzo różne rodzaje błędów; i że coś takiego jak „
scanf()
” lub „atoi()
” lub „strtoll()
” jest całkowicie i całkowicie bezwartościowe, ponieważ nie dają one żadnego wskazania, co było nie tak z danymi wejściowymi (i używają całkowicie nieistotnej i niewłaściwej definicji tego, co jest / nie jest ”prawidłowe Wejście").Zamiast tego zacznijmy pisać coś, co nie jest bezużyteczne:
Aby spełnić podane wymagania;
convertStringToInteger()
prawdopodobnie ta funkcja sama w sobie będzie zawierała kilkaset wierszy kodu.Teraz było to po prostu „analizowanie pojedynczej prostej liczby całkowitej dziesiętnej”. Wyobraź sobie, że chcesz przeanalizować coś złożonego; jak lista struktur „imię i nazwisko, adres, numer telefonu, adres e-mail”; a może jak język programowania. W takich przypadkach może być konieczne napisanie tysięcy wierszy kodu, aby utworzyć analizę składniową, która nie jest kalekim żartem.
Innymi słowy...
Napisz (potencjalnie tysiące wierszy) kod, aby dopasować go do swoich wymagań.
źródło
Oto przykład użycia
flex
do skanowania prostego wejścia, w tym przypadku pliku liczb zmiennoprzecinkowych ASCII, który może być w formacie US (n,nnn.dd
) lub European (n.nnn,dd
). Jest to po prostu skopiowane z dużo większego programu, więc mogą istnieć pewne nierozwiązane odwołania:źródło
Inne odpowiedzi podają właściwe szczegóły niskiego poziomu, więc ograniczę się do wyższego poziomu: Najpierw przeanalizuj, jak chcesz wyglądać każda linia wejściowa. Spróbuj opisać dane wejściowe formalną składnią - przy odrobinie szczęścia można je opisać za pomocą zwykłej gramatyki lub przynajmniej gramatyki bezkontekstowej . Jeśli wystarczająca jest zwykła gramatyka, możesz zakodować maszynę o skończonym staniektóry rozpoznaje i interpretuje każdy wiersz polecenia po jednym znaku na raz. Twój kod następnie odczyta wiersz (jak wyjaśniono w innych odpowiedziach), a następnie zeskanuje znaki w buforze przez maszynę stanu. W niektórych stanach zatrzymujesz i konwertujesz skanowany do tej pory podciąg na liczbę lub cokolwiek innego. Prawdopodobnie możesz „rzucić własnym”, jeśli jest to takie proste; jeśli uznasz, że potrzebujesz pełnej gramatyki bezkontekstowej, lepiej jest dowiedzieć się, jak korzystać z istniejących narzędzi do analizowania (re:
lex
i /yacc
lub ich wariantów).źródło
errno == EOVERFLOW
po użyciustrtoll
).