C/C++ (29) - Standardní knihovna počtvrté

Dnes v rychlosti probereme locale a zpracování jednotlivých znaků, v druhé části článku dojde na standardní i nestandardní ukončování programu.

15.12.2005 06:00 | Jan Němec | přečteno 22892×

Národní prostředí

Jazyk C pochází ze 70. let a je to na něm vidět. Před 35 lety jen málo lidí od počítačů napadlo, že by někdo mohl chtít používat jinou znakovou sadu, formát čísel, data a podobně, než používají Američané. Později byla do C jakási podpora národního prostředí doplněna, ale na některých překladačích dosud nefunguje nebo funguje jen částečně (což ale není problém gcc), takže pro použití v přenositelném kódu je jen obtížně použitelná. Standardní knihovna podporuje pouze terminálový vstup a výstup, přičemž řetězce jsou jen pole bytů, která se na výstup posílají v podobě binárně shodné se zápisem ve zdrojovém kódu. Lokalizace tak má jen omezené pole působnosti, neboť neřeší například problematiku fontů nebo překódování do a z unikódu. Troufl bych si proto tvrdit, že lokalizaci programu na úrovni standardní knihovny C je lepší se vyhnout.

#include <stdio.h>
#include <locale.h>

int main(void) {
  printf("%f\n", 1.45);
  setlocale(LC_ALL, "");
  printf("%f\n", 1.45);
  return 0;
}

Na počátku programu je podle normy nastavené přenositelné "C" prostředí. Funkce printf proto vypíše číslovku s desetinnou tečkou. Volání setlocale změní charakter prostředí ve všech (LC_ALL) oblastech (znaková sada, výpis čísel, ...) na výchozí (parametr "") na daném počítači. Na mém česky nainstalovaném Mandriva Linuxu 2006 s gcc 4.0.1 a glibc 2.3.5 uvedený program oddělí při druhém volání printf desetinná místa čárkou. Na českých Windows 98 s MS Visual C++ 6.0 se uvedený program choval stejně, ale níže uvedený převod národních znaků na velká písmena pomocí toupper mi fungoval pouze s gcc na Linuxu.

Lokalizace prostředí na úrovni standardní knihovny je poměrně nebezpečná záležitost. Uživatel možná rád uvidí desetinou čárku místo tečky (toho ovšem lze snadno docílit i bez speciální podpory standardní knihovny), ale pokud jiná část programu například pomocí printf generuje SQL dotazy, kde musí být oddělovačem tečka, je problém na světě. Proto, pokud programátor potřebuje nastavovat locale, je vhodné místo všech oblastí (LC_ALL) použít pouze tu, kde je to nezbytné, například LC_NUMERIC, LC_CTYPE, LC_MESSAGES a podobně. Zájemce, které se mi nepodařilo odradit, odkazuji na manuálovou stránku setlocale.

Práce se znaky

Hlavičkový soubor ctype.h obsahuje řadu funkcí a maker pro detekci typu znaku a konverzi mezi malými a velkými písmeny. Hodit by se mohly zejména tyto rutiny s dostatečně výmluvnými názvy:

int islapha(int c); /* Je znak písmeno? */
int isdigit(int c); /* Je znak číslice? */
int isalnum(int c); /* Je znak písmeno nebo číslice? */
int islower(int c); /* Je znak malé písmeno? */
int isupper(int c); /* Je znak velké písmeno? */
int isprint(int c); /* Je znak tisknutelný? */
int isspace(int c); /* Jde o "bílý" znak? (mezera, tab, ...) */
int tolower(int c); /* Velká písmena na malá, ostatní znaky nechat. */
int toupper(int c); /* Malá písmena na velká, ostatní znaky nechat. */

Co vlastně je nebo není písmeno závisí na znakové sadě a tím pádem i na locale. Ve výchozím nastavení "C" se za písmena považují pouze znaky anglické abecedy. Při nastavení českého locale, budou rutiny fungovat i pro znaky s diakritikou, ale jen s kvalitním překladačem/libc a jen pokud používáte stejnou znakovou sadu, kterou považuje překladač/libc za výchozí pro zvolené locale.

#include <ctype.h>
...

char *pc;
int silne = 0;

pc = heslo;
while (*pc) {
  if (isdigit(*pc)) {
    silne = 1;
    break;
  }
}
if (!silne) {
  puts("Slabé heslo, neobsahuje ani jednu číslici!");
}

Ukončení programu

Zatím jsme vždy ukončovali chod programu pomocí return ve funkci main. V některých případech může být tento postup nešikovný, běžným případem jsou rekurzivní nebo jiná vnořená volání funkcí, kdy program zjistí, že by se měl ukončit v nejvnitřnějším volání a je příliš komplikované zajistit návrat ze všech instancí všech funkcí. Dalším případem je ukončení programu programátorem knihovny, který nemůže ovlivnit volající kód. Standardní knihovna proto nabízí funkce abort, exit a atexit.

#include <stdlib.h>

