Jste zde

Méně známé skutečnosti o C a C++: NULL neslouží k přenositelnosti kódu

Tentokrát bych něco rád napsal o mýtu, který říká, že makro NULL slouží pro psaní přenositelného kódu, protože ne všechny architektury musí mít "nulový ukazatel" s adresou 0, ale daná adresa může být např. 0xFFFFFFFF. Ačkoliv druhá část předchozí věty pravdivá může být, tak první část nikoliv. Na konci příspěvku ukážu, že použitím makra NULL lze dokonce přenositelnost v určitých případech zhoršit!

Tento příspvěvek je založen na sekci 5 v C FAQ, takže kdo má o danou problematiku hlubší zájem, doporučuji si tuto sekci přečíst. Budu se zabývat pouze ISO C99, i když většina věcí by měla být platná i pro starší standardy. V C++ je situace obdobná, ale s makrem NULL se zde objevují další problémy (možná se o nich zmíním někdy v budoucnu).

Jak to teda je?

Na začátek bych chtěl citovat úryvek z normy ISO C99 ze sekce 6.3.2.3 (Pointers): An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. Z tohoto úryvku vyplývá, že nulový ukazatel má vždy hodnotu 0, či "castovanou" 0 (omlouvám se, ale nenapadá mě český ekvivalent) na nějaký typ ukazatele (na typu nezáleží, opět viz sekce 6.3.2.3: Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal.). Čili nulový ukazatel má ve zdrojovém kódu vždy nulovou hodnotu, nezávisle na architektuře. Pokud by měl na dané architektuře jinou hodnotu, pak je její nahrazení při překladu za danou hodnotu prací překladače (nikoliv programátora). Takto je zajištěna přenositelnost.

Makro NULL (stddef.h) je #definováno pouze proto, že některým prográmatorům může vadit, že používají konstantu 0 jako celočíselnou konstantu, tak jako hodnotu nulového ukazatele. Jeho definice je implementačně závislá, většinou se jedná o něco z následujícího:

#define NULL 0
#define NULL 0L
#define NULL ((void*)0)

Dále bych rád uvedl, že následující příkazy jsou vždy ekvivalentní:

int *p = 0;
int *p = NULL;

a tyto také:

if (p != NULL) {...}
if (p != 0) {...}
if (p) {...}

Je to dáno tím, že pokud překladač očekává pravdivostní hodnotu, pak jako pravdu bere cokoliv nenulového. Čili výraz

if (p) {...}

nahradí takto:

if (p != 0) {...}

Obdobně to funguje v případě

if (!p) {...}

Problémy s přenositelností

Jak bylo zmíněno v úvodu, tak existují situace, při kterých použití makra NULL může způsobit problémy s přenositelností. Jako příklad uvedu použití funkce int execl(const char *path, const char *arg, ...), která přijímá cestu k souboru a poté určité množství argumentů, kde poslední předaný argument musí být nulový ukazatel (aby bylo možno v této funkci zjistit, kolik se vlastně předalo argumentů, protože dochází k předávání přes výpustku). Programátora by mohlo napadnout něco takového:

execl("/bin/ls", "ls", "-a", NULL);

Problém je v tom, že pokud makro NULL je #definováno jako 0 nebo 0L, pak výsledkem preprocessingu bude

execl("/bin/ls", "ls", "-a", 0);

Jelikož překladač nemá jak zjistit, že dané funkci se musí předat nulový ukazatel (a že to programátor zamýšlel), tak místo něj předá 0 typu int, což je samozřejmě něco jiného než nulový ukazatel (int může mít odlišnou velikost od ukazatele na int, nulový ukazatel může mít odlišnou hodnotu na různých architekturách apod.)! Správné volání funkce execl() je tedy buď

execl("/bin/ls", "ls", "-a", (void *)0);

nebo

execl("/bin/ls", "ls", "-a", (void *)NULL);

Závěr

Doufám, že se mě tímto příspěvkem podařilo tento mýtus o "funkci" makra NULL vyvrátit.

Přidat komentář