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.1 Úvod do jazyka C

Programovací jazyk C je pro Unix nepostradatelný. Je určen zejména pro systémové programování, nachází však aplikace i jinde. Kompilátory existují na všech známých procesorech (PDP-11, VAX, IBM/370, MC68000, I8086,...).

Značnou měrou se zasloužil o přenesení Unixu na počítače s těmito procesory. Jazyk C vypadá jako jazyk tzv. nízké úrovně, protože pracuje se stejnými objekty jako současné procesory (slova, znaky, adresy, odpovídající výběr operátorů), avšak blíží se i jazykům tzv. vyšší úrovně tím, že nabízí peostředky strukturovaného programování (dobře strukturované řízení, datové struktury, funkce).

Jazyk C je jednoduchý a malý. Základní příručka, dokument Unixu, má 43 stran textu. Jazyk neobsahuje operace pracující se strukturovanými objekty (jako např. řetězec, pole, seznam či struktura) jako s celkem. Neobsahuje prostředky pro dynamické přidělování paměti ani prostředky vstupu a výstupu či prostředky paralerního programování.

Přesto toho v jazyku C zbývá dost na to, aby zůstal úspěšným programovacím jazykem. V tomto článku se pokusíme uvést všechny podstatné rysy jazyka C tak, jak vznikal současně s Unixem. Čtenář by po jeho pročtení měl porozumět příkladům v dalších kapitolách. V dalších článcích této kapitoly však popisujeme C podrobněji v shodě s doporučením [26].

5.1.1 Základní typy

Základní typy objektů v jazyce C jsou:

charslabika, prostor pro uložení znaku,
intcelé číslo v přesnosti určené strojovou reprezentací,
floatčíslo v pohyblivé čárce, jednoduchá přesnost
doublecelé číslo v pohyblivé čárce s dvojitou přesností.

Kromě toho je možné upřesnit základní typ specifikátorem délky: short, long a unsigned se užívají pro typ int a mohou být užity bez klíčového slova int jako samostatné označení typu. Délky reprezentací závisí na implementaci kompilátoru, jazyk jen požaduje, aby short nebyl delší než int a int delší než long. Specifikátor unsigned se užívá pro operandy aritmetiky mod 2n, kde n je počet bitů reprezentace (nezáporná celá čísla). Jsou dále povoleny kombinace jako unsigned char či long float. V následující tabulce uvádíme délky reprezentací v bitech:

TypPočet bitů pro slovo délky
16 bitů32 bitů
char88
int1632
short1616
long3232
unsigned1632
float3232
double6464

V jazyku C chybí typ boolean nebo logical, logické operace jsou definovány pro typ int (nulová hodnota znamená false, nenulová true).

Všechny proměnné musí být deklarovány. Příkladem deklarace může být:

char c;
int i,j,k;
double x,y;

Jsou-li proměnné deklarovány ve funkci, jsou automaticky zřizovány při každé aktivaci funkce na zásobníku a opět rušeny při návratu z funkce. Mohou být deklarovány jako registrované proměnné. To je však chápáno pouze jako doporučení pro kompilátor. Proměnné deklarované mimo funkce jsou statické a existují po celou dobu provádění programu.

Proměnné deklarované mimo funkce jsou rovněž globální, tj. viditelné i z jiných, separátně kompilovaných modulů.

5.1.2 Odvozené typy

Ze základních typů lze v jazyku C odvodit typy odvozené. Mezi ně patří ukazatel, pole a struktura.

Ukazatel je velmi silnou a také poměrně nebezpečnou zbraní jazyka C. Ukazatel reprezentuje adresu objektu. Neexistuje speciální deklarátor, v deklaraci se užívá operátoru indirekce (získání hodnoty, nepřímého adresování), znaku *, např.:

int i, *p;
char c, *q;
float w, *pf;

deklaruje i jako proměnnou typu int, p jako ukazatel na objekt typu int; podobně q je ukazatel na char, pf ukazatel na float. Je-li p ukazatel , zápis *p značí hodnotu objektu, na který p ukazuje, tj. jehož adresu p obsahuje. S ukazatelem je vždy spjat typ objektu, na který ukazuje; tato vlastnost se využívá v ukazatelové aritmetice.

Pole jsou pouze jednorozměrná, s indexem od nuly. Dvou- a vícerozměrná pole se vytvářejí jako pole polí atd. Zápis

float a[3], b[2][3];

deklaruje pole a[0], a[1], a[2] tří proměnných a pole tří polí se dvěma prvky b[0][0], b[0][1], b[0][2], b[1][0], b[1][1], b[1][2], tj. šest proměnných (s uložením po řádcích) typu float. Kompilátor chápe jméno pole jako ukazatel (na objekt stejného vytvářejícího typu) a naplní ho adresou prvního prvku pole. Zápis reference

