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.4 Deklarace

5.4.1 Struktura deklarací

Deklarací se identifikátoru přisoudí jeho význam v kontextu programu. Ve většině případů je to paměťová třída a typ objektu, v deklaraci typu konstrukcí typedef jde o přejmenování typů.

Paměťové třídy objektů jsou auto, static, extern a register, odst. 5.2.3. Dodejme zde, že paměťová třída nemusí být explicitně uvedena a pak je určena z kontextu. Objekty deklarované uvnitř bloku některé funkce jsou třídy auto, objekty deklarované mimo funkce jsou třídy extern. Tak např. v úryvku programu

int i;
int f(x);
int x;
{
	int y;
	...
}
jsou proměnná i a funkce f třídy extern, parametr x a proměnná y třídy auto.

Objektu smí být explicitně přisouzena nejvýše jedna paměťová třída. Typ objektu je dán konstrukcí, která následuje za případným určením paměťové třídy. Může to být základní typ (se specifikací délky), struktura nebo unie, výčtový typ nebo jméno typu vytvořeného mechanismem typedef.

Pro určení základního typu se užívají klíčová slova: char, short, int, long, unsigned, float, double, void. Typ void (prázdný) se užívá v deklaracích funkcí a označuje funkci, která nevrací žádnou hodnotu - tedy proceduru v pascalském smyslu. Klíčová slova short a long se mohou využít jako specifikace délky typu int, přitom short int má stejný význam jako short, long int má stejný význam jako long.

Chybí-li totiž určení základního typu, považuje se za int. Přípustné je rovněž long float s významem double a long double pro čtyřnásobnou přesnost.

Samostatné klíčové slovo unsigned má význam unsigned int, lze je však použít pro všechny celočíselné typy, tedy unsigned char, unsigned short int, unsigned long apod.

Specifikace délky, resp. unsigned není povolena v kombinaci se jménem typu vzniklého mechanismem typedef.

Za určením paměťové třídy a typu následuje seznam deklarovaných objektů. Ve většině programovacích jazyků jde o seznam identifikátorů objektů, v jazyku C je situace poněkud složitější.

5.4.2 Deklarace elementárních objektů

Identifikátor I je povolen a označuje objekt určené paměťové třídy a typu. Jde však o nejjednodušší případ deklarátoru označeného např. D, jehož konstrukce se tvoří podle schématu:

D:	I
	*D
	D()
	D[]
	(D)

Čteme po řadě takto: deklarátor je identifikátor, ukazatel na objekt určeného typu, funkce vracející určený typ, pole objektů určeného typu. Poslední řádek schématu povoluje uzávorkování pro složitější konstrukce. Každý deklarátor musí obsahovat jediný identifikátor, kterému se deklarací přisoudí jeho význam.

Tak deklarace

int i, *pi, fi(), ai[], *fpi(), (*pfi)();
deklaruje postupně proměnnou i typu int, ukazatel pi na int, funkci fi, vracející int, pole ai objektů typu int, funkci fpi, vracející ukazatel na int a ukazatel pfi na funkci, vracející int.

Myšlenkou, stojící za komplikovanou a nepřehlednou formou deklarací, je stejný zápis ve výrazech, které deklarované objekty používají. Tak např. přiřazení:

i = (*pfi)();
je ve shodě s deklarací. Identifikátor pfi je ukazatel na funkci. Použitím operátoru indirekce * dostaneme funkci, ta je vyvolána a vrátí objekt typu int, který je přiřazen do proměnné i.

Pole může být vytvořeno z objektů základních typů, ukazatelů, struktur nebo unií a polí (vícerozměrná pole). V lomených závorkách je uveden konstantní výraz s kladnou hodnotou vyhodnocený v době kompilace a určující počet objektů v poli. Tento výraz může chybět pouze v prvním rozměru pole. To je užitečné opatření v případě externích polí, kdy skutečná deklarace pole, alokující paměť, je umístěna v jiném programovém modulu. První rozměr pole může rovněž chybět v případě, kdy se dá určit z počtu iniciálních hodnot pole (odst. 5.4.5). Například

double ad[100], *pad[20];
deklaruje pole ad 100 čísel typu double a pole 20 ukazatelů na double. Deklarace
int x[2][4][6];
deklaruje pole x o dvou prvcích, což jsou pole, sestávající ze čtyř polí, z nichž každé obsahuje šest objektů typu int. Obvyklé chápání je třírozměrné pole. V programu se však mohou objevit výrazy
x, x[i], x[i][j], x[i][j][k]
kde první tři jsou typu pole (a převedeny na ukazatel na první objekt příslušeného pole), poslední je typu int.

5.4.3 Struktury a unie

