scanf to lustrzane odbicie printf: ten sam nagłówek, podobnie wyglądające łańcuchy formatujące, przeciwny kierunek. Czyta tekst ze standardowego wejścia, konwertuje go zgodnie z formatem i zapisuje wyniki przez wskaźniki, które mu przekażesz.
To także funkcja, która wprawia w zakłopotanie więcej początkujących niż jakakolwiek inna w C, bo trzy z jej zachowań są zaskakujące, a jedno to luka w bezpieczeństwie. Ta strona omawia wszystkie cztery.
Podstawowa postać i dlaczego &
Aby to uruchomić, wpisz liczbę i naciśnij Enter. &age to nie ozdobnik. C przekazuje każdy argument przez wartość, więc gdyby napisać scanf("%d", age), funkcja dostałaby kopię bieżącej (śmieciowej) wartości age i nie miałaby jak dotrzeć do samej zmiennej. &age przekazuje adres zmiennej, przez który scanf zapisuje wynik. Pełny obraz znajdziesz na stronie o wskaźnikach.
Pominięcie & to najczęstszy błąd przy scanf. Kompilator z -Wall go wyłapuje (format '%d' expects argument of type 'int *'), a bez włączonych ostrzeżeń kod się kompiluje i psuje pamięć pod jakimś przypadkowym adresem.
Wczytywanie kilku wartości
Jedno wywołanie może wczytać kilka wartości. Białe znaki na wejściu, czyli spacje, tabulatory i znaki nowej linii, je rozdzielają, a specyfikatory liczbowe pomijają dowolną ich ilość.
Dosłowne spacje w "%d %d %d" są właściwie zbędne, bo %d i tak pomija początkowe białe znaki, ale poprawiają czytelność formatu. Zbędne nie jest za to sprawdzanie wartości zwracanej, o którym niżej.
Znaki inne niż białe muszą w formacie dokładnie pasować do wejścia i w ten sposób wczytuje się dane o określonej strukturze:
: w formacie wymaga : na wejściu. Wpisz 9:30, a zostanie odczytane. Wpisz 9 30, a wczytywanie zawiedzie po odczytaniu 9.
Zawsze sprawdzaj wartość zwracaną
scanf zwraca liczbę pomyślnie przypisanych elementów: nie liczbę znaków i nie flagę sukcesu. Zwraca EOF, jeśli wejście skończyło się, zanim cokolwiek zostało przekonwertowane.
Wpisz hello po zachęcie. scanf zwraca 0, n zostaje nietknięte, wciąż ze śmieciem, od którego zaczynało, a problematyczny tekst nadal leży w strumieniu wejściowym. Ta ostatnia część zamienia pojedyncze złe wejście w nieskończoną pętlę:
/* BLAD: kreci sie w nieskonczonosc przy wejsciu nieliczbowym */
while (scanf("%d", &n) != 1) {
printf("Sprobuj ponownie: ");
}
scanf nigdy nie pobiera tekstu, którego nie zdołało przekonwertować, więc kolejne wywołanie zawodzi na tych samych znakach, i tak bez końca. Żeby się z tego wydostać, musisz sam wyrzucić resztę linii:
Sprawdzenie EOF ma znaczenie: gdy strumień wejściowy się kończy (użytkownik naciska Ctrl+D albo kończy się plik przekazany potokiem), scanf od tej pory zawsze zwraca EOF, więc pętla, która sprawdza tylko != 1, ponawia próby w nieskończoność. Traktuj „złe wejście” i „koniec wejścia” jako dwa różne przypadki, którymi są. Mając to, warto trzymać funkcję pomocniczą discard_line w każdym programie, który używa scanf do danych wpisywanych przez człowieka.
Przepełnienie bufora przy %s
%s wczytuje znaki do tablicy char. Zauważ brak &: nazwa tablicy już zamienia się we wskaźnik na jej pierwszy element, więc &name miałoby zły typ.
Problem w tym, że samo %s nie ma pojęcia, jak duża jest twoja tablica:
char name[10];
scanf("%s", name); /* NIEBEZPIECZNE: wpisz 40 znakow, a zapisze 41 bajtow */
Nic nie zatrzymuje zapisu na końcu tablicy. Leci dalej, w to, co leży w pamięci obok: inne zmienne, adres powrotu. To klasyczne przepełnienie bufora i historycznie najczęściej wykorzystywana klasa błędów w C. Rozwiązaniem jest szerokość między % a s, o jeden mniejsza niż rozmiar tablicy, żeby zostało miejsce na kończące '\0':
Szerokość trzeba zapisać jako literał w łańcuchu formatującym, przez co uciążliwie jest ją synchronizować z rozmiarem tablicy. To kolejny powód, dla którego produkcyjny kod w C przeważnie używa fgets.
Druga niespodzianka z %s: zatrzymuje się na pierwszym białym znaku. Wpisz Ada Lovelace, a w name będzie Ada, natomiast Lovelace poczeka w strumieniu na kolejny odczyt. %s wczytuje słowo, a nie linię.
Błąd z pozostawionym znakiem nowej linii
To jest to „scanf nie działa”, na które trafia każdy. Specyfikatory liczbowe pomijają początkowe białe znaki, a %c nie.
Oto co się dzieje bez tej spacji. Wpisujesz 30 i naciskasz Enter. %d pobiera 3 i 0 i zatrzymuje się na znaku nowej linii, zostawiając '\n' w buforze. Następne scanf("%c", &initial) odczytuje ten znak nowej linii jako znak i natychmiast wraca: zachęta pojawia się i zostaje pominięta w tym samym momencie.
Spacja w łańcuchu formatującym scanf oznacza „pomiń tutaj dowolną ilość białych znaków”, więc " %c" przeskakuje zabłąkany znak nowej linii i czeka na prawdziwy znak. Spacja jest potrzebna tylko przed %c i %[, bo wszystkie pozostałe specyfikatory same pomijają białe znaki.
Wczytywanie liczb float i znaków
Specyfikatory są w większości wspólne z printf, z jedną ważną różnicą: scanf musi znać dokładny rozmiar miejsca docelowego, bo zapisuje przez wskaźnik.
Wypróbuj 1.5 2.25 7 x.
%f wczytuje float, a %lf wczytuje double. W printf oba wypisują się tak samo, bo liczby float są promowane do double przed wywołaniem, ale w scanf nie ma promocji, a użycie %f z double * zapisuje cztery bajty do ośmiobajtowej zmiennej i zostawia ją uszkodzoną. %c nie potrzebuje tutaj początkowej spacji, bo poprzedzające je %d zatrzymało się na spacji, a nie na znaku nowej linii. (Pełna tabela jest na stronie o specyfikatorach formatu.)
fgets: solidna alternatywa
Do wszystkiego, co wpisuje człowiek, wczytuj całą linię i przetwarzaj ją później. fgets przyjmuje rozmiar bufora jako argument, więc fizycznie nie może go przepełnić, a gdy linia się mieści, pobiera też znak nowej linii, więc nic nie zostaje, by zmylić kolejny odczyt. (Linia dłuższa niż bufor zostawia swój koniec niewczytany, a brak '\n' w tym, co dostajesz, pozwala to wykryć.)
Trzy uwagi o tym wzorcu:
sizeof lineprzekazuje prawdziwy rozmiar, więc zmiana rozmiaru tablicy nie wymaga żadnej innej poprawki, w przeciwieństwie do literału%19s.fgetszostawia znak nowej linii, jeśli linia się zmieściła, i dlatego jest tu przycinanie przezstrcspn. W ten sam sposób wykryjesz linię za długą dla bufora: w tym, co dostajesz, nie ma'\n'.- Przetwarzaj później.
strtoldla liczb całkowitych (potrafi też powiedzieć, gdzie zatrzymało się przetwarzanie),strtoddla liczb zmiennoprzecinkowych albosscanf(line, "%d %d", &a, &b), aby użyć formatu scanf na napisie, który już bezpiecznie trzymasz.
sscanf zasługuje na osobną wzmiankę: to scanf, który czyta z napisu zamiast ze strumienia wejściowego, i idealnie łączy się z fgets. Dostajesz wygodne przetwarzanie scanf bez żadnych jego problemów ze strumieniem.
Częste błędy
- Zapominanie o
&przy argumencie, który nie jest tablicą. Kompiluj z-Wall. %sbez szerokości. Przepełnienie bufora czekające na długie wejście.- Ignorowanie wartości zwracanej. A potem odczyt zmiennej, do której
scanfnigdy niczego nie przypisało. - Pętla na nieudanym
scanfbez wyrzucania wejścia. Nieskończona pętla. %cbez początkowej spacji po wczytaniu liczby. Błąd pominiętej zachęty.- Używanie
%fdladouble. Ciche uszkodzenie danych,scanfpotrzebuje%lf. - Oczekiwanie, że
%swczyta linię. Wczytuje jedno słowo ograniczone białymi znakami.
Najczęściej zadawane pytania
Dlaczego scanf potrzebuje & w C?
Bo C przekazuje argumenty przez wartość. scanf musi zmienić twoją zmienną, więc potrzebuje jej adresu, a nie kopii wartości: scanf("%d", &age). Jedynym wyjątkiem jest tablica znaków, której nazwa już jest adresem, więc scanf("%19s", name) nie ma &.
Dlaczego scanf pomija moje dane przy wczytywaniu znaku lub napisu?
Wcześniejsze scanf("%d", ...) zostawiło w buforze wejścia znak nowej linii z klawisza Enter. %d i %f pomijają początkowe białe znaki, ale %c nie, więc wczytuje ten znak nowej linii zamiast czekać. Rozwiązaniem jest spacja w formacie, scanf(" %c", &ch), która każe scanf najpierw pominąć białe znaki.
Jak bezpiecznie wczytać napis przez scanf w C?
Podaj dla %s maksymalną szerokość o jeden mniejszą niż bufor: char name[20]; scanf("%19s", name);. Bez szerokości %s zapisze tyle znaków, ile wpisze użytkownik, i bez oporów wyjdzie poza koniec tablicy. Pamiętaj też, że %s zatrzymuje się na pierwszym białym znaku, więc nie wczyta imienia i nazwiska ze spacją.
Używać scanf czy fgets w C?
Do wszystkiego, co wpisuje człowiek, używaj fgets. Wczytuje całą linię do bufora o znanym rozmiarze, nie może go przepełnić i (gdy linia się mieści) pobiera też znak nowej linii, więc klasyczne błędy przy mieszanym wczytywaniu znikają. Linię przetwarzasz potem przez sscanf lub strtol. scanf wystarczy do małych ćwiczeń i poprawnie sformatowanych danych z maszyny.