Operační systém UNIX a jazyk C

Jan Brodský
Luděk Skočovský

SNTL Nakladatelství technické literatury Praha 1989, ISBN 80-03-00049-1

5. Programovací jazyk C


5.3 Výrazy

5.3.1 Obecná pravidla

S objektem je spojena jeho adresa (adresa přiděleného prostoru v paměti) a jeho hodnota (bitový vzorek nacházející se v přiděleném prostoru a interpretovaný ve shodě s typem objektu). Proto rozeznává jazyk C dva druhy výrazů. Adresový výraz (v angličtině lvalue, protože je obvykle na levé straně přiřazovacího výrazu) odkazuje na objekt, hodnotový výraz udává hodnotu objektu. Typickým případem obou výrazů je identifikátor, který je podle kontextu chápán jako adresa objektu (na levé straně přiřazovacího výrazu) nebo jako jeho hodnota (na pravé straně přiřazovacího výrazu). Dále např. objekt typu ukazatel má hodnotu, která je adresou jiného objektu. Je-li E hodnotový výraz typu ukazatel, pak *E je adresový výraz odkazující na objekt, na který ukazuje E. Operátorem výběru složky struktury získáme rovněž adresový výraz.

Výrazy jsou tvořeny z operandů pomocí operátorů. Je stanovena priorita a rovněž asociativa operátorů (zleva či zprava). Jinak není pořadí vyhodnocování výrazů definováno a předpokládá se, že kompilátor může toto pořadí měnit tak, aby dosáhl efektivního kódu. Tak např. není definováno pořadí vyhodnocování podvýrazů a dokonce ve výrazech s komutativními a asociativními operátory nemusí kompilátor brát ohled na uzávorkování. Jediná metoda, jak vnutit pořadí vyhodnocení je použití mezivýsledků.

Ošetření chyb při výpočtu (přeplnění apod.) není definováno. Celočíselné přeplnění je zpravidla ignorováno. Dělení nulou a chyby při aritmetice v pohyblivé řádové čárce má důsledky závislé na implementaci.

5.3.2 Primární výrazy

Primární výraz je základním stavebním kamenem všech dalších výrazů. V jazyku C k primárním výrazům patří:

První z nich je tvořen primárním výrazem, za kterým následuje tečka a identifikátor, druhá varianta využívá šipku (->) místo tečky. Tak např.

i, 123, "Leden", (i+12), a[3], f(1,2,3), s.x, ps->x
jsou všechno povolené primární výrazy. Naopak
(a) (b), [3], s.1
nejsou primární výrazy.

Typ identifikátoru v roli primárního výrazu je udán jeho deklarací; jde-li o identifikátor pole objektů, jeho hodnotou je ukazatel na první objekt a typ výrazu je ukazatel na objekt. Navíc identifikátor pole není adresovým výrazem. Obdobná přeměna na ukazatel platí rovněž pro identifikátor funkce.

O konstantách a řetězcích jsme hovořili v předešlém článku; protože řetězec je pole znaků, je ve shodě s předešlým pravidlem modifikován na ukazatel na první znak řetězce.

Uzávorkování výrazu nemění jeho typ a hodnotu, tedy např. ((((i)))) znamená totéž co i.

Indexový výraz se zpravidla používá ve spojení s poli. Jde-li např. o výraz a[i], kde a je identifikátor pole, i proměnná typu int, lze chápat hodnotu výrazu jako i-tý prvek pole a. Podle definice jsou zápisy

a[i]                  *((a) + (i)),
stejně jako zápisy
b[i][j]               *(*((b) + (i)) + (i)).

Pole jsou v jazyku C ukládána po řádcích, tedy tak, že poslední index se mění nejrychleji. Rozeberme si nyní poslední uvedený příklad pro případ deklarace

int b[3][5].

