Je "Céčko" opravdu příliš složité?

1. 2. 1998

Sdílet

Mezi programátory se lze často setkat s názorem, že programování v jazyce C (natožpak v C++) je natolik složité, ...


Mezi programátory se lze často setkat s názorem, že programování
v jazyce C (natožpak v C++) je natolik složité, že je daleko
lepší naučit se a používat raději nějaký jiný jazyk.
V následujícím malém příkladu se pokusím ukázat, že s trochou
logického uvažování lze pochopit i na první pohled poněkud
nejasný zápis a posléze jej i aplikovat ve vlastní tvorbě.
Podívejme se tedy například na jedno zajímavé použití operátoru
inkrementace (++). Třeba takto lze v C++ kopírovat typický
řetězec zakončený nulou:

void cpy(char* r, const char* s) {

while(*r++ = *s++);

}

Právě v zápise typu while (*r++ = *s++); tkví oblíbenost jazyka
C/C++, přestože je více než trochu záhadný pro toho, kdo
v C příliš neprogramuje. Jelikož tento druh zápisu není ve
zdrojových textech ničím neobvyklým, jistě si zaslouží i naše
bližší zkoumání.
Postaví-li nás znenadání někdo před problém kopírování řetězce,
první způsob, který nás napadne, může například používat řetězec
jako pole znaků:

int delka = strlen(s);

for (int i=0; i<=delka; i++) r[i] = s[i];

Uvedený zápis však zavání neefektivitou: řetězec je ukončen nulou
a k zjištění jeho velikosti je nutné jej prohledat od začátku,
takže vlastně řetězec přečteme dvakrát - jednou při zjišťování
délky, podruhé při vlastním kopírování. Ve smyslu tohoto poznatku
zkusíme tohle:

for (int i = 0; s[i] != 0; i++) r[i] = s[i];

r[i] = 0; // a zakončíme nulou

Proměnná i, použitá jako index pole, může být klidně vynechána,
protože r a s jsou ukazatele.

while (*s != 0) {

*r = *s;

r++; // posun v 1. řetězci

s++; // posun v 2. řetězci

}

*r = 0; // nezapomeneme zakončit nulou

Víme také, že postfixové operace umožňují napřed použít hodnotu
a potom ji teprve zvýší:

while (*s != 0) {

*r++ = *s++;

}

*r = 0; // a opět nezbytná nula

Dále si musíme uvědomit, že hodnota *r++ = *s++ je *s. Snadno
tedy kód upravíme:

while ((*r++ == *s++) != 0) { }

Zde vidíme, že *s je nulové až po překopírování do *r, takže jsme
mohli vyloučit i dodatečné zapisování koncové nuly. Tuto verzi
nadto můžeme ještě zjednodušit tím, že si uvědomíme nepotřebnost
prázdného bloku příkazů, a že !=0 je nadbytečné, neboť každý
výsledek podmíněného výrazu se vždy porovnává s nulovou hodnotou.
S nemalým překvapením tedy zjišťujeme, že finální verze while
(*r++ = *s++); se shoduje s kódem, který jsme se rozhodli
analyzovat.
Nakonec však zbývá to nejdůležitější, totiž zda je tento tvar
srozumitelnější než verze předchozí a jaké má časové a paměťové
nároky na svoji činnost. Pomineme-li první verzi s použitím
strlen(), pak jsou tyto verze v podstatě ekvivalentní, konečný
výsledek bude záviset na konkrétním překladači a na architektuře
vašeho počítače. (Nejúčinnější verzí by měla být standardní
funkce na kopírování řetězců int strcpy(char* , const char*) ze
<string.h>.) Hodnocení srozumitelnosti pak záleží na vašich
znalostech, ale vezmeme-li v potaz eleganci kódu, jasně vítězí
krátký jednořádkový příkaz před daleko méně přehlednými verzemi.


Autor článku