Menu

printf w C: format, specyfikatory i wypisywanie liczb zmiennoprzecinkowych

Jak naprawdę działa printf w C: model łańcucha formatującego, specyfikatory używane na co dzień, szerokość i precyzja do wyrównanych wyników, rozsądne wypisywanie liczb float, wartość zwracana i dlaczego printf(user_input) to dziura w bezpieczeństwie.

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

printf to pierwsza funkcja, jakiej uczy się każdy programista C, i zarazem ta, do której dokumentacji większość z nich nigdy porządnie nie zajrzała. Warto poświęcić jej dziesięć minut, bo jej łańcuch formatujący to mały, osobny język, a błędny specyfikator to nie literówka, tylko niezdefiniowane zachowanie.

Funkcja znajduje się w <stdio.h>, więc każdy program, który jej używa, zaczyna się od #include <stdio.h>.

Model łańcucha formatującego

printf przechodzi przez łańcuch formatujący znak po znaku. Wszystko, co nie jest %, wypisuje dokładnie tak, jak jest zapisane. Znak % rozpoczyna specyfikację konwersji, która pobiera kolejny argument i wypisuje go w żądanej postaci.

Argumenty są dopasowywane do specyfikatorów wyłącznie według pozycji: pierwszy % bierze count, drugi name i tak dalej. printf nie ma jak sprawdzić, czy przekazujesz to, co obiecujesz, bo nie widzi typów, więc łańcuch formatujący to umowa, której musisz dotrzymać. Właśnie dlatego warto włączyć -Wall: GCC czyta literał formatujący w czasie kompilacji i ostrzega, gdy argument nie pasuje.

Nic nie wymusza nowej linii. printf("Hello") zostawia kursor za o, i dlatego prawie każdy łańcuch formatujący kończy się na \n.

Specyfikatory używane na co dzień

Pięć z nich pokrywa większość kodu:

SpecyfikatorWypisujePrzykładowe wywołanieWynik
%dint ze znakiemprintf("%d", -42)-42
%fdoubleprintf("%f", 2.5)2.500000
%słańcuch znaków (char *)printf("%s", "hi")hi
%cpojedynczy znakprintf("%c", 'A')A
%padres wskaźnikaprintf("%p", (void*)&x)0x7ffd...

W tym fragmencie kryją się dwie zasady. Dosłowny % zapisuje się jako %%, a samotny % na końcu łańcucha formatującego to niezdefiniowane zachowanie. Z kolei %p wymaga void *, więc rzutuj wskaźnik: przekazanie int * wprost jest formalnie niezdefiniowane, nawet jeśli w praktyce wypisuje się dobrze.

Pełna tabela, ze wszystkimi specyfikatorami, modyfikatorami długości i kolumną dla scanf, jest na stronie o specyfikatorach formatu. Ta strona trzyma się tych, po które sięgasz najczęściej.

Szerokość, precyzja i wyrównanie

Między % a literą możesz wstawić flagi, szerokość i precyzję. Tak zamienia się pętlę w tabelę.

Szerokość to minimum, nigdy maksimum: %3d z wartością 123456 wypisze wszystkie sześć cyfr. Precyzja znaczy co innego dla każdego typu: liczbę cyfr po przecinku dla %f, maksymalną liczbę znaków dla %s i minimalną liczbę cyfr dla %d.

Obie można podać w czasie działania programu za pomocą *, które odczytuje argument typu int:

Zmiana width przebudowuje całą tabelę. To standardowy sposób na wyrównane kolumny w C, bez żadnej pomocniczej funkcji do dopełniania napisów.

Rozsądne wypisywanie liczb zmiennoprzecinkowych

%f bez precyzji zawsze wypisuje sześć cyfr po kropce, a rzadko ktoś tego chce:

Trzy opcje i praktyczna zasada dla każdej:

  • %.Nf dla pieniędzy, pomiarów i wszystkiego, co ma naturalną liczbę miejsc po przecinku. %.2f to właściwy wybór dla kwot.
  • %e dla notacji naukowej, zawsze w postaci d.dddddde±dd.
  • %g, gdy nie znasz z góry rzędu wielkości. Wybiera krótszą z form %f i %e oraz usuwa końcowe zera, więc to najrozsądniejszy domyślny wybór przy logowaniu wartości, której jeszcze nie znasz.

Jedną pułapkę warto powiedzieć wprost: w printf nie ma %lf. Argumenty typu float są promowane do double, zanim printf je zobaczy, więc %f obsługuje oba typy, a %lf ma znaczenie tylko w scanf, gdzie liczy się rozmiar miejsca docelowego. (C99 akceptuje %lf w printf jako synonim, ale nawyk pisania go prowadzi do wpisywania %f w scanf, a to już naprawdę się psuje.)