Identifikátor b se převede na ukazatel na první ze tří pětičlenných polí typu int. Výraz b[i] se chápe jako *((b)+(i)), b se opět bere jako ukazatel na pole o pěti objektech typu int, i se převede na stejný typ, tedy vynásobí se délkou ukazovaného objektu (tj. délkou pole o pěti objektech typu int), výsledek se sečte, operátor indirekce dá výsledek pole (pěti objektů int), a to se převede na ukazatel na první z těchto pěti objektů. Dále se j převede na stejný typ, tj. na int, vynásobí se délkou typu int a výsleek se sečte. Poslední nevyřízený operátor indirekce (úplně vlevo) dá očekávaný výsledek j-tý objekt z i-tého řádku pole b.

Primární výraz, tvořící první část volání funkce, musí být typu funkce vracející objekt; hodnotou volání funkce je pak vrácený objekt. Tak např. ve výrazu

f(1,2,3)
musí být f deklarováno jako funkce, ve výrazu
(*pf)(1,2,3)
musí pf být ukazatelem na funkci, aby šlo o správně utvořené volání funkce. Zde považujeme za nutné upozornit čtenáře na jistý "přátelský" , přesto však nebezpečný rys jazyka. Je jím (alespoň pro starší verze kompilátoru) skutečnost, že funkce f v programu používaná, nemusí být deklarována. Kompilátor ji totiž automaticky prohlásí za externí funkci vracející int. Technicky vzato kompilátor doplní deklaraci
extern int f();
přičemž podle návrhu normy musí být deklarována každá použitá funkce.

Při předávání skutečných parametrů se provádějí konverze typu float na typ double, char a short na int. Jména polí a funkcí se převedou na ukazatele. Jiné konverze se automaticky neprovádějí. Jsou-li třeba, musí je programátor vyznačit přetypováním. Typickým příkladem je použití celočíselného argumentu pro funkci s parametrem typu double:

sqrt((double)2).

Pořadí vyhodnocení ergumentů není jazykem definováno.

Ve složkových výrazech jde o výběr složky struktury nebo unie. Primární výraz před tečkou musí být struktura nebo unie a identifikátor za tečkou musí označovat některou její složku. Hodnota složky je hodnotou výrazu. Jde přitom o adresový výraz, byl-li primární výraz před tečkou adresovým výrazem. Šipka (->) vyžaduje, aby před ní byl použit ukazatel na strukturu nebo unii. Výsledkem je adresový výraz, ukazující na složku určenou identifikátorem. Je tedy

ps->x
totožné s
(*ps).x
za předpokladu, že ps je ukazatel na strukturu nebo unii se složkou x.

V následujících odstavcích budeme probírat jednotlivé skupiny operátorů. Učiníme tak v pořadí jejich přednosti při vyhodnocování, tzn. že unární operátory se vyhodnotí před multiplikativními operátory atd.

5.3.3 Unární operátory

Unární operátory mění hodnotu jednoho výrazu. S výjimkou operátorů inkrementace a dekrementace se zapisují před výrazem a je-li jich použito více současně, vyhodnocují se zprava doleva:

Unární operátory

*		indirekce (získání odkazovaného objektu),
&		adresa (získání adresy objektu),
-		aritmetické minus,
!		logická negace (změní nulu na jedničku , nenulu
		na nulu),
~		bitový komplement (změní bitové nuly
		na jedničky a naopak),
++		zvýšení hodnoty před, resp. po vyhodnocení 
		následujícího, resp. předcházejícího operandu
		(inkrementace),
--		snížení hodnoty před, resp. po vyhodnocení,
		následujícího, resp. předcházejícího operandu,
		(dekrementace),
(type)		explicitní převod na typ v závorkách,
sizeof		délka objektu, typu.

Operátor indirekce očekává ukazatel na objekt a vrací tento objekt; je-li pi ukazatel na int, je *pi odkazovaný objekt typu int. Naopak ukazatel na objekt je výsledkem operátoru adresy aplikovaného na hodnotový výraz (např. na identifikátor). Můžeme tedy psát např.