Struktura je agregovaný objekt sestávající z posloupnosti pojmenovaných složek (pascalský záznam). Unie je výběrový objekt, obsahující vždy jednu z několika pojmenovaných složek, tj. bitový vzorek uložený v paměti pro unii se interpretuje shodně s typem vybrané složky (pascalské variantní složky záznamu). Složky struktury nebo unie mohou být libovolného typu. V deklaraci struktury nebo unie je podstatná šablona (srov. s odst. 5.1.2), která pouze určí, jak budou objekty tohoto typu vypadat, např.

struct  rodcislo {
	short den;
	short mesic;
	short rok;
	int kod;
}

Objekt struktury rodcislo sestává ze tří složek short a jedné složky typu int. Za šablonou může ihned následovat seznam deklarátorů objektů:

struct  rodcislo {
	short den;
	short mesic;
	short rok;
	int kod;
} dd, *pdd;

Identifikátor dd označuje objekt typu struct rodcislo, identifikátor pdd ukazatel na objekt typu struct rodcislo. Je-li struktura použita v programu vícekrát, je výhodnější použít šablonu jako definici nového typu struct rodcislo a později deklaraci tvaru

struct rodcislo dd, *pdd;

Pvolené výrazy jsou

dd.rok = 1950;
pdd = ⅆ
pdd->den = 1;
a výraz
++pdd->den
je nejasný jen na první pohled. Otázkou je, zda se zvýší ukazatel pdd nebo složka den struktury dd. Priorita operátorů jasně určí, že se nejdříve použije operátor výběru -> složky a potom unární operátor inkrementace. Zvýší se den.

Obdobně můžeme deklarovat šablonu

struct osoba {
	char jmeno[15];
	char prijmeni[20];
	struct rodcislo rc;
}
a objekty deklarací
struct osoba sapo[60], *pos;

Objekt sapo je polem šedesáti struktur typu osoba; každá sestává ze dvou polí char a struktury rodcislo, obsahující tři objekty typu short a jeden objekt typu int.

Chceme-li např. naplnit rok narození i-té osobě sapo, můžeme tak učinit výrazem

sapo[i].rc.rok = 1900;
nebo
pos = &sapo[i];
pos->rc.rok = 1900;

Použijeme nyní znovu strukturu uzel z odst. 5.1.2:

struct uzel{
	char text[10];
	int op;
	struct uzel *levy;
	struct uzel *pravy;
}
s deklarací
struct uzel u, *pu;

Pak výraz

pu->op
označuje složku op struktury typu uzel, na kterou pu ukazuje,
u.levy
je ukazatelem na levý podstrom a
u.pravy->text[0]
označuje první znak složky text pravého podstromu struktury u.

Složkou struktury, resp. unie může být i bitové pole, jehož počet bitů reprezentace je explicitně určen kladnou konstantou oddělenou dvojtečkou od deklarátoru. Tak např. ve struktuře

struct hs {
	unsigned hlava	:5;
			:3;
	unsigned sektor	:8;
}
jsou objekty členěny na tři bitová pole. hlava má pět bitů, následuje nepojmenované pole 3 bitů a sektor o 8 bitech. Bitová pole se skládají do objektů typu int. Nesmějí být delší než int a nejsou ukládána přes hranici strojového slova. Problém je však v tom, že některé implementace ukládají bitová pole zleva doprava, jiné zprava doleva.

Složky struktury obecně (nejen bitová pole) se postupně ukládají ve smyslu vzrůstajících adres. Přitom se dbá na potřebná zarovnání. Složka tvořená nepojmenovaným bitovým polem nulové délky, tedy :0 je požadavkem o explicitní zarovnání následující složky struktury nebo unie na hranici závislou na implementaci.

Jazyk neomezuje typ bitových polí, ale většina implementací podporuje pouze celočíselné pole, a to ještě typu unsigned.

Unie si můžeme představit jako struktury, jejichž všechny složky mají stejnou (nulovou) adresu od počátku přidělené paměti. Velikost paměti se volí tak, aby stačila pro uchování nejdelší složky. Například s použitím deklarace

union diskadr {
	int ca;
	struct hs, da;
} bulhar;
můžeme obsah objektu bulhar z typu union diskadr chápat buď jakoobjekt
bulhar.ca
typu int, nebo jako objekt
bulhar.da
typu struct hs.

S jednotlivými složkami se pak pracuje takto:

bulhar.da.hlava = 1;
bulhar.da.sektor = 3;

Je rovněž myslitelná varianta

bulhar.ca = 1 << 11 | 3;
ale pouze za předpokladu, že se bitová pole skládají zleva doprava.

5.4.4 Výčtový typ

Deklarací výčtového typu určíme celočíselný podtyp. Stejně jako v deklaraci struktury, resp. unie se využívá mechanismu šablony, např.

enum den {
PO,UT,ST,CT,PA,SO,NE
};
specifikuje šablonu typu enum den a deklarace
enum den dnes, *d;
jsou zřízeny objekty dnes a ukazatel d na objekt typu enum den.