*(a+2)

má pak stejný význam jako

a[2].

Struktura je obdobou pascalského záznamu. Deklaruje se dvoufázově klíčovým slovem struct.

V první fázi se vytvoří šablona (schéma) určující složky struktury. Šabloně se paměť nepřiděluje. Příkladem může být:

struct member {
	char name[20];
	long phone;
}

specifikuje šablonu struktury member jako agregátu pole 20 znaků a čísla typu long. V druhé fázi se zápisem

struct member Petr, Pavel, *who;

deklarují proměnné typu struct member, tj. dvě struktury member Petr a Pavel a ukazatel who na objekt typu struktura member. Těmto objektům se paměť přidělí.

Proměnné typu struktura lze deklarovat i jednofázově tak, že jména proměnných jsou vedena hned za specifikací šablony:

struct {
	char name[20];
	long phone;
} Petr, Pavel, *who;

Dvoufázový postup s uvedením jména šablony za klíčové slovo struct je však nutný v případě, kdy složkou struktury je ukazatel na stejnou strukturu, např.:

struct uzel {
	char text[10];
	int op;
	struct uzel *levy;
	struct uzel *pravy;
};
struct uzel strom[20];

Aanalogicky se postupuje u dalšího odvozeného typu, tj. unie. Klíčovým slovem je union a rozdíl mezi strukturou a unií je v přidělení paměti. Složky struktury jsou uloženy za sebou a složky unie jsou uloženy přes sebe, tj. objektu typu struct se přidělí paměť potřebná pro uložení všech složek a objektu typu unie se přidělí paměť potřebná pro nejdelší složku.

5.1.3 Konstanty, operátory, výrazy

Znakové konstanty se zapisují jako tištitelný znak mezi apostrofy, např. 'a', '*', 'ab'. Apostrofu jako významovému znaku ve znakové konstantě musí předchízet obrácené lomítko, stejně jako obrázenému lomítku: '\",'\\'.

Pro některé řídící znaky kódu (zpravidla ASCII) se využívá stejné konvence, např. znak nového řádku se zapisuje jako \n. Libovolný znak kódu může být vyjádřen svou oktalovou hodnotou, uvedenou za obrázeným lomítkem, např. '\12' je totéž co '\n'.

Celočíselné konstanty se zapisují jako posloupnost číslic, je-li první číslice nula, chápe se jako ktálový nbo hexadecimální zápsi (pak po nule následuje x, X), např. 123, 0177, 0xFFFF.

Pokud konstanta svou hodnotou přesáhne rozsah typu int, považuje se za long. Typ long lze předepsat závěrečným písmenem l, L, např. 1L, 0XFFL.

Konstanty v pohyblivé řádové čárce se zapisují obvyklým způsobem, např. 1., .78, 1E-10, 17.3e-7.

Řetězce se zapisují jako znaky v uvozovkách, např.: "abc", "UVOZOVKA\"", "Radek textu.\n".

Výrazy jsou v C tvořeny z konstant a proměnných pomocí operátorů:

Unární operátory

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

Binární operátory

=		přiřazení,
+		sčítání, 
-		odčítání, 
*		násobení, 
/		dělení, 
%		modulo (zbytek po celočíselném dělení), 
<<		posuv vlevo, 
>>		posuv vpravo, 
&		bitový AND, 
^		bitový XOR,
|		bitový OR,
&&		logický AND,
||		logický OR, 
,		čárka (zapomenutí hodnoty výrazu).

Relační operátory

<		menší než,
>		větší než,
<=		menší než nebo rovno,
>=		větší než nebo rovno,
==		rovno,
!=		nerovno.

Kromě konvenčních operátorů jsou programování v asembleru blízké operátory s bity a posuvy. Za zmínku ještě stojí, že & a * jsou využity jako unární i jako binární operátory s různým výrazem, schází umocňování.

Binární operátory se zapisují v infixové notaci, přiřazovací operátor se užívá buď konvenčně

x=y

nebo v kombinaci s jinými operátory (+=, -=, *=, /=, %=, <<=, >>=, &=, ^=, |=). Přitom např.

b[1][3] += 3.141592

má význam

b[1][3] = b[1][3] + 3.141592

se zřejmou výhodou pouze jednoho vyhodnocení levé strany.

Operátory ++, -- zvyšují, resp. snižují hodnotu celočíselných proměnných o 1. Hodnotu ukazatelů pak zvyšují (snižují) o délku objektu, na který ukazují.

Kromě uvedených operátorů obsahuje jazyk C jediný ternární operátor ?: podmíněného výrazu, např. ternární výraz