pi=&i;
a *&pi znamená totéž co i. Identifikátor i je je objekt typu int, tedy obsahuje celé číslo, např. 5; &i je adresa objektu i, * znamená vzetí obsahu adresy, &*i je chybně vytvořen, protože i není typu ukazatel.

Operátor logické negace je překvapivě použitelný na aritmetické typy a ukazatele, dává výsledek typu int. Opět připomínáme, že nulová hodnota se implementuje jako false, nenulová jako true. Ukazatel má nulovou hodnotu, neodkazuje-li na žádný objekt.

Bitový komplement lze použít pouze na celočíselné typy. Operátory inkrementace a dekrementace pracují s adresovým výrazem. Výsledek závisí na tom, zda jsou použity v prefixové nebo postfixové notaci. V prefixové notaci je výsledkem zvýšená nebo snížená hodnota objektu určeného adresovým výrazem, např. ++i je ekvivalentní i=i+1, analogicky --i je ekvivalentní i=i-1.

V postfixové notaci je výsledkem hodnota objektu určeného adresovým výrazem a hodnota objektu je následně zvýšena, resp. snížena.

Zvýšení nebo snížení závisí na typu objektu. Pro cesločíselné typy se hodnota změní o 1, pro typ ukazatel na objekt v poli dojde k přesunutí na ukazatele na následující, resp. předcházející objekt pole. Obě použití operátorů inkrementace najdeme ve funkci strlen, která vrátí počet znaků argumentu - řetězce:

int strlen(s)
char *s;
{
	int i;
	i = 0;
	while(*s++)
		i++;
	return(i);
}

Přetypování byl již věnován odst. 5.1.6 a bude ještě diskutováno v čl. 5.4.

Operátor sizeof dává jako výsledek počet slabik reprezentace objektu nebo typu. Nikde jinde se v definici jazyka o slabice nemluví a předpokládá se, že slabika je přesně prostor pro char. Typ výsledku je unsigned a chápe se jako konstanta (odvodí se z deklarací objektů operandu nebo použitého typu).

Kontrolním testem implementace může být např. program

main(){
if(sizeof(char) != 1)
	printf("chybna implementace\n");
}
Operandem sizeof může být pole, struktura a unie. Pak výsledkem je počet slabik reprezentace celého pole, struktury či unie.

Tak např. úryvek programu:

char s[20];
unsigned j;
   ...
j = sizeof(s);
by měl v proměnné j zanechat hodnotu 20.

5.3.4 Multiplikativní operátory

Jsou to operátory násobení *, dělení / a modulo % a jsou vyhodnocovány zleva doprava. Operátor násobení je asociativní a komutativní (což platí i pro operátor sčítání), takže kompilátor může změnit libovolně pořadí vyhodnocování i v případě uzávorkování. Tak např. a * (b * c) může být vyhodnoceno jako (a * b) *c.

Na hodnotu výsledku to má vliv jen ve zcela výjimečných případech a pak je třeba využít mezivýsledkové proměnné, např.

x = b *c;
a použít výraz a * x.

Operátor dělení zaokrouhlí výsledek dělení dvou celočíselných kladných operandů směrem k nule; je-li však jeden z operandů záporný, je způsob zaokrouhlování závislý na implementaci. Zpravidla má zbytek stejné znaménko jako dělenec. Tak např. 7/4 je 1 a -7/4 je -1.

Analogické vlastnosti má operátor modulo. Tedy 7%4 dá výsledek 3 a -7%4 dá výsledek -3.

Pro celočíslená a, b vždy platí, že (a / b) * b + a % b = a, pro b různé od 0.

5.3.5 Aditivní operátory