void abort(void);
void exit(int status);
int atexit(void (*function)(void));

Funkce abort zajistí okamžité chybové ukončení programu, na linuxu signálem SIGABRT. Buffery všech otevřených souborů by měly být zapsány a soubory uzavřeny, ale programátor by na to pochopitelně neměl spoléhat. Funkce je určena především pro nestandardní ukončení programu při ladění, případně po nějaké velmi vážné chybě programu, kdy už nemá smysl snažit se cokoli zachránit a kdy je záměrem uživatele informovat následující hláškou:

[honza@localhost ~]$ ./abort
Neúspěšně ukončen (SIGABRT)
[honza@localhost ~]$

Ukončení programu mimo main nemusí vždy znamenat chybu, případně chyba není tak vážná, aby bylo nutné program ukončovat nestandardním způsobem. V tom případě použijeme funkci exit. Její volání (včetně parametru) odpovídá return ve funkci main. Jedná-li se o řádné ukončení programu, voláme exit(0), v opačném případě s nějakou nenulovou hodnotou nebo lépe exit(EXIT_SUCCESS) a exit(EXIT_FAILURE).

Občas se hodí provést na konci programu nějakou akci bez ohledu na to, zda byl programu ukončen returnem v main nebo funkcí exit. Není jednoduché toho dosáhnout, neboť program může být ukončen i z knihovny, kterou nemá programátor pod kontrolou. Naštěstí existuje funkce atexit, která zaregistruje uživatelskou funkci, jež bude runtimem zavolaná v době ukončení programu. Je-li funkcí více, volají se v opačném pořadí, než byly zaregistrovány.

#include <stdio.h>
#include <stdlib.h>

void po_main(void) {
  puts("po_main");
}

void nakonec(void) {
  puts("nakonec");
}

int main(void) {
  puts("main - začátek");
  
  puts("registruji nakonec");
  atexit(nakonec);

  puts("registruji po_main");
  atexit(po_main);

  puts("main - konec pomocí exit");

  exit(EXIT_SUCCESS); /* Jen test funkce exit, return by to zvládnul stejně. */


  puts("Sem bych se neměl dostat...");
  return 0;
}

Ladění s pomocí assert

Při vývoji větších projektů je třeba počítat s chybami. Nejobtížněji se hledají chyby typu přepsání paměti za polem nebo přístupu na neinicializovaný ukazatel, neboť se projevují jen někdy a často až dodatečně a daleko od místa chyby. Do programu se proto běžně přidává kód, který se snaží chyby odhalit již v místě jejich vzniku a zpravidla jen vypíše chybovou hlášku a ukončí program. Podobný kód však může zbytečně zdržovat již odladěný program a navíc v distribuční verzi (zejména komerčního software) může být někdy přiznání chyby a násilné ukončení programu větší zlo než chyba sama o sobě. Již víme, že s pomocí makra preprocesoru a podmíněného překladu můžeme vyvolávat ladící testy jen ve vývojové verzi programu. Standardní knihovna se nám to snaží zjednodušit a nabízí rutinu assert.

#include <assert.h>

int main(void) {

  int pole[10];
  int i;
  
  for (i = 0; i <= 10; i++) {
    assert(i < sizeof(pole) / sizeof(int));
    pole[i] = 0;
  }
  return 0;
}

V příkladu se snažím vynulovat pole, ale dopustil jsem se známé chyby "+1", kdy přistupuji o jeden prvek za konec pole. Chování programu v tomto případě není definované, záleží na tom, co se nachází za polem. Naštěstí jsem vložil do cyklu ladící test assert, který v případě nesplnění podmínky ukončí program pomocí funkce abort a vypíše chybovou hlášku:

assert: assert.c:10: main: Assertion `i < sizeof(pole) / sizeof(int)' failed.

Ladící testy můžeme snadno vypnout, pokud definujeme makro NDEBUG. Rozhodující je přitom místo (posledního) inkludu hlavičkového souboru assert.h, #define NDEBUG tedy musí předcházet #include <assert.h>. Nejlepší je ovšem definice pomocí parametru překladače:

gcc assert.c -o assert -DNDEBUG

Uvedený příklad je trochu vykonstruovaný. Programátor, který správně testuje meze polí přímo v cyklu pravděpodobně neudělá chybu v ukončovací podmínce toho samého cyklu. V praxi se spíše setkáme s testováním parametrů funkcí, užitečné je to zejména pokud jeden programátor napíše knihovnu poskytující sadu funkcí (jejichž parametrem například nesmí být NULL) a jiný programátor tuto knihovnu používá.

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

Standardní knihovnu jsme již probrali. V příštím dílu se zamyslíme nad tím, co ve standardní knihovně nejvíce chybí. Vydalo by to na několik dalších samostatných seriálů, proto vždy uvedu jen stručný popis a především odkaz na web, který se problematice věnuje. Ze základní syntaxe jazyka C nám chybí výčtový typ, i na něj dojde v příštím dílu.

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