C/C++ (5) - Funkce printf podruhé

Dnes dokončíme povídání o funkci printf a dojde i na jednu oblíbenou programátorskou chybu.

18.10.2004 10:00 | Jan Němec | přečteno 52543×

Pokročilé použití printf

V minulém dílu jsem ukázal jednoduché příklady na printf. V řídících sekvencích formátovacího řetězce jsem používal pouze povinné části, kterými jsou úvodní znak procento a typ. Jak již víme, řídící sekvence může být složitější:

%[příznaky][minimální šířka][přesnost][modifikátor typu]typ

Příznak může mít jednu z hodnot uvedených v tabulce. Dá se jím ovlivnit zarovnání vypsaného parametru, pokud je jeho šířka menší, než je žádoucí. Zarovnávací příznaky se proto používají hlavně ve spojení s další nepovinnou částí řídící sekvence - minimální šířkou. Příznakem lze také vynutit znaménko před číslem (např. +2 místo 2). U záporných čísel se znaménko pochopitelně vypisuje i bez příznaku automaticky. Příznak # slouží k názornějšímu výpisu parametru. Například v šestnáctkové soustavě se předřadí 0x a u reálných čísel je ve výpisu vždy desetinná tečka.

PříznakVýznam
#Vypíše typ více explicitně.
0Zleva zarovnat nulami
-Zprava zarovnat mezerami
mezeraZleva zarovnat mezerami
+Před číslem vždy znaménko

Význam položky minimální šířka je celkem zřejmý. Jde o celé číslo a pokud je vypisovaný prvek příliš krátký, text se rozšíří. Způsob rozšíření lze ovlivnit položkou příznak. Jako šířku lze uvést i znak *, šířka potom není uvedená ve formátovacím řetězci, ale jako další parametr funkce printf předcházející tomu, který chceme vypsat. Jedna řídící sekvence tak zkonzumuje dva parametry funkce printf. Ukážeme si to na příkladech.

Další na řadě je přesnost. Začíná vždy tečkou a následuje celé číslo nebo podobně jako v případě minimální šířky znak * - odkaz na hodnotu z parametru. Používá se hlavně u reálných čísel a znamená počet cifer za desetinou tečkou. Méně obvyklé je použití u celých čísel - počet všech cifer a u řetězců - maximální počet znaků, který se má vytisknout.

Nejméně významnou položkou je modifikátor typu, během sedmi let programování v C jsem se s ním v praxi nesetkal. Zájemce proto odkazuji na manuálové stránky a přistoupím raděj k příkladům.

#include <stdio.h>
int main(void) {
  /* 1 */
  printf("Pro opakování: 1 = %i\n", 1);
  /* 2 */
  printf("255 = %#x = %x\n", 255, 255);
  /* 3 */
  printf("1 = %i = %10i = % 10i = %-10i = %010i\n", 1, 1, 1, 1, 1);
  /* 4 */
  printf("%+i = %i, %+i = %i\n", 1, 1, -1, -1);
  /* 5 */
  printf("%f, %.0f, %#.0f\n", 8.3, 9.7, 5.4);
  /* 6 */
  printf("Slepice dělá %-10.*s.\n", 6, "kokokodák");
  return 0;
}

Program vypíše na standardní výstup následující text:

Pro opakování: 1 = 1
255 = 0xff = ff
1 = 1 =          1 =          1 = 1          = 0000000001
+1 = 1, -1 = -1
8.300000, 10, 5.
Slepice dělá kokoko    .

Nejprve (1) jsem zopakoval z minula jednoduchou sekvenci %i na výpis jednoho intu. Příznakem # jsem ukázal (2) předsazení 0x před šestnáctkový výpis. Potom (3) dokumentuji různé způsoby výpisu jedničky. Nejprve je ve formátovacím řetězci napsaná natvrdo, potom jako parametr, přičemž řídící sekvence neobsahuje nepovinné části. V posledních čtyřech výpisech jedničky ji rozšířím na deset znaků. Nejdřív nechám výpis bez příznaků (výchozí zarovnání, zarovná se doprava) a potom jedničku obložím zleva a zprava mezerami a nakonec zleva nulami. V příkladu (4) ukazuji vynucení znaménka + před kladným číslem a potom (5) výpis reálných čísel s přesností na 0 desetinných míst, tedy vlastně zaokrouhlení na celá čísla ve výpisu. Příznakem # si můžeme vynutit desetinou tečku. Pokud pochopíte poslední příklad (6), umíte printf. Vypisuji řetězec (%s), ze kterého se vezme jen počet znaků daný předchozím parametrem (%.*s), má navíc specifikovanou minimální šířku 10 (%10.*s), a pokud tato šířka bude větší než počet vypsaných znaků řetězce (to je náš případ), vloží se doprava patřičný počet mezer.

Doufám, že jste se nenechali poněkud komplikovanou funkcí printf odradit od Céčka. Rozhodně si nevtloukejte syntaxi řídící sekvence do hlavy. Pro úspěšné programování v C vám postačí znát význam %i, %sman 3 printf.

S printf jsou spojené závažné chyby programátorů s nepříjemnými následky. Je dost nebezpečné, že (typicky nastavený a běžný) překladač nekontroluje syntaxi formátovacího řetězce a počet a druh parametrů. Obecně to ani nelze, neboť formátovací řetězec nemusí být zadán jako konstanta, ale jako proměnná nebo obecně výraz typu const char *. Zde se právě skrývá nebezpečí, zamyslete se nad následujícím kusem kódu:

  char *s;
  s = ziskej_retezec_od_uzivatele();
  printf(s); /* Špatně!!! Dobře je printf("%s", s) */

Může jít třeba o kus www serveru a funkce ziskej_retezec_od_uzivatele vrací požadovaný soubor z URL v GET nebo POST dotazu, voláním printf se píše do logu (třeba s pomocí přesměrování). Programátor serveru vše odladí ze svého prohlížeče na URL typu http://www.mujserver.cz/index.html a nasadí do ostrého provozu. Problém nastane, až nějaký vtipálek začne posílat dotazy místo na index.html třeba na %s. Co se stane? Server zavolá printf("%s") a funkci printf pak chybí jeden parametr, ukazatel na char, řetězec, který se má vypsat kvůli sekvenci %s. Při volání printf žádná taková hodnota předána nebyla, ale to printf neví. Za ukazatel na char bude považovat data, která na místě v paměti určeném pro předávání parametrů zbyla z dřívějška a z paměti, kam tento "ukazatel" míří bude zkoušet načíst nulou ukončený řetězec. Chování programu pak závisí na architektuře, operačním systému, překladači a hlavně náhodě, na Linuxu obvykle spadne s hláškou Segmentation fault, ale běžný je i výpis nějakého nesmyslného řetězce.

Pokračování příště

Doufám, že jsem podrobným výkladem funkce printf a jejích záludností čtenáře neodradil a nezastrašil. V řadě věcí jsme trochu předběhli, ale psát programy bez výstupu by asi nikoho nebavilo. V dalším dílu se podíváme na operátory.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=447