Jsou pak přípustné výrazy

dnes = UT;
d = &dnes;
switch(*d) {
	case PO: ...
	case UT: ...
	...
}
apod. Není-li v šabloně použito přiřazení číselného vyjádření, jsou prvky výčtu očíslovány zleva doprava nulou počínaje, tedy konstanta PO má hodnotu 0, UT hodnotu 1 atd. Po explicitním přiřazení se pokračuje v přirozeném číslování, např.
enum barva { bila, cervena=15, modra } odstin;
odpovídají typu čísel {0, 15, 16}.

5.4.5 Inicializace

Objektu může být v deklaraci přisouzena počáteční hodnota, může být inicializován. Inicializace má formálně tvar přiřazovacího výrazu, ale její interpretace závisí na paměťové třídě objektu. Počáteční hodnoty statických a externích objektů musí být konstantní výrazy, aby je bylo možno vyhodnotit v době kompilace. Těmto objektům je počáteční hodnota totiž přisouzena pouze jednou, a to v okamžiku zavádění programu do paměti.

Automatické a registrované objekty mohou být inicializovány libovolnými výrazy, i ty se totiž vyhodnocují při každém vstupu do bloku, v němž jsou deklarace těchto objektů umístěny.

Statické a externí proměnné bez inicializace mají zaručenou nulovou počáteční hodnotu. Počáteční hodnotu automatické a registrované proměnné bez inicializace nelze určit, protože obsahuje pozůstatky předchozí historie procesu.

Inicializace jednoduchého objektu (proměnné, ukazatele) se provede jedním výrazem:

int i = 0;
double d = 3.141592;
inicializace pole se provede seznamem výrazů ve složených závorkách:
float x[4]  = { 1,2,3 };
double y[] = { 1,2,3,4,5 };
kde první řádek je ekvivalentní zápisům x[0]=1, x[1]=2, x[2]=3 a x[3]=0 a druhý řádek specifikuje, že pole má 5 prvků (podle počtu výrazů) s hodnotami v inicializačním seznamu.

V případě vícerozměného pole se inicializace provádí buď s využitím úplného závorkování

long z[2][3]  = {
	{1,2,3},
	{4,5,6}
};
tj.z[0][0]=1, z[0][1]=2, z[0][2]=3, z[1][0]=4, atd. nebo po řádcích
long z[2][3]  = {1,2,3,4,5,6};
s tím, že první tři hodnoty naplní první řádek, tj. pole z[0], druhé tři hodnoty druhý řádek, tj. pole z[1].

Speciálním případem je inicializace znakového pole

char text[]  = "Zprava pro uzivatele\n";

Inicializace struktur probíhá rovněž po složkách, např.

struct rodcislo dd = {1,1,1950,755};
nebo
struct osoba on = {"Josef", "Novak", {1,1,1950,755}};
(deklarace použitých struktur jsou uvedeny v odst. 5.4.3). Není povoleno inicializovat unie nebo automatické pole a automatické struktury.

5.4.6 Externí definice funkcí a dat

Program v C sestává z posloupností externích definic. Externí definice přisoudí identifikátoru paměťovou třídu extern (implicitně) a specifikovaný typ (implicitně int).

Externí definice má tvar deklarací s tím, že je to jediné místo, kde lze zadat kód (tělo) funkcí. Definice jsou dvojí, a to funkcí a dat.

Definice funkce sestává z deklarátoru funkce se seznamem formálních parametrů, popř. seznamu specifikací formálních parametrů a těla funkce, tj. složeného příkazu, resp. bloku. Blok je složený příkaz s deklaracemi lokálních objektů (odst. 5.5.2). Povolené paměťové třídy funkcí jsou extern a static. Zápis

int min(x,y,z)
int x,y,z;
{
	int m;
	m = (x < y) ? x:y;
	return ((m < z) ? m:z);
}
je příkladem úplné definice funkce min. Na prvním řádku je deklarátor funkce, na druhém specifikace formálních parametrů a následuje blok - tělo funkce.

V seznamu specifikací se smí objevit specifikace právě těch identifikátorů, které jsou v deklarátoru funkce uvedeny jako formální parametry. Pokud chybí specifikace některého z nich, chápe se jako int. Jedinou povolenou paměťovou třídou je zde třída registr (implicitně mají třídu auto). Je-li to možné, je skutečný argument předán v registru.

Externí definice dat se zapisuje jako jejich deklarace. Povolenou paměťovou třídou je extern (implicitně) a static. Například

extern char zprava[35];
static double pi = 3.141592;

5.4.7 Lexikální rozsah platnosti