Jsou jen dva, sčítání + a odčítíní - a jsou vyhodnocovány zleva doprava. Pro operandy aritmetických typů znamenají obvyklé aritmetické operace. Obou operací se však mohou zúčastnit i operandy typu ukazatel. Možné kombinace uvádí tabulka:
Operátor První operand Druhý operand Výsledek
+ aritmetický ukazatel aritmetický int aritmetický ukazatel
- aritmetický ukazatel aritmetický ukazatel aritmetický int

Při sčítání ukazatele na objekt v poli a celého čísla se celé číslo převede na posunutí adresy tak, že se vynásobí velikostí objektu. Je-li tedy p ukazatel na objekt v poli, pak výraz p + 1 ukazuje na následující objekt, p - 1 na objekt předcházející. To odpovídá použití operátorů inkrementace p++ a dekrementace p--.

Odečtením dvou ukazatelů dostaneme vzdálenost obou objektů, tj. počet objektů, která je v poli dělí (vzdálenost sousedních objektů je rovna 1). To se využívá v následující variantě funkce strlen:

int strlen(s)
char *s;
{
	char *p;
	p = s;
	while(*p++);
return(p-s);
}

5.3.6 Operátory posuvů

Operátory posuvů jsou dva, posuv vlevo a vpravo a jejich operandy musí být celočíselné. Značí se dvojicemi znaků << a >>. Levostranný operand se posouvá o počet bitů udaný pravostranným operandem. Výsledek posuvu není definován, je-li pravostranný operand záporný nebo větší než počet bitů reprezentace levostranného objektu.

Při posuvu vlevo jsou bity zprava nulovány. Při posuvu vpravo jsou bity zleva nulovány pouze pro objekty bez znaménka (unsigned) a výsledkem je logický posuv. U jiných objektů se do bitů zleva kopíruje znaménkový bit a jde o aritmetický posuv. Operátor posuvů vpravo můžeme s úspěchem využít v celočíselnéfunkci nbint, vracející počet bitů reprezentace typu int:

int nbint()
{
	int i; unsigned w;
	i = 0; w = ~0;
	while(w){
		w = w >> 1;
		i++;
	}
	return(i);
}

Konstantní výraz ~0 je převeden na typ int a bitový komplement naplní všechny bity reprezentace w jedničkami. Proměnná w se tak dlouho posouvá vpravo o jeden bit, až se zleva vynulují všechny bity.

5.3.7 Relační operátory, operátory rovnosti

Provnávat lze aritmetické typy a ukazatele na objekty v jednom poli. Povolené relace jsou menší <, větší >, menší nebo rovno <= a větší nebo rovno >=. Výsledkem typu int je 1, je-li relace splněna, nebo 0, když splněna není.

Příkladem může být úryvek programu hledající menší ze dvou hodnot x, y:

...
if(x < y)
	min = x;
else
	min = y;
...

Pro ukazatele se relace menší interpretuje jako relace předchozí. Operátory rovnosti == a nerovnosti != mají shodné vlastnosti s relačními operátory, ale mají nižší prioritu.

Máme-li např. naprogramovat funkci, která určí počet výskytů znaku v řetězci, můžeme to učinit takto:

int match(s, c)
char *s,c;
{
	int i;
	while(*s){
		if(*s+ + = = c)
		i++;
	}
	return(i);
}
V podmínce cyklu while nelze ukazatel s zvyšovat na následující znak, protože zkoumaný znak budeme ještě testovat na shodu s argumentem c.

Na rovnost či nerovnost můžeme testovat i ukazatele. Lze dokonce testovat, zda ukazatel má nebo nemá nulovou hodnotu, tj. zda neodkazuje nebo odkazuje na nějaký objekt. Doporučuje se však využít symbolickou konstantu NULL (aby se vyloučila) závislost na implementaci ukazatelů).

5.3.8 Bitové logické operátory

Jde o operátory bitových operací logického součinu &, nonekvivalence ^ a logického součtu |. Operandy musí být celočíselné a jejich priorita klesá od součinu k součtu.