Liczby całkowite innych rozmiarów

%d jest dla int. Większe typy lub typy bez znaku wymagają modyfikatora długości, a ich pomylenie to niezdefiniowane zachowanie, a nie błąd zaokrąglenia:

%zu dla size_t to ten, który ludzie najczęściej mylą. sizeof zwraca size_t, a nie int, więc printf("%d", sizeof x) to błąd, który przypadkiem działa na systemach 32-bitowych, a na 64-bitowych zachowuje się źle.

Wartość zwracana

printf zwraca liczbę wypisanych znaków albo liczbę ujemną w razie błędu. Zwykle się ją ignoruje i przeważnie nie ma w tym nic złego, ale bywa naprawdę przydatna, gdy musisz wiedzieć, jak szeroki był wynik:

Pokrewne sprintf i snprintf zapisują do bufora zamiast na ekran i zwracają długość wygenerowanego tekstu. Zawsze wybieraj snprintf, które przyjmuje rozmiar bufora i nie może go przepełnić:

snprintf zwraca długość, jakiej potrzebowałby pełny łańcuch, więc wartość zwracana równa rozmiarowi bufora lub większa oznacza, że wynik został obcięty.

Podatność format string

To jedyny problem bezpieczeństwa w printf, ale poważny. Nigdy nie przekazuj jako łańcucha formatującego danych, które nie pochodzą od ciebie:

/* NIEBEZPIECZNE: nigdy tego nie rob */
printf(user_input);

/* Poprawnie */
printf("%s", user_input);

Jeśli user_input zawiera %s, printf odczyta argument, którego nigdy nie przekazano, i podąży za śmieciami, które wyglądają jak wskaźnik, zwykle kończąc awarią. Powtórzone %x zrzuca zawartość stosu na ekran, a mogą tam być hasła lub klucze. Natomiast %n, które zapisuje liczbę znaków do argumentu, historycznie zamieniało ten błąd w dowolne uszkodzenie pamięci i zdalne wykonanie kodu.

Zasada jest bezwzględna i nic nie kosztuje: łańcuch formatujący to zawsze literał napisany przez ciebie. Wszystko, co zmienne, trafia do argumentów. Opcja GCC -Wformat-security wyłapuje ten błąd i jest zawarta w -Wall -Wextra.

Buforowanie: dlaczego wynik czasem pojawia się z opóźnieniem

printf nie pisze od razu do terminala. Wyjście jest buforowane: opróżniane, gdy bufor się zapełni, gdy w terminalu pojawi się znak nowej linii albo gdy program zakończy się normalnie. Wynikają z tego dwie rzeczy:

  • Program, który się wysypie, może zgubić tekst, który „już został wypisany”. Jeśli debugujesz za pomocą printf i ostatni komunikat się nie pojawia, awaria nastąpiła po tej linii, a nie przed nią. Dodaj fflush(stdout) albo wysyłaj komunikaty debugowania na stderr, który nie jest buforowany.
  • fprintf(stderr, ...) to właściwy sposób wypisywania błędów. Nie jest buforowany i trafia do osobnego strumienia, który użytkownicy mogą przekierować niezależnie od zwykłego wyjścia.

Najczęściej zadawane pytania

Jak działa printf w C?

printf przyjmuje łańcuch formatujący i przegląda go znak po znaku. Zwykłe znaki wypisuje bez zmian, a każdy % rozpoczyna specyfikację konwersji, która pobiera jeden z kolejnych argumentów i wypisuje go w żądanej postaci. printf("%d apples\n", 5) wypisuje 5 apples i znak nowej linii.

Jak wypisać liczbę float z 2 miejscami po przecinku w C?

Użyj precyzji: printf("%.2f\n", 3.14159) wypisuje 3.14. Liczba po kropce określa, ile cyfr ma się pojawić po przecinku dziesiętnym. %f bez precyzji zawsze wypisuje 6 cyfr i dlatego niesformatowane liczby zmiennoprzecinkowe wyglądają tak chaotycznie.

Dlaczego printf wypisuje śmieci albo program się wysypuje?

Prawie zawsze chodzi o niezgodność specyfikatora z argumentem. printf("%d", 3.5) albo printf("%s", 42) to niezdefiniowane zachowanie: printf nie widzi prawdziwych typów, ufa łańcuchowi formatującemu. Kompiluj z -Wall, wtedy GCC sprawdza format względem argumentów.

Dlaczego printf(user_input) jest niebezpieczne?

Jeśli tekst użytkownika zawiera sekwencje %, printf je wykona i odczyta argumenty, których nigdy nie przekazano. To wycieka pamięć stosu, a z %n pozwala nawet zapisywać do pamięci. To klasyczna podatność format string. Zawsze pisz zamiast tego printf("%s", user_input).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