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 tym getline
). A następnie następnym krokiem jest jakoś zinterpretować ten wiersz tekstu.
Oto podstawowy przepis na dzwonienie w fgets
celu odczytania wiersza tekstu:
char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
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 tablicy
line
, w fgets
której czytamy. Ten fakt - że możemy powiedzieć, fgets
ile można odczytać - oznacza, że możemy być pewni, że fgets
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ą, istnieje atof()
. (I są też lepsze sposoby, jak zobaczymy za chwilę.) Oto bardzo prosty przykład:
printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);
Jeśli chcesz, aby użytkownik wpisał pojedynczy znak (być może y
lub
n
jako odpowiedź tak / nie), możesz dosłownie złapać pierwszy znak linii, na przykład:
printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
(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
hello world!
ponieważ po łańcuchu "hello"
następuje coś innego (co zrobiłby scanf
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
fgets(line, 512, stdin);
czytać do tablicy line
, a gdzie 512 jest rozmiarem tablicy, line
więc fgets
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, gdziekolwiek line
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:
#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
Lub (b) użyj sizeof
operatora C :
fgets(line, sizeof(line), stdin);
(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 rzeczy
printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
printf("Well, never mind, then.\n");
exit(1);
}
Wreszcie 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:
printf("you typed: \"%s\"\n", line);
Jeśli uruchomię to i po wyświetleniu monitu napiszę „Steve”, zostanie wydrukowane
you typed: "Steve
"
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
lub atof
, 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. Ale fgets
zaczyna wyglądać jak uciążliwość. Dzwonienie scanf
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 na fgets
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 porusza gets
.) Ale w porównaniu do scanf'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:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b) Tricky i kompaktowy sposób:
strtok(line, "\n");
Niestety ten nie zawsze działa.
(c) Kolejny zwarty i nieco niejasny sposób:
line[strcspn(line, "\n")] = '\0';
A teraz, gdy to już nie przeszkadza, możemy wrócić do innej rzeczy, którą pominąłem: niedoskonałości atoi()
i atof()
. 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 - to strtol
i strtod
.
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 funkcji atoi
/ 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 go sscanf
. W ten sposób uzyskujesz większość zalet scanf
, 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 jak strchr
lub strrchr
, lub strspn
lub strcspn
lub strpbrk
. Lub możesz parsować / konwertować i pomijać grupy znaków cyfrowych za pomocą funkcji strtol
lub
strtod
, 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 = sscanf("1 2 junk", "%d%d", &x, &y)) != 2
nie wykrywa tak źle końcowego tekstu nieliczbowego.