(x < y) ? x:y

nabude hodnoty x, je-li x < y, jinak hodnoty y. Přiřazení má hodnotu, může tedy být použito ve složitějších výrazech. Operátor čárky (zapomenutí) odděluje dva vrazy; takto vytvořený složený výraz má typ a hodnotu pravostranného výrazu (levostranný výraz se zapomene). Tak např.

x = 3, x++

je výraz složený ze dvou jednodušších. Prvním je přiřazení, druhým je inkrementace. První výraz se vyhodnotí, následuje čárka, operátor zapomenutí, a hodnota prvního výrazu se v dalším vyhodnocení neuplatní. V proměnné x však už je přiřazena hodnota 3. Vyhodnotí se druhý výraz (4), a to je rovněž hodnota celého složeného výrazu.

5.1.4 Příkazy

Výraz zakončený středníkem je příkazem (jedná se zpravidla o přiřazení, resp. volání funkce). K vytváření složených příkazů jsou v C k dispozici složené závorky { }.

Podmíněný příkaz má tvar např.

if(x < y)
	z = x;
else
	z = y;

část else je volitelná, případná nejednoznačnost se řeší tak, že else patří k nejbližšímu předcházejícímu if, kterému else schází.

Přepínačem se program větví na několik možných pokračování v závislosti na hodnotě celočíselného výrazu za klíčovým slovem switch. Příkladem může být určení desítkové hodnoty hexadecimální číslice:

switch(hexa){
	case 'A': dec = 10;
		break;
	...
	case 'F': dec = 15;
		break;
	default dec = hexa - '0';
}

přičemž výraz hexa se vyhodnotí a výpočet pokračuje za návěštím case, které má stejnou hodnotu. Pokud nedojde ke shodě a je uvedeno návěští default, výpočet pokračuje za ním. Není-li uvedeno default, předá se řízení za ukončovací složenou závorku. Provedení větve nekončí přechodem přes další návěští; pro ukončení větve, za kterou lexikograficky následuje další větev, slouží příkaz break.

Pro cyklus má jazyk C varianty while, do a for. První dvě

while(i > 0){			do
x += x;				x += x;
i--;				while(--i);
}

jsou klasické, třetí varianta, např.

for(i = 0; i < n ; i++)
	d[i] = 0;

je neobvykle mocná a podle definice C je tento zápsi ekvivalentní konstrukci

i=0;
while( i < n ){
	d[i] = 0;
	i++;
}

Všechny tři výrazy v závorce za klíčovým slovem for mohou být vynechány, středníky zůstávají, chybějící prostřední výraz se pokládá za trvale true. Příkaz

for(;;){
	...
}

je tedy nekonečným cyklem, který musí být opuštěn příkazem break, nebo příkazem goto (ovšem jen v rámci téže funkce).

Příkazem continue se v těle cyklu používá pro přechod na novou iteraci nejblíže nadřazeného cyklu. Chceme-li např. určit počet nenulovývh prvků pole d, můžeme zvolit tento postup:

k = 0;
for(i=0; i < n; i++){
	if( d[i] == 0 )continue;
	else k += 1;
}

5.1.5 Program, funkce

Program v jazyku C je tvořen alespoň jednou funkcí. V každém programu musí být deklarována právě jedna funkce smluveného jména main. Program, který se vyskytuje ve všech učebnicích jazyka C, je tento:

main()
{
printf("Dobry den.\n");}

Aktivací funkce main jádrem začíná provádění programu. Funkce main buď obsahuje příkazy celého programu, nebo vyvolá další funkce deklarované v programu nebo funkce externí, doplněné v etapě sestavování. V našem příkladě je to funkce printf formátovaného výstupu na terminál (čl. 5.9).

Funkce je deklarována uvedením případného typu vrácené hodnoty, jména funkce, seznamu formálních parametrů a těla funkce, tj. složeného příkazu. Není-li typ funkce uveden, považuje se implicitně za int. Za seznamem formálních parametrů může být uvedena jejich specifikace. Užívat funkce s implicitním typem int se nedoporučuje, ale ve starších programech se s nimi setkáme.

Například prázdná funkce (implicitního typu int) se deklaruje zápisem:

empty()
{
}

a funkce index, která vrátí ukazatel na hledaný znak v řetězci, se deklaruje takto:

char *index(retez, znak)
char *retez, znak;
{
do 
{
if(*retez == znak)return(retez);
} while(*retez++);
return(NULL);
}

Návratu z funkce a předání hodnoty funkce se docílí příkazem return, jinak se funkce opouští přechodem přes uzavírací závorku složeného příkazu, který tvoří tělo funkce. Uvnitř těla funkce lze používat příkaz goto s cílovým návěštím ve stejném těle.

