Rodzina printf scanf - MarekBykowski/readme GitHub Wiki

Rodzina printf / scanf — formatowane I/O w C

  • sprintf pisze do buf tekst sformatowany ze zmiennych
  • sscanf bierze sformatowane dane z buf i zapisuje je do zmiennych.
  • sprintf czyta zmienne -> podajesz wartość
  • sscanf zapisuje do zmiennych -> podajesz adres (&a)

Ten sam string formatujący opisuje ten sam kształt danych — raz do zapisu, raz do odczytu.

char buf[64];
int a = 10, b = 20;

sprintf(buf, "%d %d", a, b);   /* zmienne → buf   : buf = "10 20" */
sscanf (buf, "%d %d", &a, &b); /* buf     → zmienne: a=10, b=20    */

Asymetria wskaźników: sprintf tylko czyta zmienne → podajesz wartość. sscanf musi zapisać wynik → podajesz adres (&).


Wyjście — rodzina printf

Nazwa koduje: dokąd trafia wynik + czy wariant bierze va_list.

Funkcja Cel wyjścia Uwagi
printf stdout
fprintf FILE * plik, stderr, …
sprintf char * bufor bez limitu — niebezpieczne, łatwo przepełnić
snprintf bufor z limitem size return = długość pełnego napisu (może być >= size)
dprintf deskryptor int fd POSIX, nie FILE *
asprintf / vasprintf bufor mallociem GNU/BSD, sam alokuje; potem free
vprintf, vfprintf, vsprintf, vsnprintf, vdprintf jw. biorą va_list zamiast ...

Wejście — rodzina scanf

Funkcja Źródło Uwagi
scanf stdin
fscanf FILE *
sscanf const char * parsowanie gotowego stringa — bardzo częste
vscanf, vfscanf, vsscanf jw. biorą va_list

scanf — pułapki

  • Przekazuj adresy (&a), bo funkcja zapisuje do zmiennych. scanf("%d", a) skompiluje się (może z ostrzeżeniem) i wywali w runtime.
  • %s jest już adresem — nazwa tablicy rozpada się do wskaźnika: sscanf(buf, "%s", slowo) bez &.
  • Sprawdzaj return = liczba dopasowanych pól, nie 0/1 sukcesu. Return mniejszy niż liczba % → część zmiennych nietknięta (śmieci).
if (sscanf(buf, "%d %d", &a, &b) != 2) {
        /* wejście nie pasowało do szablonu */
}
  • %s bez szerokości przepełnia cel jak sprintf. Ograniczaj: sscanf(buf, "%31s", cel) dla char cel[32] — 31 znaków + miejsce na \0.

Kernelowe warianty (lib/vsprintf.c)

Kernel nie linkuje glibc — ma własne implementacje. sscanf, snprintf, sprintf istnieją i nazywają się tak samo. Po stronie zapisu dochodzi wariant, którego w libc nie ma: scnprintf.

snprintf vs scnprintf — różnica w return

Funkcja Return przy obcięciu
snprintf (libc) długość pełnego napisu — może przekroczyć size
scnprintf (kernel) realnie zapisane bajty, maks. size - 1

Implementacja to cienka nakładka na vsnprintf:

int vscnprintf(char *buf, size_t size, const char *fmt, va_list args)
{
        int i;

        if (unlikely(!size))
                return 0;

        i = vsnprintf(buf, size, fmt, args);

        if (likely(i < size))
                return i;
        return size - 1;          /* przycięcie do size - 1 */
}

Idiom „buduj string w pętli"

To główny powód istnienia scnprintf. Bo return nigdy nie przekracza size - 1, pos nasyca się na buf_size - 1, a buf_size - pos (typ size_t, bez znaku) nigdy się nie zawija w gigantyczną liczbę.

char buf[128];
size_t buf_size = sizeof(buf);
size_t pos = 0;
int i;

for (i = 0; i < 10; i++)
        pos += scnprintf(buf + pos, buf_size - pos, "%d ", liczby[i]);
/* buf[pos] to już '\0' — scnprintf go tam postawił */

Z snprintf return może być > size → pos przeskakuje buf_size → buf_size - pos zawija się do ~2^64 → następne wywołanie rozwala bufor.

scnprintf chroni pamięć, nie treść. Gwarantuje, że nie wyjdziesz poza bufor, ale ostatni token może wylądować obcięty (np. "10" zamiast "100"). O sensowność treści przy braku miejsca musisz zadbać sam (jawny guard przed pętlą, jak w cpufreq).

Nowsze API sysfs

W handlerach show zamiast scnprintf zaleca się dziś sysfs_emit() / sysfs_emit_at() — cienkie wrappery robiące to samo, ale zakładające PAGE_SIZE i liczące offset względem początku strony (trudniej się pomylić). scnprintf zostaje do reszty: proc, debugfs, budowanie komunikatów.


Terminator '\0' — rozmiar i rola

  • '\0' to bajt o wartości 0, zajmuje 1 bajt jak każdy znak. To nie to samo co '0' (cyfra, wartość 48).
  • Rola magiczna (koniec stringa), rozmiar zwyczajny.
char a[] = {'a', 'b'};        /* sizeof == 2, BEZ nulla — NIE string  */
char b[] = {'a', 'b', '\0'};  /* sizeof == 3, terminator ręcznie      */
char c[] = "ab";              /* sizeof == 3, kompilator dokłada '\0'  */

b i c są identyczne w pamięci. Literał "ab" to skrót — kompilator zawsze dokleja \0, dlatego napis 2-znakowy zajmuje 3 bajty.

  • sizeof vs strlen: dla "ab" → sizeof == 3 (pamięć, z terminatorem), strlen == 2 (treść, bez terminatora).

Dodanie \0 do istniejącej tablicy

Nie „powiększasz" tablicy — jej rozmiar jest stały. Albo jest zapas miejsca i wpisujesz \0 pod właściwy indeks, albo kopiujesz do większego bufora.

char t[3];
memcpy(t, (char[]){'a','b'}, 2);
t[2] = '\0';                  /* teraz poprawny string "ab" */

Niuans inicjalizacji: char t[10] = {'a','b'}; — podanie choć jednego inicjalizatora w {} zeruje resztę, więc t[2] już jest \0. Ta gwarancja nie obowiązuje przy wypełnianiu w pętli / memcpy bez {}.

Przy scnprintf terminatora nie dokładasz ręcznie — funkcja stawia go sama i zostawia pos wskazujące tuż za treścią (czyli na terminatorze), więc kolejny zapis nadpisuje go i buduje ciągły string bez dziur.