Menu

scanf w C: wczytywanie danych, zasada & i dlaczego przestaje działać

Jak wczytywać dane przez scanf w C: dlaczego & jest wymagane, jak traktowane są białe znaki, przepełnienie bufora przy %s i jego naprawa szerokością, sprawdzanie wartości zwracanej, błąd z pozostawionym znakiem nowej linii i fgets jako solidna alternatywa.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 line przekazuje prawdziwy rozmiar, więc zmiana rozmiaru tablicy nie wymaga żadnej innej poprawki, w przeciwieństwie do literału %19s.
  • fgets zostawia znak nowej linii, jeśli linia się zmieściła, i dlatego jest tu przycinanie przez strcspn. W ten sam sposób wykryjesz linię za długą dla bufora: w tym, co dostajesz, nie ma '\n'.
  • Przetwarzaj później. strtol dla liczb całkowitych (potrafi też powiedzieć, gdzie zatrzymało się przetwarzanie), strtod dla liczb zmiennoprzecinkowych albo sscanf(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.
  • %s bez szerokości. Przepełnienie bufora czekające na długie wejście.
  • Ignorowanie wartości zwracanej. A potem odczyt zmiennej, do której scanf nigdy niczego nie przypisało.
  • Pętla na nieudanym scanf bez wyrzucania wejścia. Nieskończona pętla.
  • %c bez początkowej spacji po wczytaniu liczby. Błąd pominiętej zachęty.
  • Używanie %f dla double. Ciche uszkodzenie danych, scanf potrzebuje %lf.
  • Oczekiwanie, że %s wczyta 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