Deklarace funkce uvnitř funkce není dovolena, struktura deklarací programu je lineární a připomíná spíše fortranovský program než vnořené pascalské blokové struktury.

Funkce je volána zápisem jména funkce a seznamu argumentů v okrouhlých závorkách. Nejsou-li argumenty uvedeny, závorky zůstávají. Tak naše prázdná funkce se volá zápisem

empty();

funkce index může být volána např. takto:

char *kde, c;
...
kde = index("bflmpsvz", c);

Argumenty jsou funkcím předávány hodnotou (všechny základní typy, ukazatele a struktury) nebo referencí (pole, řetězce a funkce). Reference zde znamená, že se předá ukazatel na první prvek pole (řetězce) nebo funkci.

5.1.6 Změna typu výrazu

V době deklarace je objektu přisouzen jeho typ. Použijeme-li ve výrazu objekty různých typů, musí dojít ke konverzi typů. Některé (rozumné) konverze provádí

[OBR. 5.1]

kompilátor automaticky. Mazi ně patří konverze mezi číslenými typy podle obr. 5.1. Konverze se provede na nejvyšší použitý typ s tím, že celočíselná aritmetika se provádí v přesnosti int nebo long, aritmetika v pohyblivé čárce v přesnosti double, nověji i v přesnosti float.

Automatická konverze se týká i přiřazovacího příkazu (tam se převádí na typ levostranného objektu) a skutečných parametrů funkcí (na int, resp. double).

Konverzi lze zadat explicitně změnou typu výrazu, uvedením žádaného typu v okrouhlých závorkách před objekt. Tak např. standardní matematické funkce očekávají argumenty typu double, tedy zápis

int n;
...
sqrt((double)n); 

explicitně vyjadřuje konverzi celočíselného argumentu na typ double.

Konverze probíhá tak, jako by hodnota argumentu byla přiřazena do proměnné odpovídajícího typu, a ta pak byla předána do funkce.

Při změně typu se v závorkách užívají tzv. abstraktní deklarátory. Vzniknou tak, že se z myšlené deklarace objektu stejného typu vynechají identifikace objektu (identifikátor, počet prvků). To, co zbude, je abstraktní deklarátor. Nejčastěji používaná přetypování (pro základní typ double):

(int)typ int,
(int *)ukazatel na int,
(int *[])pole ukazatelů na int,
(int (*)[])ukazatel na pole int,
(int *())funkce vracející ukazatel na int,
(int (*)())ukazatel na funkci vracející int.

Návod na čtení abstraktních deklarátorů se dá shrnout do tří pravidel:

Jazyk C umožňuje pojmenovat konstrukce odvozené z pojemnovaných typů nebo z typů základních (zavést synonyma). Pro zavedení nového jména slouží konstrukce typedef, udávající zleva doprava konstrukci ze známých typů, a pak všechna nově definovaná synonyma, např.

typedef int INDEX, FLAG, BOOLEAN;
typedef char *STRING;
INDEX i,j,k;
FLAG f;
BOOLEAN b;
STRING s;

Nová jména se využívají v deklaracích proměnných stejně jako typy základní a s takto deklarovanými objekty se zachází, jako by byly definovány pomocí původní konstrukce. Například s proměnnou s se zachází, jako by byla typu char *, s proměnnými i, j, k, f a b se zachází jako s objekty typu int.

5.1.7 Textový makroprocesor

Součástí kompilátoru jazyka C je textový makroprocesor. V tomto kontextu se seznámíme jen se dvěma jeho příkazy. Tyto příkazy se zpravidla nacházejí v úvodní části programu a jsou vyznačeny znakem # na první pozici řádku. Příkaz define je obdobný konstrukci typedef a užívá se zpravidla pro symbolické konstanty, např.:

#define BUFSIZE 512

kde každý výskyt symbolu BUFSIZE bude v následujícím textu programu nahrazen textem 512. Záměnu zajistí makroprocesor. Konstrukci typedef zpracovává až kompilátor.

Jde vlastně o definici makra bez parametru. Příkazem define však lze definovat i makra s jedním i více parametry:

#define TISK(x) printf("%d", x);

přičemž text

TISK(a[i])

makroprocesor rozvine na

printf("%d", a[i]);

Příkazem include se do kompilovaného programu vloží obsah souboru (místo příkazu include), např.

#include 

(uzavření jména souboru do závorek < > značí, že makroprocesor ho má hledat na konvencí stanoveném místě, v Unixu v adresáři /usr/include).

Nyní již by čtenář, kterému stačí pasívní znalost jazyka C, měl být schopen číst programy v dalších kapitolách. Jinak je třeba dočíst zbytek této kapitoly.