Program v C nemusí být kompilován vcelku. Posloupnost externích definic může být rozdělena do několika souborů, některé funkce mohou být doplněny z knihoven. Návaznost funkcí v jednom programu je zajištěna buď vzájemným explicitním voláním, nebo předáváním externích dat. Cílem pravidel, která určují rozsah platnosti identifikátorů, je zajistit spolupráci jednotlivých částí programu.

Pravidla jsou dvojího druhu. První druh pravidel se týká lexikálního rozsahu platnosti a jak jednotlivé části programu řadit za sebou, aby nedocházelo k hlášením kompilátoru typu "nedefinovaný identifikátor". Druhý druh pravidel řídí návaznost externích identifikátorů a je mu věnován následující odstavec.

Lexikální rozsah platnosti identifikátoru z externí definice je od místa jeho definice do konce zdrojového souboru s touto definicí. Lexikální rozsah platnosti formálních parametrů funkce končí s koncem těla funkce. Lexikální rozsah platnosti identifikátorů, deklarovaných v bloku, končí s koncem tohoto bloku. Lexikální rozsah platnosti návěští je tělo funkce, v němž je uvedeno.

Poznamenejme ještě, že pro použití stejných identifikátorů ve vnořených blocích má jazyk C obvyklé řešení ve formě "zneviditelnění" vnějšího významu. V úryvku programu

int z;
f(z)
double z;
{
	z = 2.78;
}
není uvnitř těla funkce f celočíselné z vidět.

Posledním pravidlem lexikální návaznosti je, že identifikátory proměnných, šablon a složek struktur nebo unií patří do tří různých kategorií, takže nemůže dojít ke konfliktu. Lze pak psát např.

struct proc {
char status;
	...
} proc[NPROC];
tj. deklarovat šablonu typu struct proc a pole proc objektů téhož typu.

5.4.8 Návaznost externích objektů

Cílem pravidel pro externí identifikátory je, aby odkazy na týž externí identifikátor vedly k témuž objektu. Jestliže funkce odkazuje na identifikátor deklarovaný jako extern, pak v jiné části programu (zdrojovém souboru, knihovní funkci) musí existovat alespoň jedna externí definice tohoto identifikátoru. Přirozeným požadavkem je, aby typ a velikost odkazovaného objektu byly ve všech definicích stejné. Dále je povolena nejvýše jedna explicitní inicializace externího objektu.

Je však povolena více než jedna definice datového externího identifikátoru (pro funkce to nemá smysl). V Unixu se násobné definice externích dat vyřeší na úrovni sestavování. Externí datové objekty pak připomínají spíše fortranskou pojmenovanou COMMON oblast.

Podle návrhu normy jazyka C má klíčové slovo extern dodatečný význam. Je-li použito explicitně (a bez inicializace) v externí definici identifikátoru, rozšiřuje lexikální rozsah tohoto identifikátoru do uvažovaného zdrojového souboru, paměť je objektu přidělena jinde. Dále se vyžaduje, aby v celém programu existovala právě jedna definice tohoto identifikátoru (bez klíčového slova extern).

Tak v implementaci pro Unix je přípustná situace:

soubor a.c                   soubor b.c

extern int errno;            extern int errno;
extern char errmsg[30];      extern char errmsg[30];
extern void f();
main()                       void f(x)
{ ...                        int x;
errno = 0;                   {
f(15);                       if(x > 10){
if(errno != 0)               errno = 3;
printf("%s", errmsg);        errmsg = "ARG. > 10\n";
...                          }
}                            ...
                             }
tj. v obou zdrojových souborech jsou externí definice proměnných errno a errmsg. Paměť je jim však přidělena v etapě sestavování pouze jednou.

V situaci, kdy klíčové slovo extern má dodatečný význam, musí programátor postupovat např. takto:

soubor a.c                   soubor b.c

int errno;                   extern int errno;
char errmsg[30];             extern char errmsg[];
extern void f();             void f(x);
main()                       int x;
{                            { 
...                          ...
}                            }

V obou případech budou objekty programu umístěny v paměti podle obr. 5.2.

[OBR. 5.2]

Statické objekty, tj. objekty explicitně deklarované klíčovým slovem static, mají dvojí povahu. Jsou-li uvedeny mezi externími definicemi, mají lexikální platnost externích identifikátorů, nejsou však vidět z jiných zdrojových souborů. Statické objekty deklarované v těle funkce mají lexikální rozsah platnosti jako automatické proměnné, ale na rozdíl od nich trvají po celou dobu provádění programu. Představují tedy možnost stálé a přitom lokální paměti pro funkci. Například v části programu umístěném v jednom zdrojovém souboru:

static int x;
int f()
{
	static int z;
...
}

static int y;
int q()
{
...
}
int h()
{
...
}
představuje proměnná x společnou privátní paměť pro funkce f, g, h, proměnná y společnou privátní paměť funkcí g, h; konečně proměnná z je privátní pamětí funkce f.