Ukažme si jejich použití na příkladě funkce swap, která vrátí argument se zaměněným pořadím slabik:

unsigned swap(w)
unsigned w;
{
	return( (w&0377) << 8 | w>>8);
}

5.3.9 Logické operátory

Logické operátory součinu && a součtu || se vyhodnocují vždy zleva doprava. Výsledkem typu int je v případě součinu hodnota 1 (true), je-li alespoň jeden z ooperandů nenulový (true). V obou případech se druhý operand nevyhodnocuje, je-li znám výsledek již po vyhodnocení prvního operandu. Pěkným příkladem je funkce hexa, která zjistí, zda zadaný znak je hexadecimální číslicí:

int hexa(c)
char c;
{
	if(c >= '0' && c <= '9' ||
	   c >= 'A' && c <= 'F' ||
	   c >= 'a' && c <= 'f')
		return(1);
	else
		return(0);
}

Logický součin má přednost před logickým součtem. Je třeba rozlišovat bitové logické a logické operátory. podívejme se, jaké hodnoty bude postupně mít proměnná z:

...
int x,y,z;
x = 1;
x = 2;
z = x & y;
z = x && y;
z = x | y;
z = x || y;
...
(0, 0, 3 a 1).

5.3.10 Podmíněný operátor

Podmíněný operátor je jediným ternárním operátorem jazyka C. Má tvar

E1 ? E2 : E3
a jeho výsledkem je typ a hodnota výrazu E2, je-li E1 nenulový (true), jinak typ a hodnota výrazu E3. Typy výrazů E2 a E3 musí být shodné a vždy se vyhodnocuje jediný z nich. Například minimální hodnotu lze najít takto:
...
min = x < y ? x : y;
...
pdobně se zjistí absolutní hodnota:
...
abs = x < 0 ? -x : x.
...

5.3.11 Přiřazovací operátory

Přiřazovací operátory jsou =, +=, -=, *=, /=, %=, >>=, <<=, &=, ^=, |=. Na levé straně přiřazovacího operátoru musí být adresový výraz a hodnota výrazu na pravé straně nahradí objekt jím odkazovaný. Součástí přiřazení je konverze aritmetických typů, jinak musí být výrazy na obou stranách stejného typu. Jde-li např. o struktury, resp. unie, musí mít stejný typ (tj. šablonu), jde-li o ukazatele, musí ukazovat na objekt stejného typu.

První uvedený operátor = je prosté přiřazení. Všechny další jsou kombinované binární operátory s přiřazením. Výraz

E1 op = E2
se chápe jako E1 = E1 op E2, ale s tím, že E1 se vyhodnocuje pouze jednou.

Pro operátory + = a - = může být levý operand ukazatelem a pravý operand typu int. Pak platí totéž, co pro aditivní operátory (odst. 5.3.5). Operátory přiřazení jsou asociativní zprava doleva, tedy

x = y = z = 0;
se provede jako
x = (y = (z = 0));

Přiřazení je výrazem, výsledkem je přiřazovací hodnota (v našem příkladě nula).

5.3.12 Operátor čárky

Operátor čárky (zapomenutí) odděluje dva výrazy a vyhodnocuje se vždy zleva doprava. Výsledkem je typ a hodnota druhého výrazu, první výraz však může být přiřazovací a mít vedlejší efekty. Tak např. výsledkem výrazu

x = 0, ++y
je hodnota y zvýšená o jedničku, s vedlejším efektem vynulování x.

Znak čárky se však používá jako oddělovač v seznamu argumentů ve volání funkce a v inicializačním seznamu (odst. 5.6.2 a 5.4.5). Má-li být operátor čárky použit i na těchto místech, musí být výraz opatřen závorkami. Tak např. volání funkce

f(x,(y = 0, y + 2), z)
má tři argumenty, z nichž druhý má hodnotu 2.