Jan Bielecki Język C

Przedmowa


Język programowania C został zaprojektowany w 1972 roku przez Dennisa Ritchie, pracownika Bell Laboratories. Język ten wywodzi się od Algolu 60 (1960), CPL (1963), BCPL (1963) i B (1970). Ponieważ w języku tym został opracowany system operacyjny UNIX i jego bogate oprogramowanie użytkowe, język C jest powszechnie uznawany za język do programowania systemowego, mimo iż w istocie jest to język ogólnego przeznaczenia.

Na popularność języka C, zwłaszcza jako języka programowania mikrokomputerów, składa się kilka czynników: znaczna liczba operatorów języka, nowoczesne struktury danych i instrukcje strukturalne, użyteczne operacje na danych różnych typów, zwłaszcza na danych wskazujących, oraz bogata i w znacznym stopniu przenośna biblioteka programów standardowych, w tym programów do realizowania operacji wejścia/wyjścia. Główną jednak zaletą języka C jest efektywność i relatywnie mały rozmiar napisanych w nim programów. Z tego względu język C coraz częściej przejmuje rolę nowoczesnego języka typu asemblerowego, a większość programów realizowanych na komputerach osobistych jest opracowywana w tym właśnie języku.

Mimo iż liczba różnych implementacji języka C znacznie przekracza 20, wykazują one duże podobieństwo. W głównej mierze należy to zawdzięczać istnieniu nieformalnego standardu, jakim jest książka pt. “Język programowania C”, której autorami są Brian Kernighan i Dennis Ritchie. Została ona wydana w 1978 roku i do chwili obecnej wiele opisów implementacji ogranicza się do wyszczególnienia różnic między konkretną realizacją, a tym nieformalnym wzorcem.

Rosnące zainteresowanie językiem C oraz szczególna rola, jaką odgrywa on w oprogramowaniu mikrokomputerów, spowodowały, że w 1982 roku Amerykański Instytut do Spraw Standardów — ANSI — powołał podkomisję standaryzacyjną języka. Zadaniem tej podkomisji jest opracowanie standardu języka C, bibliotek oraz środowiska operacyjnego. Dotychczasowe rezultaty prac wykazują, że język C w swej nowej postaci nie będzie znacznie odbiegał od jego implementacji dokonanych przez wiodące firmy produkujące oprogramowanie.

Niniejsza książka składa się z trzech części: z referencyjnego opisu standardu języka, jakim do zatwierdzenia standardu ANSI jest opracowanie Kernighana i Ritchiego, opisu zasad programowania operacji wejścia/wyjścia w języku C oraz krótkiego omówienia przykładowej implementacji języka, dokonanej przez firmy Lattice i Microsoft. W dodatkach umieszczono m.in. obszerny zestaw programów realizujących operacje na plikach w systemie CP/M oraz reprezentatywny zestaw funkcji do przetwarzania znaków.

Ponieważ książka jest kierowana do programistów, którzy już opanowali sztukę programowania w innych językach i jedynie pragną zapoznać się z nowym, zrezygnowano z przytaczania długich programów, koncentrując się w głównej mierze na szczegółowym omówieniu konstrukcji programowych oraz nadaniu interpretacji sformułowaniom formalnego opisu języka. Taki sposób zaprezentowania tematu czyni niniejsze opracowanie książką towarzyszącą innemu mojemu opracowaniu pt. “Wprowadzenie do języka C" lub wzorcowemu opracowaniu Kernighana i Ritchiego.

Ze względu na podobieństwo języka C do Pascal i PL/I użyta tu terminologia nie powinna sprawić czytelnikom specjalnych trudności, a określenia takie jak unie, wyłuskiwanie i referencja nie powinny budzić większego sprzeciwu, jako że były już używane w innych opisach. Świadomie natomiast zdecydowano się użyć określeń “zmienna wskazująca" i “wskazanie", jako że tylko one właściwie oddają semantykę angielskiego terminu pointer. Ci, którzy znają język angielski wiedzą, że słowo wskaźnik wywodzi się raczej od takich słów jak flag i indicator.

Uważny czytelnik dostrzeże także pewne różnice w sposobie opisywania operacji. Ponieważ język C jest językiem wyrażeń, a pewne konstrukcje spotykane w innych językach programowania, takie jak np. instrukcja przypisania, właściwie w nim nie istnieją, uznano za celowe przyjęcie innego modelu opisywania operacji. Zgodnie z tym modelem deklaracje i wyrażenia podlegają opracowaniu (ang. elaboration), a argumentami i rezultatami operacji są dane. Dane te mogą być przypisywane zmiennym programu. Dzięki takiemu ujęciu możliwe jest pełne oddzielenie opisów czynności stanowiących program od rezultatów tych czynności, tj. skutków opracowywania opisów.

Podzielając pogląd, że największą trudność w studiowaniu opracowań referencyjnych sprawiają odwołania do pojęć jeszcze nie zdefiniowanych, rozszerzono pracę o słownik terminów, w którym zawarto krótkie definicje podstawowych pojęć oraz przykłady odwoływania się do nich.

Ponieważ książka jest miejscami trudna, zawarto w niej wiele przykładów wyjaśniających, opatrując je licznymi komentarzami. Należy żywić nadzieję, że odwoływanie się do słownika i staranne studiowanie przykładów ułatwi lekturę tekstu, przyczyniając się do pogłębienia wiadomości o języku i zakresie przenośności opracowanych w nim programów.


l. Wstęp


Język C należy do tych języków programowania, których głównym zastosowaniem jest programowanie systemowe. Został on opracowany z przeznaczeniem dla kom­puterów rodziny PDP-11, jednak dzięki swoim zaletom znacznie się rozpowszechnił i obecnie jest dostępny w wielu mikrokomputerach osobistych, zwłaszcza 16-bitowych, a także w komputerach IBM/370, VAX-11 i innych.

Przedstawiony tu opis języka C stanowi interpretację nieformalnego standardu ję­zyka, którym jest Dodatek A do książki pt. “The programming language C". Auto­rami tej książki wydanej w 1978 roku przez Prentice Hall, a następnie przetłumaczo­nej na język polski przez WNT, są Brian W. Kernighan i Dennis M. Ritchie. W przeważającej większości opisów implementacji tego języka są podawane odwo­łania do wspomnianego dodatku, przy czym podaje się inicjały nazwisk autorów, po których następuje numer rozdziału, np. KR/2.3. Z tego to względu w przytoczo­nych dalej opisach posłużono się zastosowaną przez Kernighana i Ritchiego numeracją punktów i utrzymano zbliżony sposób prezentacji języka. Ponieważ najlepszą ilustracją opisów formalnych są odpowiednio dobrane przykłady, trud­niejsze partie opisu uzupełniono szczegółowo skomentowanymi programami lub fragmentami programów.


2. Jednostki leksykalne


Jednostkami leksykalnymi języka C są identyfikatory, słowa kluczowe, literały i se­paratory, spacje, znaki tabulacji, znaki przejścia do nowego wiersza i komentarze nie stanowią jednostek leksykalnych. Każdy ciąg takich znaków i komentarzy jest traktowany tak jak pojedyncza spacja i będzie nazywany krótko odstępem.

Zgodnie z przytoczoną klasyfikacją każdy program zapisany w języku C składa się z jednostek leksykalnych i odstępów. Wyodrębnienie tych obiektów odbywa się podczas analizowania programu, dokonywanego w naturalnym porządku, tj. od lewej do prawej i od góry w dół. Za jednostkę leksykalną uznawany jest w takim przypadku najdłuższy ciąg znaków nie zawierający odstępów, który może uchodzić za jednostkę leksykalną. Ponieważ jednostki leksykalne nie mogą zawierać odstępów, te ostatnie mogą być wykorzystywane do separowania słów kluczowych i identyfika­torów oraz narzucania podziału tekstu programu na odstępy i jednostki leksykalne.

Przykład. Wiersz o postaci

if(var+ + +len)fun(arg);

składa się z odstępu, ciągu jednostek leksykalnych i odstępu. Ponieważ + jest jednostką leksykalną, napis + + + składa się z jednostki leksykalnej + + oraz jed­nostki leksykalnej +. Przytoczony wiersz programu jest równoważny wierszowi

if(var++ +len)fun(arg);

ale jest różny od

if(var+ ++Ien)fun(arg);

gdyż w pierwszym przypadku operator + + dotyczy zmiennej var, a w drugim zmien­nej len.


2.1. Komentarze

Komentarz rozpoczyna para następujących bezpośrednio po sobie znaków /*, a kończy najbliższa para znaków */. Jeśli w obrębie komentarza po parze znaków /* występuje ponownie para znaków /*, to nie stanowi ona początku komentarza.

Przykład. W deklaracji o postaci

int/*/ integer * /*/*var;

napis

/*/ integer * /*/

jest komentarzem, a przytoczona deklaracja jest równoważna deklaracji

int *var;

2.2. Identyfikatory

Identyfikatorem w języku C jest ciąg literowo-cyfrowy rozpoczynający się od litery, taki który nie jest słowem kluczowym. Literami języka są duże i małe litery alfabe­tu angielskiego oraz znak podkreślenia. Litery małe i duże są uznawane za różne, a pary identyfikatorów nie różniące się do ósmego znaku włącznie są uznawane za identyczne. Zezwala się, aby rozróżnienie identyfikatorów z atrybutem extern do­konywało się na podstawie silniejszych ograniczeń, tj. np. liczby znaków mniejszej od ośmiu, wykluczenia pewnych znaków lub utożsamiania liter dużych i małych.

Przykład. Jeśli w pewnej sekcji programu występuje definicja funkcji

static

StringLength(s)

char *s;

{ register char *c = s;

while(*c+ +);

return c - s - 1;

}

to w obrębie tej sekcji można posługiwać się np. identyfikatorem StringLen jako synonimem identyfikatora StringLength. Gdyby w omawianej definicji pominięto słowo static, to zakresem nazwy StringLength stałby się cały program. W takim przypadku zbiór synonimów nazwy funkcji byłby ustalony przez implementację, a mogłyby do niego należeć takie np. nazwy jak String, STRING i string.

2.3. Słowa kluczowe

Słowem kluczowym języka. C jest spójny ciąg małych liter języka angielskiego tworzą­cy jedno z wymienionych niżej słów:

auto else register union

break extern return unsigned

case float short while

char for sizeof

continue goto static entry

default if struct fortran

do int switch asm

double long typedef



Słowom kluczowym entry, fortran i asm nie nadano w standardzie interpretacji.

Przykład. W definicji funkcji

main(){

char Char[] = "char";

printf("%s",Char);

}

występuje słowo kluczowe. Słowem tym jest char i występuje ono jednokrotnie.

2.4. Literały arytmetyczne i znakowe

Literały arytmetyczne dzielą się na stałopozycyjne i zmiennopozycyjne. Literały stałopozycyjne są typu (int), (short int), (long int) i (unsigned int). Literały zmienno­pozycyjne są typu (float), (long float) i (double). Literały znakowe są typu (char). Literały stałopozycyjne i znakowe będą nazywane literałami całkowitymi.

2.4.1. Literały stałopozycyjne

Podstawowy literał stalopozycyjny składa się ze spójnego ciągu cyfr dziesiętnych. Jeśli pierwszą cyfrą takiego ciągu jest 0, a następne są cyframi ósemkowymi, to przyjmuje się, że literał stanowi liczbę ósemkową. Jeśli pierwszą cyfrą literału jest 0, a bezpośrednio po niej następuje litera x (mała albo duża), to przyjmuje się, że literał jest liczbą szesnastkową, której cyfry większe od 9 mogą być wy­rażone za pomocą liter z przedziału a-f lub A-F.

Jeśli wartość literału stałopozycyjnego dziesiętnego przekracza największą dopu­szczalną wartość danej typu (int), to przyjmuje się, że literał jest typu (long int). Jeśli wartość literału stałopozycyjnego ósemkowego albo szesnastkowego przekra­cza największą wartość danej typu (unsigned int), to przyjmuje się, że literał jest także typu (long int).

Przykład. Jeżeli w pewnej implementacji dane typu (int) są 16-bitowe, to

32767 jest typu (int)

32768 jest typu (long int)

0x10000 jest typu (long int)


2.4.2. Jawne literały typu (long int)

Jeśli bezpośrednio po ciągu znaków stanowiącym zapis literału stałopozycyjnego następuje litera L (duża albo mała), to uznaje się, że literał jest typu (long int).

Przykład. Jeśli parametr funkcji fun jest typu (long int), to aby skojarzyć go z ar­gumentem o wartości 0, można posłużyć się wywołaniem fun(0L), ale nie można posłużyć się wywołaniem fun(0).

2.4.3. Literały znakowe

Podstawowy literał znakowy składa się z jednego znaku graficznego - różnego od kreski ukośnej pochylonej w lewo - zawartego między parą sąsiadujących z nim apostrofów. Wartością takiego literału jest wartość liczbowa reprezentowanego przezeń znaku graficznego.

Innym sposobem przedstawiania znaku za pomocą literału jest umieszczenie między parą apostrofów napisu ‘\d’ w którym \ jest wspomnianą wyżej kreską ukośną, natomiast d jest ciągiem od l do 3 cyfr ósemkowych określających wartość liczbową znaku albo jest kreską ukośną, apostrofem albo literą reprezentującą jeden z 5 zna­ków sterujących, tak jak wynika to z poniższego zestawienia

Znak Oznaczenie Literał

nowy wiersz NL (LF) '\n’

tabulacja HT '\t’

cofnięcie BS '\b'

powrót karetki CR *\r'

nowa strona FF '\f

kreska ukośna w lewo '\\'

apostrof '\"

Jeśli zapis literału ma postać ‘\0', to literał taki reprezentuje znak nul. Jeśli po kresce ukośnej nie następuje jeden z wymienionych znaków, to kreska ukośna jest pomijana.

Przykład. Jeśli alfabet procesora składa się ze znaków kodu ASCII, w którym znak apostrof ma wartość liczbową taką jak literał 0x27, to literał znakowy reprezentujący apostrof można przedstawić w postaci '\’’.albo w równoważnej mu postaci '\47'.

2.4.4. Literały zmiennopozycyjne

Literał zmiennopozycyjny składa się z części całkowitej, kropki, części ułamkowej, małej albo dużej litery e oraz z wykładnika. Części całkowita i ułamkowa składają się z ciągów cyfr, a wykładnik składa się z ciągu cyfr, który może być poprzedzony znakiem + albo -.

Zezwala się na pominięcie części całkowitej albo części ułamkowej, jak również na pominięcie litery e wraz z wykładnikiem albo kropki.

Ze względu na nieodróżnianie literałów pojedynczej i podwojonej dokładności, każdy literał zmiennopozycyjny jest uznawany za literał typu (double) równoważ­nego typowi (long float).

Przykład. Każdy z literałów

234.5 .2345e3 2345.E-1 23.45e+1 jest typu (double) i ma taką samą wartość jak pozostałe.

2.5. Literały tekstowe

Podstawowy literał tekstowy składa się z ciągu znaków graficznych zawartego mię­dzy parą cudzysłowów. W ciągu tym znak cudzysłowu musi być zapisany za pomocą pary znaków \', analogicznie jak miało to miejsce dla literałów znakowych.

Ponadto między wspomnianą parą cudzysłowów może wystąpić przedstawienie zna­ku za pomocą napisu \d, tak jak to omówiono uprzednio. W każdym z tych przypad­ków, jeśli bezpośrednio po kresce ukośnej występuje koniec wiersza, to kreska ta wraz z ewentualnym znakiem końca wiersza także jest pomijana.

Przykład. Jeśli bezpośrednio po kresce ukośnej występuje znak końca wiersza (np. cr), to wykonanie następującego programu spowoduje wyprowadzenie napi­su jan, ,ewa.

main(){

printf("%s%s" , "jan,\

,”

,"ewa");

}

Ponieważ na końcu każdego ciągu znaków reprezentowanego przez literał tekstowy jest umieszczany znak \0, każdy taki literał reprezentuje co najmniej jeden znak. Należy także dodać, że literał znakowy reprezentuje daną klasy static, a każde dwa wystąpienia literału tekstowego reprezentują różne dane.

Przykład. W definicji funkcji

fun()

{ putchar((*"j")++);

putchar( *"j" );

}

dwukrotnie występuje literał tekstowy "j". Ponieważ każde z tych wystąpień repre­zentuje inną daną, operacje na danej reprezentowanej przez pierwszy literał nie mają

wpływu na wartość danej reprezentowanej przez drugi. Z tego względu podczas pierwszego wywołania funkcji fun nastąpi wyprowadzenie napisu jj, a podczas drugiego - nastąpi wyprowadzenie napisu kj.

2.6. Uwagi implementacyjne

Mimo iż istnienie kilku typów danych stałopozycyjnych oraz dwóch typów danych zmiennopozycyjnych może sugerować bogactwo typów, należy zdać sobie sprawę, że są zgodne ze standardem te implementacje, w których np. typy (short int) i (long int) są identyczne z typem (int), jak również te implementacje, w których typ (float) jest tożsamy z typem (double).


3. Sposób opisu składni

W tych miejscach opisu składni, w których wyjaśnienia słowne byłyby mało czytel­ne, zostaną podane definicje formalne. W definicjach tych bezpośrednio po nazwie definiowanej jednostki składniowej wystąpi dwukropek, a alternatywne postaci tej jednostki zostaną podane w kolejnych wierszach. Te fragmenty definicji, które mogą być opuszczone, zostaną oznaczone uniesioną literą o. Ze względu na częste wystę­powanie pojęcia lista, zdefiniowanego np. jako

lista-obiektów:

obiekt

obiekt, lista-obiektów

wystarczy ograniczyć się do definiowania obiektów, pojęcie listy obiektów uznając za oczywiste i zgodne z przytoczoną tu definicją ogólną.


4. Klasy i typy danych


Identyfikatory występujące w programie reprezentują dane. Z każdym identyfika­torem można związać atrybuty, które określą klasę i typ danych reprezentowanych przez ten identyfikator.

Pojęcie klasy obiektu reprezentowanego przez wybrany identyfikator wiąże się ze sposobem przydzielania pamięci dla obiektu oraz zwalniania jej, natomiast poję­cie typu wiąże się ze sposobem interpretowania zawartości pamięci przydzielonej obiektowi.

W języku C zdefiniowano 4 klasy pamięci: pamięć automatyczną (auto), statyczną (static), zewnętrzną (extern) i rejestrową (register). Stosownie do tego podziału bę­dziemy mówić o zmiennych automatycznych, statycznych, zewnętrznych i reje­strowych.

Zmienne automatyczne są tworzone podczas wykonywania prologu tego bloku, w którym zostały zadeklarowane, i są usuwane podczas wykonywania jego epilogu.

Zmienne statyczne są tworzone podczas wykonywania prologu programu i są usu­wane podczas wykonywania jego epilogu.

Jeśli podczas deklarowania zmiennej każdej z tych klas polecono nadać jej wartość początkową, to nastąpi to podczas tworzenia zmiennej. Jeśli polecenie takie nie wy­stąpiło, to zmienne statyczne otrzymają wartości początkowe 0, a zmienne automa­tyczne otrzymają wartości początkowe nieokreślone. Oczywiście w odróżnieniu od zmiennych statycznych, które otrzymują wartości początkowe tylko jednokrotnie, zmienne automatyczne mogą otrzymywać je wielokrotnie. Ponieważ jednak w ko­lejnych wcieleniach zmiennej automatycznej ten sam identyfikator dotyczy różnych zmiennych, korzystanie z wyników przetwarzania pewnego bloku może w następ­nych jego wywołaniach odbywać się jedynie za pomocą zmiennych statycznych.


Przykład. Wykonanie następującego programu powoduje wyprowadzenie liczb 5 i 6 main(){

fun();fun();

}

fun(){

{ static int var = 5;

printf("%d",var++);

}

Gdyby w przytoczonym programie zastąpiono atrybut static atrybutem auto, to wykonanie programu spowodowałoby wyprowadzenie liczb 5 i 5.

W odróżnieniu od zmiennych statycznych, których zakres dostępności nie wykracza poza blok lub sekcję, w którym zostały zdefiniowane, zakres dostępności identyfi­katorów zmiennych zewnętrznych obejmuje wszystkie bloki i sekcje, w których wystąpiły ich deklaracje.

Dwie pozostałe klasy zmiennych, a mianowicie zmienne rejestrowe i zewnętrzne wykazują znaczne podobieństwo do omówionych już zmiennych automatycznych i statycznych.

Zmienne rejestrowe różnią się od zmiennych automatycznych tylko tym, iż pamięć dla nich jest przydzielana w szybkich rejestrach procesora, jeżeli oczywiście jest to możliwe. Ze względu na tę właściwość odmienność zmiennych rejestrowych i zmien­nych automatycznych sprowadza się do tego, że ich nazwy nie mogą być argumentami operatora referencji.

Zmienne zewnętrzne są podobnie jak zmienne statyczne tworzone podczas wykony­wania prologu programu, a usuwane podczas wykonywania jego epilogu. W odróż­nieniu od zmiennych statycznych zmienne te mogą być deklarowane wielokrotnie, w różnych blokach i sekcjach programu, a ogół tych deklaracji, w którym dokładnie jedna musi być deklaracją definiującą (bez atrybutu extern) dotyczy jednej zmiennej o wybranej nazwie. Dzięki temu zmienne zewnętrzne mogą być wykorzystane do komunikowania się rozłącznych bloków i sekcji programu.

Przykład. Wykonanie następującego programu powoduje wyprowadzenie napi­su ewa. Wśród trzech deklaracji zmiennej zewnętrznej var pierwsza jest deklaracją definiującą, a dwie pozostałe są deklaracjami odwoławczymi

char var = 'e';

main(){

register char var = 'w';

{ extern char var;

putchar(var);

var - = 4;

putchar(var);

}

{ extern char var;

putchar(var);

}

}

Należy nadmienić, że usunięcie dowolnego ze słów kluczowych extern uczyniłoby program niepoprawnym, ponieważ przekształciłoby deklarację zmiennej wewnętrznej w deklarację zmiennej automatycznej, a zmienna taka podczas odwoływania się do funkcji putchar miałaby wartość nieokreśloną.

Jak już wspomniano, w języku C wprowadzono kilka podstawowych typów obiek­tów. Obiekty zadeklarowane jako znakowe (char) są tak dobrane, że umożliwiają przypisanie im dowolnego znaku alfabetu procesora. Jeśli przypisanie takie istotnie nastąpi, to zmienna, której przypisano znak otrzymuje wartość liczbową równą wartości kodu tego znaku. Skutki przypisania zmiennym znakowym innych danych są zależne od implementacji.

Przykład. Niezależnie od tego czy alfabetem procesora jest kod ASCII czy EBCDIC, opracowanie deklaracji

char chr = 'J';

powoduje przypisanie zmiennej chr znaku J. Natomiast skutki opracowania de­klaracji

char chr = Ox4A;

są zależne od implementacji (w kodzie ASCII są równoważne przypisaniu chr znaku J, a w kodzie EBCDIC znaku [ ).

Obiekty stalopozycyjne występują w języku C w trzech odmianach i są typu (short int), (int) albo (long int). Zazwyczaj dana typu (short int) zajmuje mniej pamięci niż dana typu (long int). Dopuszcza się jednak, aby w konkretnej imple­mentacji dane typu (short int) bądź typu (long int), a nawet dane obu tych typów zajmowały tyle samo miejsca, co dane typu (int).

Obiekty typu (unsigned int) są traktowane tak, jak dane bez znaku, i podlegają zasadom arytmetyki modulo 2n, gdzie n jest liczbą bitów użytą do reprezentowania danej.

Zezwala się także, aby obiekty typu (float) oraz typu (double) i (long float) były traktowane identycznie.

Zgodnie z wcześniejszymi ustaleniami będziemy przyjmować następującą klasy­fikację :

Należy nadmienić, że oprócz wymienionych tu podstawowych typów arytmetycz­nych istnieje w języku C nieskończenie wiele typów pochodnych stanowiących

W ogólnym przypadku rozszerzanie zbioru typów danych przez posłużenie się ta­blicami, funkcjami, itp. może być zastosowane rekurencyjnie.

Przykład. Przytoczona definicja typu (struct celi) zawiera w sobie powołania na typy podstawowe oraz pochodne

struct cell{

struct cell *east;

struct celi *west;

Int array[3];

union {

int ival;

char *(*(fun)()

} *ptr;

};

fun jest zmienną wskazującą na funkcje która zwraca wskazanie na znak.


5. Obiekty i l-wyrażenia

Pod pojęciem obiektu rozumiany jest przedmiot przetwarzania, a więc dana zajmująca określony obszar pamięci operacyjnej. Do tej danej można odwoływać się za pomocą l-wyrażenia, tj. napisu identyfikującego położenie danej w pamięci.

Pojęcie l-wyrażenia ma istotne znaczenie w języku C, ponieważ tylko l-wyrażeni, mogą występować z lewej strony operatora przypisania (stąd “l-" jako skrót od (“lewa") oraz mogą być argumentami pewnych innych operatorów, a mianowicie operatora selekcji (.), inkrementacji (+ +), dekrementacji (- -) i referencji (&).

Najprostszym przykładem l-wyrażenia jest identyfikator zmiennej. Jeśli chr jest zmienną znakową zadeklarowaną jako

char chr == 'j';

to zarówno w kontekście chr = 'b';

jak i w kontekście

putchar(chr);

napis chr identyfikuje ten obszar pamięci operacyjnej, w którym podczas tworzeni;

zmiennej umieszczono kod znaku j.

Bardziej złożony przypadek l-wyrażenia występuje w programie

char arr[3] == "jB", *ref = arr;

main(){

*++ref='b';

printf("%s",arr);

}

W programie tym, którego wykonanie powoduje wyprowadzenie napisu j b, wystę­puje m.in. instrukcja

*++ref = 'b';

w której *++ref jest w l-wyrażeniem reprezentującym drugi znak wektora arr. Opracowanie tego wyrażenia, równoważnego w rozpatrywanym kontekście l-wyra-żeniu arr[1], ma skutek uboczny w postaci zmiany wartości zmiennej ref. Gdyby rozpatrywaną instrukcję przedstawiono w równoważnej postaci

++ref,*ref= 'b';

to występujący w niej napis *ref byłby l-wyrażeniem, którego opracowanie nie ma skutku ubocznego.

Przykład. Wykonanie następującego programu powoduje wyprowadzenie liczby 5

struct{

int var;

} str[2],

*ptr = str,

agr ={5};

main(){

(ptr++)->var = (&agr)->var;

printf("%d",(- -ptr)->var);

}

Wykonanie instrukcji przypisania ma taki sam skutek jak wykonanie instrukcji

ptr->var = agr.yar, ptr+ + ;




6. Konwersje


Wykonanie pewnych operacji wymaga dokonania wstępnej konwersji ich argumen­tów. Konwersje te mogą być zadawane jawnie, za pomocą operatora konwersji albo mogą wynikać z przyjętych domniemań.


6.1. Dane znakowe i stałopozycyjne

W każdym miejscu programu, w którym może być użyte odwołanie do danej ty­pu (int), może wystąpić odwołanie do danej typu (char) albo (short int). Wynika to stąd, że podczas wykonywania programu dane typu (char) i (short int) są podda­wane niejawnej konwersji na dane typu (int).

Konwersja danej typu (short int) na daną typu (int) nie powoduje zmiany wartości danej, ponieważ konwersja ta polega na powieleniu bitu znaku. Konwersja danej typu (char) na daną typu (int) może natomiast powodować zmianę wartości danej, ponieważ decyzja o powielaniu lub nie powielaniu bitu znaku zależy od implemen­tacji. Zagwarantowano jedynie, że dana typu (int) powstała z danej typu (char) reprezentującej znak alfabetu procesora będzie miała wartość dodatnią.

Konwersje danych stałopozycyjnych na dane typu znakowego oraz konwersje da­nych stałopozycyjnych na dane stałopozycyjne, których reprezentacja wymaga mniejszej liczby bitów, są znacznie prostsze i polegają na odrzuceniu bardziej zna­czących bitów reprezentacji danej.

Przykład. Jeśli dane typu (char) są reprezentowane za pomocą 8 bitów, a dane typu (int) za pomocą 16 bitów, to po opracowaniu deklaracji

char chr = -1;

char fix = chr;

zmienna chr ma wartość 255, natomiast wartość zmiennej fix zależy od implementacji. Może nią być 255 albo -1.


6.2. Dane zmiennopozycyjne

Ponieważ operacje zmiennopozycyjne są w języku C wykonywane na danych ty­pu (double), dane typu (float) są poddawane niejawnej konwersji na dane typu (double). Wyjątek stanowi operacja przypisania, w której z lewej strony operatora przypisania występuje l-wyrażenie reprezentujące zmienną typu (float), a z prawej strony występuje wyrażenie typu (double). W takim przypadku dana typu (double) zostaje poddana konwersji na daną typu (float).

Konwersja z typu (float) na (double) polega na wydłużeniu mantysy o dodatkowe bity zero, a konwersja odwrotna polega na odrzuceniu nadmiarowych bitów man­tysy, po uprzednim zastosowaniu zaokrąglenia.

Przykład. Podczas opracowywania deklaracji

auto float _float = 3e2;

auto float _real = _float + _float;

następuje konwersja danej reprezentowanej przez literał 3e2 z typu (double) na typ (float), konwersja danych typu (float) reprezentowanych przez _float na dane typu (double) oraz konwersja danej typu (double) stanowiącej ich sumę na daną typu (float) przypisywaną zmiennej _real.


6.3. Dane zmiennopozycyjne i całkowite

Konwersje danych zmiennopozycyjnych na dane całkowite są uzależnione od imple­mentacji. Dotyczy to zwłaszcza sposobu zaokrąglania wartości danych podczas od­rzucania części ułamkowych ujemnych danych zmiennopozycyjnych. Przyjmuje się jednak, że niepoprawne są konwersje, których rezultatami są dane o wartościach wykraczających poza zakres wartości danych całkowitych.

Konwersje danych całkowitych na dane zmiennopozycyjne przebiegają zgodnie z oczekiwaniami. W przypadkach, gdy mantysy danych zmiennopozycyjnych są reprezentowane za pomocą zbyt małej liczby bitów, należy liczyć się z pewną utratą dokładności, a co za tym idzie - ze zmianą wartości danej.

Przykład. Jeśli w pewnej implementacji zarówno zmienne typu (float), jak i zmienne typu (long int) są reprezentowane za pomocą 32 bitów, a mantysa zmiennych ty­pu (float) liczy 24 bity, to w następstwie konwersji z typu (long int) na typ (float) dwóch różnych danych o wartościach 33554431L (225-1) oraz 33554430L (225-2) powstaną dwie dane typu (float) o identycznych wartościach.


6.4. Dane wskazujące i stałopozycyjne

Operacje dotyczące danych wskazujących i stałopozycyjnych mogą dotyczyć jedynie dodania albo odjęcia danej stałopozycyjnej typu (int) albo (iong int) odpowiednio do albo od danej wskazującej.

W każdym z tych przypadków następuje konwersja danej stałopozycyjnej na prze­sunięcie względem wartości danej wskazującej. Przesunięcie to jest wyznaczane jako iloczyn wartości danej stałopozycyjnej i rozmiaru obiektu wskazywanego przez daną wskazującą.

Zezwala się również na odjęcie dwóch danych wskazujących obiekty tego samego typu. W tym przypadku wartości danych wskazujących zostaną poddane konwersji na adresy obiektów wskazywanych przez te dane, a następnie ich różnica zostanie podzielona przez rozmiar obiektów wskazywanych przez poddawane konwersji dane wskazujące.

Przykład. Wykonanie następującego programu powoduje wyprowadzenie liczby 2 char arr[3][2],

(*ref)[2],

var;

main()

{ ref = arr;

ref += 2;

var = ref - arr;

printf("%d",var);

}

Po wykonaniu pierwszego przypisania wartością zmiennej ref jest wskazanie na arr[0], a po wykonaniu drugiego - wskazanie na arr[2]. Te dwa wskazania różnią się o 2, ale rozpatrywane jako adresy w pamięci operacyjnej różnią się o 4.


6.5. Dane całkowite bez znaku

Operacje dotyczące danych całkowitych bez znaku oraz dowolnych danych całkowi­tych są wykonywane w ten sposób, że przed wykonaniem właściwej operacji dane ze znakiem są poddawane konwersji na dane bez znaku. Konwersja ta prowadzi do utwo­rzenia danej o takiej najmniejszej wartości całkowitej nieujemnej, która modulo 2n (n jest liczbą bitów wykorzystanych do reprezentowania danej) jest równa wartości danej ze znakiem. W zapisie uzupełnieniowym do 2 wspomniana konwersja ma cha­rakter czysto formalny i nie powoduje zmiany reprezentacji danych.

Pozostaje nadmienić, że o ile konwersja danej typu (int) na daną typu (unsigned int) może spowodować zmianę wartości danej, o tyle konwersja danej typu (unsigned int) na daną typu (Iong int) nie powoduje takiej zmiany.

Przykład. Jeśli var jest zmienną zadeklarowaną jako

unsigned var = -1;

to jej wartością początkową jest 65535 (216-1).


6.6. Konwersje arytmetyczne

Ponieważ użycie większości operatorów wiąże się z wykonaniem konwersji, których wielokrotne omawianie mijałoby się z celem, wprowadzimy pojęcie typowych kon­wersji arytmetycznych. Konwersje te są stosowane w następującej kolejności:


Jeśli po tych konwersjach

W pozostałych przypadkach oba argumenty muszą być typu (int) i rezultat operacji jest typu (int).

Przykład. Jeśli deklaracje zmiennych Int, Short, Char i Float mają postać

int Int;

short int Short;

char Char;

float Float;

to instrukcja

Int = Short + Char + Float;

jest wykonywana tak jak instrukcja

Int = (int)( (double)( (int)Short + (int)Char) + (double) Float);


7. Wyrażenia

Niniejszy rozdział poświęcono zasadom opracowywania wyrażeń. W kolejnych pod­rozdziałach opisano grupy operatorów języka o tym samym priorytecie, rozpoczy­nając od priorytetu najwyższego, a kończąc na najniższym.

Tablica 7.1. Priorytety i wiązania operatorów

Priorytet Wiązanie Operator

15 lewe () [] ->.

14 prawe ! ~ ++ - - - (typ) * & sizeof

13 lewe * / %

12 lewe + -

11 lewe << >>

10 lewe < <= > >=

9 lewe == !=

8 lewe &

7 lewe ^

6 lewe |

5 lewe &&

4 lewe ||

3 prawe ?:

2 prawe = += -= *= /= %= >>= <<= &= ^= |=

1 lewe ,

W tablicy 7. 1 umieszczono zestawienie zbiorcze priorytetów i wiązań poszczególnych operatorów. Na pod­kreślenie zasługuje fakt, że priorytety i wiązania określają jedynie zasady interpre­towania wyrażeń języka, jednak nie określają porządku, w jakim są opracowywane elementy wyrażenia. W języku C porządek ten jest nieokreślony.

Przykład. Wobec tego iż priorytet mnożenia jest wyższy od priorytetów dodawania

i odejmowania, wyrażenie arytmetyczne

a - b + c * d

jest interpretowane tak jak wyrażenie a - b + (c * d)

a wobec tego iż operatory + i - wiążą argumenty od lewej do prawej jest ono in­terpretowane tak jak wyrażenie

(a - b) + (c * d) a nie jak wyrażenie

a - (b + (c * d))

co miałoby miejsce gdyby + i - wiązały argumenty od prawej do lewej. Mimo iż

rozpatrywane wyrażenie jest interpretowane tak jak wyrażenie

(a - b) + (c * d)

nie można wnioskować o kolejności opracowywania argumentów operacji dodawa­nia, gdyż jest ona zależna od implementacji.

W języku C posunięto swobodę opracowywania wyrażeń tak daleko, że zezwolono nie tylko na dowolny wybór kolejności opracowywania podwyraźeń, i to nawet wtedy, gdy pociąga to za sobą skutki uboczne, ale również zezwolono na dowolne przestawienie argumentów operacji łącznych i przemiennych (*, +, &, |, ^) nie ba­cząc na obecność nawiasów.

Przykład. Jeśli a, b i c są dowolnymi podwyrażeniami, nic nie stoi na przeszkodzie, aby w pewnej implementacji wykonanie instrukcji

var = (a + b) + c;

w której var jest identyfikatorem zmiennej prostej, sprowadzono do wykonania instrukcji

var = b + c, var + = a;

Od implementacji zależy także sposób reagowania kompilatora na sytuacje wyjąt­kowe powstające podczas opracowywania wyrażeń. Dotyczy to w szczególności powstawania nadmiaru, dzielenia przez 0 i reagowania na niepoprawnie określone argumenty funkcji.


7.1. Wyrażenia pierwotne

W wyrażeniach pierwotnych występują co najwyżej operatory . i -> oraz nawiasy okrągłe, kwadratowe i wywołania funkcji. Wszystkie one wiążą argumenty od lewej do prawej.

Składnia

wyraźenie-pierwotne:

identyfikator

literał

( wyrażenie)

wyraźenie-pierwotne [ wyrażenie ]

wyraźenie-pierwotne (lista-wyrażeń)

l-wyrażenle-pierwotne. identyfikator

wyraźenie-pierwotne -> identyfikator

Każdy zadeklarowany identyfikator jest wyrażeniem pierwotnym. Typ takiego identyfikatora jest ustalany na podstawie deklaracji, chyba że typem identyfikatora jest “tablica elementów typu...", kiedy to rezultatem opracowania wyrażenia skła­dającego się z tego identyfikatora jest dana o wartości równej wskazaniu na pierwszy element tablicy, a typem tego wyrażenia jest “wskazanie na element typu ...". Należy przy tym nadmienić, że wyrażenie pierwotne, które jest identyfikatorem tablicy nie jest l-wyrażeniem.

Przykład. Jeśli zadeklarowano tablicę arr i zmienną wskazującą ptr

char arr[3][4],

(*ptr)[4];

to w instrukcji

ptr = arr;

rezultatem opracowania wyrażenia składającego się z wyrażenia arr jest dana, której wartością jest wskazanie na pierwszy wiersz tablicy dwuwymiarowej, a typem tego wyrażenia jest (char (*)[4]), tj. wskazanie na 4 elementy typu (char). Ponieważ arr nie jest l-wyrażeniem, niepoprawna byłaby nawet taka instrukcja jak np.

arr = arr;

W podobny sposób jak identyfikatory tablic są traktowane identyfikatory funkcji. Jeśli identyfikator funkcji zostanie zadeklarowany jako “funkcja o rezultacie ty­pu ...", to we wszystkich kontekstach, w których ten identyfikator nie występuje w wywołaniu funkcji w znaczeniu jej nazwy, jest on traktowany jak “wskazanie na funkcję o rezultacie typu...".

Przykład. Jeśli zadeklarowano funkcję fun i zmienną wskazującą ref

char fun(),

(*ref)();

to instrukcja

fun (fun);

mogłaby być zastąpiona instrukcją

ref = fun, fun(ref);

Należy nadmienić, że w każdym z tych przypadków funkcja fun jest wywoływana jednokrotnie.

Wyrażeniami pierwotnymi są także literały. W zależności od ich postaci są one typu (int), (long int) albo (double). Literały znakowe są typu (int), a literały zmiennopozycyjne są typu (double). Literały tekstowe są, podobnie jak jednowymiarowe tablice znakowe, typu “tablica znaków", jednak w wyrażeniach są traktowane po­dobnie jak identyfikatory tablic i z tego względu są typu “wskazanie na znak, tj. (char *). Wyjątek od tej zasady dotyczy jedynie pewnych form przypisywania danych początkowych.

Przykład. Jeśli zadeklarowano tablicę arr i dwie zmienne wskazujące ptr i ref

char arr[4] = "jan",

*ptr,

*ref= {"jan"};

a następnie wykonano instrukcję ptr = "jan";

to spowodowano przypisanie zmiennym ptr i ref danych, których wartościami są wskazania na pierwsze znaki tekstów "Jan", oraz przypisano elementom tabli­cy arr takie dane początkowe, jakby użyto deklaracji

char arr[4] ={ 'j','a','n','\0' };

Dowolne wyrażenie ujęte w nawiasy okrągłe jest wyrażeniem pierwotnym. Wartość i typ tak uzyskanego wyrażenia pierwotnego odpowiednio pokrywa się z wartością i typem wyrażenia w nawiasach. Ujęcie w nawiasy nie nadaje jednak wyrażeniu, które nie jest l-wyrażeniem charakteru l-wyrażenia.

Przykład. Poprawnym wyrażeniem języka C jest m.in.

(ptr) = (ref)

Wyrażenie pierwotne, bezpośrednio po którym następuje wyrażenie ujęte w nawiasy kwadratowe, jest wyrażeniem pierwotnym. W typowych przypadkach wyrażenie pierwotne jest typu “wskazanie na daną typu...", wyrażenie w nawiasach jest ty­pu (int), a rezultatem operacji jest dana typu " ... ". Przyjmuje się z definicji, że wyrażenie

e[i]

jest równoważne wyrażeniu

*((e)+(i))

Ponieważ argumentem operacji wyłuskania (*) może być tylko wyrażenie wskazu­jące, dokładnie jedno z wyrażeń e oraz i powinno być wyrażeniem wskazującym.

Przykład. Wykonanie następującego programu powoduje wyprowadzenie litery b

main(){

putchar((1+"janb")[2]);

}

Argument funkcji putchar można z równym skutkiem przedstawić w postaci 2 [1 + "janb"], ale nie można przedstawić w postaci 2[++"janb"] ponieważ ope­rator ++ może dotyczyć jedynie l-wyrażenia. Wywołanie funkcji składające się z wyrażenia pierwotnego, bezpośrednio po którym następuje ujęta w nawiasy okrągłe lista argumentów wywołania, jest także wyraże­niem pierwotnym. Występujące w tym wywołaniu wyrażenie pierwotne musi być typu “funkcja o rezultacie typu...", a rezultat wywołania funkcji jest typu " ... ". Jeśli wywołanie funkcji ma postać nazewnika, w którym wyrażeniem pierwotnym jest jak dotąd nie zadeklarowany identyfikator, to przyjmuje się, że rezultat funkcji jest typu (int).

Przykład. Wykonanie następującego programu powoduje wyprowadzenie na­pisu Jan

main(){

char *(*sub)(),

*fun();

sub = fun;

printf("%s",(*sub)(2));

}

char *arr[] = { "Ewa","Iza","Jan" };

char *

fun(arg)

char arg;

{ return arr[arg]; }

Wywołanie funkcji ma tu postać

(*sub)(2) i mogłoby w danym programie zostać zastąpione wywołaniem

fun(2)

Tuż przed wywołaniem funkcji jej argumenty typu (float) zostają poddane konwersji na dane typu (double), a argumenty typu (char) i (short int) zostają poddane kon­wersji na dane typu (int). Jeśli argumentami są nazwy tablic albo funkcji, to zostają poddane konwersji na wskazania. Ponieważ tylko wymienione tu konwersje są wy­konywane niejawnie, a rezygnuje się z badania zgodności typu parametrów i odpo­wiadających im argumentów, wszelkie dostosowania typu argumentów do typu parametrów muszą być wykonywane jawnie — za pomocą operatorów konwersji.

Przykład. Jawne konwersje argumentów dla uzyskania zgodności z typami para­metrów

char arr[2][6] = {"Janek"," Marek"},

cnt = 3;

mam(){

fun( (long int)cnt, (char *)arr);

}

fun(par.ref)

long int par;

char *ref;

{ while(par - -)

putchar(*ref + +);

}

W wyrażeniu Stanowiącym drugi argument funkcji fun użyto operatora konwer­sji (char*), ponieważ arr jest typu (char (*)[3]), a typ ten jest odmienny od typu parametru ref. Jak można wywnioskować, w następstwie wykonania przytoczonego programu zostanie wyprowadzony napis Jan. Taki sam wynik otrzymano by, gdyby zastąpiono wywołanie funkcji fun wywołaniem

fun(3L,(char *)arr) q

Ponieważ w języku C skojarzenia parametrów z argumentami dokonują się przez wartość, operacje na parametrach nie mogą spowodować zmian wartości argumen­tów. Nic jednak nie stoi na przeszkodzie, aby argumentem było wskazanie obiektu. W takim przypadku wykonanie operacji na parametrze może spowodować zmianę wartości obiektu wskazywanego przez ten argument, z którym skojarzono dany parametr.

Przykład. Zmiana wartości obiektu wskazywanego przez argument. Wykonanie programu spowoduje wyprowadzenie cyfry 5

main0{

char val = 2;

add(val,&val);

putchar(val+'0');

}

add(one.two)

char one,*two;

{ one = 3;

*two == *two + one;

}

Przypisanie parametrowi one danej o wartości 3 nie powoduje zmiany wartości ar­gumentu val.

Kolejność opracowywania argumentów funkcji zależy od implementacji. Z tego

względu nie wiadomo, czy wykonanie instrukcji

printf("%c%c",putchar('j'),putchar('b'));

spowoduje wyprowadzenie napisu jbjb czy napisu bjjb.

Wszystkie funkcje języka są traktowane tak, jakby były rekurencyjne. Rekurencyjność jest więc integralną częścią definicji języka.

Przykład. Funkcja wyznaczająca n-ty wyraz ciągu Fibonacciego

Fibonacci(n)

char n;

{

if(n<2) return 1;

else

return Fibonacci(n-1) + Fibonacci(n-2);

}

Wyrażeniem pierwotnym języka C jest także wyrażenie składające się z l-wyrażenia pierwotnego reprezentującego strukturę albo unię, po którym następuje operator selekcji zapisany za pomocą symbolu kropka (.), a za nim identyfikator kompo­nentu tej struktury albo unii. Tak skonstruowane wyrażenie pierwotne o posta­ci Iwp.id jest wyrażeniem reprezentującym wybrany komponent danej struktury albo unii.

Ponadto wyrażeniem pierwotnym jest wyrażenie składające się z wyrażenia pier­wotnego, reprezentującego daną wskazującą strukturę albo unię, po którym następuje operator wskazania utworzony z pary symboli minus i większe (->), a po nim identyfikator komponentu tej struktury albo unii. Tak skonstruowane wyrażenie pierwotne o postaci wp->id jest wyrażeniem reprezentującym wybrany komponent danej struktury albo unii. Wyrażenie to jest zawsze równoważne wyra­żeniu (*lwp).id.

Jeśli wyrażenie o postaci Iwp.id albo wp->id reprezentuje komponent struktury albo unii, który nie jest polem ani tablicą, to jest ono l-wyrażeniem. W przeciwnym razie nie jest ono l-wyrażeniem, a więc nie może występować jako lewy argument operatora przypisania i selekcji oraz jako argument operatora inkrementacji, dekrementacji i referencji. Z tego względu w zasięgu deklaracji

struct{

char arr[2];

struct{ charline[80];

} vec;

Int var;

} *ptr,str;

niepoprawne jest wyrażenie &ptr->arr oraz &str.vec.line, mimo iż poprawne jest wyrażenie &ptr->var oraz &ptr->vec.

Przykład. W zasięgu deklaracji

struct{

char chr;

int arr[3];

union{

float smali;

double large;

} real [3];

} str,*ptr = &str;

poprawne są m.in. następujące instrukcje

str.chr = 'j';

ptr->arr[0] =2;

printf("%f", ptr- >real[2].small);

W ostatniej z nich wyrażenie

ptr->real[2].small jest interpretowane tak jak wyrażenie

((ptr->real)[2].small i jest równoważne wyrażeniu

(*ptr).real[2].small


7.2. Operatory jednoargumentowe

Operatory jednoargumentowe wiążą argumenty od prawej do lewej. Składnia wyrażenie-jednoargumentowe:

* wyrażenie

& l-wyraźenie

- wyrażenie

! wyrażenie

~ wyrażenie

+ + l-wyraźenie

- - l-wyraźenie

l-wyraźenie + +

l-wyraźenie - -

( oznaczenie-typu ) wyrażenie

sizeof wyrażenie

sizeof( oznaczenie-typu )


Operator *

Operator ten oznacza wyłuskanie. Następujące po nim wyrażenie musi być typu wskazującego, a rezultatem operacji wyłuskania jest wskazywany obiekt. Jeśli wy­rażenie jest typu “wskazanie na obiekt typu...", to rezultatem operacji wyłuskania jest obiekt typu " ... ".

Przykład. Wykonanie programu powoduje wyprowadzenie znaku J

char arr[2][3];

main(){

(*arr)[0] = 'J';

fun (*arr);

}

fun(par)

char *par;

{ putchar(*par); }

W wyrażeniu *arr argumentem operacji * jest wskazanie na 3-elementowy wektor znaków, a więc rezultatem wyłuskania jest ten właśnie wektor. Rezultatem indekso­wania tego wektora w instrukcji

(*arr)[0] = 'J';

jest element arr[0][0] tablicy arr, a argumentem funkcji fun w instrukcji

fun(*arr);

jest wskazanie na zerowy element wektora *arr.


Operator &

Operator referencji & może dotyczyć tylko l-wyrażenia. Rezultatem operacji jest wskazanie na obiekt reprezentowany przez to l-wyrażenie. Jeśli l-wyrażenie jest typu " ... ", to poprzedzenie go operatorem referencji przekształca je w wyrażenie typu "wskazanie na...". Wyrażenie to nie jest już l-wyrażeniem.

Przykład. Wykonanie programu powoduje wyprowadzenie liczby 13

main(){

int var = 13, *ref = &var;

sub(&ref);

}

sub(par)

int **par;

{printf("%d",**par);}

W wywołaniu funkcji sub identyfikator ref jest l-wyrażeniem typu (int *), a *ref jest wyrażeniem typu (int **).


Operator -

Operator zmiany znaku służy do zanegowania wartości argumentu. W przypadku argumentów typu (unsigned int) operacja ta polega na odjęciu argumentu od 2", gdzie n jest liczbą bitów, za pomocą których jest reprezentowany argument.

Przykład. Jeśli dane typu (int) są reprezentowane za pomocą 16-bitów, to po opra­cowaniu deklaracji

int Signed = -6;

unsigned int Unsigned = -6;

zmienne Signed i Unsigned otrzymują odpowiednio wartości początkowe -6 i 65530.


Operator !

Operator zaprzeczenia służy do porównania wartości argumentu ze stałą o war­tości 0. Jeśli argument ma wartość 0, to rezultatem operacji zaprzeczenia jest 1. Jeśli ma wartość różną od 0, to rezultatem operacji jest 0. Rezultat operacji jest zawsze typu (int), a operacja może dotyczyć tak danych arytmetycznych, jak i wska­zujących.

Przykład. Funkcja, której rezultatem jest dana o wartości -1 dla argumentów ujem­nych, dana o wartości +1 dla argumentów dodatnich i dana o wartości O dla ar­gumentów 0.

sign(par)

int par;

{ return par<0 ? -1 : !par; }


Operator ~

Operator negacji służy do zmiany wartości każdego z bitów stanowiących repre­zentację jego argumentu. Wymaga się, aby argument operatora negacji był typu całkowitego.

Przykład. Jeśli w pewnej implementacji dane typu (Int) są reprezentowane w zapisie uzupełnieniowym do 2, to po opracowaniu deklaracji

int Fix = ~2;

zmienna Fix otrzymuje wartość początkową 1.


Operator preinkrementacji ++ i predekrementacji - -

Operatory preinkrementacji i predekrementacji mogą dotyczyć tylko l-wyrażeń. Wy­konanie operacji ++ powoduje zwiększenie o 1, a wykonanie operacji - - zmniej­szenie o 1, wartości danej identyfikowanej przez l-wyrażenie. Rezultatem operacji jest dana o zmienionej wartości. Typ rezultatu jest identyczny z typem l-wyrażenia. Wyrażenie utworzone z preoperatora i l-wyrażenia nie jest l-wyrażeniem.

Przykład. Wykonanie programu powoduje wyprowadzenie napisu JB

char arr[] = "JB";

main{

char *ref = arr,

*ptr = &arr[1];

sub(* ++ ref, - - ptr);

}

sub(one,two)

char one,*two;

{ putchar(*two),

putchar(one);

}

Operator postinkrementacji ++ i postdekrementacji - - Operatory postinkrementacji i postdekrementacji mogą dotyczyć tylko l-wyrażeń. Wykonanie operacji + + powoduje zwiększenie o 1, a wykonanie operacji - - po­woduje zmniejszenie o 1 wartości danej identyfikowanej przez l-wyrażenie. Rezulta­tem operacji jest dana o pierwotnej wartości. Typ rezultatu jest identyczny z typem l-wyrażenia. Wyrażenie utworzone z l-wyrażenia i postoperatora nie jest l-wyrażeniem.

Przykład. Wykonanie programu powoduje wyprowadzenie napisu JB

char arr[] = "JB";

main(){

char *ref = arr,

*ptr = &arr[1];

sub(*ptr- -,ref++);

}

sub(one,two)

char one,*two;

{ putchar(*two), putchar(one);}


Operator konwersji

Operator konwersji ma postać nazwy typu ujętej w nawiasy okrągłe. Rezultatem konwersji jest dana, której typ pokrywa się z nazwą typu.

Przykład. Jeśli zmienna Fix jest typu (int) i przypisano jej daną o wartości -2, a zmienne typu (short int) są reprezentowane za pomocą 8 bitów w zapisie uzupeł­nieniowym do 2, to rezultatem opracowania wyrażenia

(long int) (short int) Fix

jest dana typu (long int) o wartości 254.


Operator rozmiaru sizeof

Operator rozmiaru może dotyczyć dowolnego wyrażenia albo ujętej w nawiasy okrągłe nazwy typu. W każdym z tych przypadków rezultatem operacji jest dana typu (int), której wartość określa wyrażony w bajtach rozmiar pamięci operacyjnej niezbędny do reprezentowania danych o typie wyrażenia albo danych typu określonego przez nazwę typu. Wyjątek od tej zasady dotyczy jedynie wyrażeń i nazw typów tablico­wych, dla których wartość rezultatu określa rozmiar pamięci niezbędny do reprezen­towania takich tablic.

Mimo iż w języku C nie zdefiniowano pojęcia bajtu, we wszystkich dotychczasowych implementacjach bajt jest jednostką pamięci wystarczającą do reprezentowania da­nych typu (char).

Ponieważ niejednoznaczność syntaktyczną powstałą przy interpretowaniu wyrażenia

sizeof( oznaczenie-typu ) wyrażenie

rozstrzygnięto w ten sposób, że uznano je za równoważne wyrażeniu

(sizeof( oznaczenie-typu)) wyrażenie

w przykładowej implementacji, w której dane typu (int) są reprezentowane za po­mocą 2 bajtów, rezultatami opracowania wyrażeń

sizeof((int)-2L) sizeof(int)-2L

są dane typu (int) odpowiednio o wartościach 2 i 0L.

Przykład. Jeśli w pewnej implementacji dane wskazujące są reprezentowane za po­mocą 2 bajtów, a dane typu (char) za pomocą jednego bajtu, to w zasięgu deklaracji

char *(*ref)[3], arr[3][4];

rezultatami przytoczonych dalej operacji sizeof są dane o następujących wartościach:

sizeof(ref) 2

sizeof(*ref) 6

sizeof(**ref) 2

sizeof(*ref[0][0]) 1

sizeof(arr) 12

sizeof(arr[0]) 4

sizeof(**arr) 1


7.3. Operatory typu mnożenia

Operatorami typu mnożenia są *, / i %. Operatory te są dwuargumentowe, mogą dotyczyć tylko danych arytmetycznych i wiążą argumenty od lewej do prawej.

Podczas opracowywania wyrażeń zawierających operatory typu mnożenia są wykony­wane typowe konwersje arytmetyczne.

Składnia

wyrażenie-z-operatorem-typu-mnoźenia:

wyrażenie * wyrażenie

wyrażenie / wyrażenie

wyrażenie % wyrażenie


Operator *

Operator ten oznacza mnożenie. Ponieważ operacja mnożenia jest łączna i przemien­na, wyrażenia zawierające więcej niż jeden operator typu mnożenia mogą być nie­jawnie przekształcone w wyrażenia matematycznie równoważne.

Rezultatem opracowania operacji mnożenia jest dana, której wartość jest iloczynem wartości argumentów.

Przykład. Mimo jawnego użycia nawiasów, opracowanie wyrażenia

a * (b * c) może rozpocząć się od wyznaczenia wartości iloczynu a * b albo a * c.


Operator /

Operator ten oznacza dzielenie. Rezultatem opracowania operacji dzielenia jest dana, której wartość jest ilorazem pierwszego argumentu przez drugi.

Jeśli oba argumenty operacji są całkowite i dodatnie, to następuje odrzucenie części ułamkowej bez zaokrąglenia. Jeśli przynajmniej jeden ma wartość ujemną, to sposób odrzucenia części ułamkowej zależy od implementacji. W większości implementacji reszta ma taki sam znak jak dzielna.

Przykład. Rezultatem dzielenia danej o wartości 3 przez daną o wartości 5 jest dana o wartości 0.


Operator %

Operator ten oznacza określenie reszty z dzielenia pierwszego argumentu przez drugi.

Za definicję operacji % można przyjąć wzór, który zapisany w języku C ma postać

a % b = a - (a / b) * b

Przykład. Resztą z dzielenia danej o wartości 5 przez daną o wartości 3 jest dana o wartości 2.


7.4. Operatory typu dodawania

Operatorami typu dodawania są + i -. Operatory te są dwuargumentowe i wiążą argumenty od lewej do prawej. Podczas opracowywania wyrażeń zawierających te operatory są wykonywane typowe konwersje arytmetyczne. W odróżnieniu od ope­racji typu mnożenia, argumentami operacji typu dodawania mogą być także dane wskazujące.

Składnia

wyraźenie-z-operatorem-typu-dodawania:

wyrażenie + wyrażenie

wyrażenie - wyrażenie


Operator +

Operator ten oznacza dodawanie. Ponieważ operacja dodawania jest łączna i prze­mienna, wyrażenia zawierające więcej niż jeden operator dodawania mogą być przekształcone w wyrażenia matematycznie równoważne.

Rezultatem operacji dodawania jest dana, której wartość jest sumą wartości ar­gumentów. Jeśli jednym z argumentów jest dana wskazująca, to druga dana musi być całkowita. Rezultat dodawania jest wówczas daną wskazującą.

Operacje dodawania dotyczące danych wskazujących mają w praktyce sens jedynie wtedy, gdy zarówno dana wskazująca, jak i rezultat operacji dotyczą tej samej ta­blicy. W takim przypadku jeśli ptr reprezentuje daną wskazującą i-ty element ta­blicy, a j reprezentuje daną całkowitą, to

ptr + j reprezentuje daną wskazującą element i+j tej tablicy.

Przykład. Jeśli w zasięgu deklaracji

char arr[3][4], (*ref)[4], (*ptr)[4];

zostaną wykonane instrukcje

ref = arr;

ptr = ref + 2;

to ref będzie wskazywać element arr[0], a ptr będzie wskazywać element arr[0]. Różnica adresów stanowiących reprezentacje tych wskazań będzie w większości implementacji równa 8.

Operator -

Operator ten oznacza odejmowanie i może dotyczyć nie tylko danych arytmetycz­nych, ale i danych wskazujących.

Rezultatem operacji odejmowania jest dana, której wartość jest różnicą wartości ar­gumentów. Jeśli jednym z argumentów jest dana wskazująca, a drugim dana całko­wita, to obowiązują zasady jak dla dodawania danej całkowitej i danej wskazującej.

Oznacza to, że jeśli ptr reprezentuje daną wskazującą i-ty element tablicy, a j repre­zentuje daną całkowitą, to

ptr - j

reprezentuje daną wskazującą element tablicy o indeksie i-j. Jeśli oba argumenty odejmowania są typu wskazującego, to operacja odejmowania ma sens jedynie wtedy, gdy wartościami obu danych są wskazania na elementy tej samej tablicy. W takim przypadku jeśli ptr reprezentuje daną wskazującą element i, a ref reprezentuje daną wskazującą element j, to np.

ptr - ref

reprezentuje daną o wartości i-j. Przykład. Jeśli w zasięgu deklaracji

char arr[3][4], len;

zostanie wykonana instrukcja

len = arr[2] - (char *)arr;

to zmiennej len zostanie przypisana dana o wartości 2.


7.5. Operatory przesuwania

Operatorami przesuwania są << i >>. Operatory te wiążą argumenty od lewej do prawej, a podczas opracowywania wyrażeń zawierających te operatory są wykony­wane typowe konwersje arytmetyczne. Zarówno lewy, jak i prawy argument operacji musi być całkowity. Przed wykonaniem operacji prawy argument zostaje poddany konwersji na daną typu (int). Rezultatem operacji jest dana takiego typu jak lewy argument. .

Jeśli prawy argument operacji przesuwania ma wartość ujemną albo jeśli ma wartość większą albo równą liczbie bitów użytych do reprezentowania przesuwanej danej, to rezultatem przesuwania jest dana o wartości nieokreślonej.

Składnia

wyrażenie-z-operatorem-przesunięcia:

wyrażenie << wyrażenie

wyrażenie >> wyrażenie


Operator <<

Operator ten oznacza przesunięcie w lewo. Rezultatem operacji e1<<e2 jest dana, której reprezentacja powstała z reprezentacji danej e1 po przesunięciu jej w lewo o e2 pozycji. Podczas tego przesuwania na miejsce bitów najmniej znaczących wcho­dzą bity 0, a bity najbardziej znaczące są odrzucane.

Przykład. Jeśli var jest zmienną całkowitą, to instrukcja

var = ((var<<2) + var)<<1;

realizuje pomnożenie var przez 10. Ze względu na odrzucanie bitów najbardziej znaczących rezultat jest poprawny tylko wtedy, gdy 10*var mieści się w zakresie danych tego typu, jakiego jest var.


Operator >>

Operator ten oznacza, przesunięcie w prawo. Rezultatem operacji e1>>e2 jest dana, której reprezentacja powstała z reprezentacji danej e1 po przesunięciu jej w prawo o e2 pozycji. Jeśli przesuwana dana jest typu (unsigned), to bity najmniej znaczące są odrzucane, a na miejsce bitów najbardziej znaczących wchodzą bity 0. W pozo­stałych przypadkach bity najmniej znaczące także są odrzucane, ale stan bitów najbardziej znaczących zależy od implementacji. W pewnych implementacjach mogą to być bity 0 (przesunięcie logiczne), a w innych powielone bity znaku (przesunięcie arytmetyczne).

Przyklad. Jeśli var jest zmienną typu (unsigned int), to instrukcja

var - = (var>>1)<<1;

realizuje przypisanie zmiennej var danej o wartości równej reszcie z dzielenia var przez 2.


7.6. Operatory relacji

Operatorami relacji są <, >, <= i >=. Operatory te wiążą argumenty od lewej do prawej, jednak znajomość tego faktu jest mało użyteczna, gdyż np. wyrażenie

a<b<c jest równoważne wyrażeniu

a<b ? c<1 : c<0 a nie - jak można by się spodziewać - wyrażeniu

a<b ? b<c :0

Składnia

wyrażenie-z-operatorem-relacji:

wyrażenie < wyrażenie

wyrażenie > wyrażenie

wyrażenie < = wyrażenie

wyrażenie > = wyrażenie

Jeśli relacja jest prawdziwa, to rezultatem operacji < (mniejsze), > (większe), <= (mniejsze lub równe) albo >= (większe lub równe) jest dana o wartości' 1. W przeciwnym razie rezultatem operacji jest dana o wartości 0. W obu przypadkach dane te są typu (int). Przed określeniem prawdziwości relacji są wykonywane typowe konwersje arytmetyczne.

Jeśli relacja dotyczy danych wskazujących, to ma sens jedynie wtedy, gdy obie te dane wskazują elementy tej samej tablicy.

Przykład. Jeśli Uns jest zmienną typu (unsigned int) o wartości 3, to rezultatem relacji

-5 > Uns

jest dana o wartości l. Wynika to z faktu, że przed określeniem prawdziwości relacji jest wykonywana konwersja wyrażenia -5 na daną typu (unsigned int).


7.7. Operatory porównań

Operatorami porównań są = = i !=. Operatory te są dwuargumentowe i wiążą. argumenty od lewej do prawej. Są one analogiczne do operatorów relacji, tyle że mają niższy priorytet.

Składnia

wyraźenie-z-operatorem-porównania:

wyrażenie = = wyrażenie

wyrażenie != wyrażenie

Jeśli porównanie jest prawdziwe, to rezultatem opracowania wyrażenia z operatorem porównania jest dana o wartości 1. W przeciwnym razie rezultatem jest dana o war­tości 0. W obu przypadkach dane te są typu (int). Przed określeniem prawdziwości porównania są wykonywane typowe konwersje arytmetyczne.

Jeśli porównanie dotyczy danej wskazującej i danej całkowitej, to wyrażenie jest poprawne, a rezultat jest niezależny od implementacji tylko wtedy, gdy dana całko­wita ma wartość 0.

Przykład. Jeśli arr jest zadeklarowane jako wektor, to rezultatem porównania

arr = = &arr[0] jest dana typu (int) o wartości 1.


7.8. Bitowy operator koniunkcji

Bitowym operatorem koniunkcji jest dwuargumentowy operator &. Ponieważ koniunkcja jest operacją łączną i przemienną, wyrażenia zawierające więcej niż jeden operator koniunkcji mogą być przekształcone w wyrażenia matematycznie rów­noważne.

Przed określeniem wartości koniunkcji są wykonywane typowe konwersje arytme­tyczne. Koniunkcja może dotyczyć tylko danych całkowitych.

Składnia

wyrażenie-z-bitowym-operatorem-koniunkcji:

wyrażenie & wyrażenie

Rezultatem opracowania wyrażenia z bitowym operatorem koniunkcji jest dana typu (int), której reprezentacja powstaje z iloczynu logicznego wyznaczonego równo­legle na wszystkich bitach reprezentacji argumentów.

Przykład. Jeśli var jest zmienną typu (int) o wartości 10, to rezultatem opracowa­nia wyrażenia.

var & 12

jest dana typu (int) o wartości 8.


7.9. Bitowy operator różnicy symetrycznej

Bitowym operatorem różnicy symetrycznej jest dwuargumentowy operator ^. Po­nieważ różnica symetryczna jest operacją łączną i przemienną, wyrażenia zawiera­jące więcej niż jeden operator różnicy symetrycznej mogą być przekształcone w wy­rażenia matematycznie równoważne.

Przed określeniem wartości różnicy symetrycznej są wykonywane typowe konwersje arytmetyczne. Różnica symetryczna może dotyczyć tylko danych całkowitych, a jej rezultatem jest dana typu (int).

Składnia

wyraźenie-z-bitowym-operatorem-różnicy-symetrycznej:

wyrażenie ^ wyrażenie

Rezultatem opracowania wyrażenia z bitowym operatorem różnicy symetrycznej jest dana typu (int), której reprezentacja powstaje z różnicy symetrycznej wyznaczo­nej równolegle na wszystkich bitach reprezentacji argumentów.

Przykład. Jeśli var jest zmienną typu (Int) o wartości 10, to rezultatem opracowania wyrażenia

var ^ 12

jest dana typu (Int) o wartości 6. D


7.10. Bitowy operator sumy logicznej

Bitowym operatorem sumy logicznej jest |. Ponieważ operacja | jest łączna i prze­mienna, wyrażenia zawierające więcej niż jeden operator | mogą być przekształcone w wyrażenia matematycznie równoważne.

Przed wykonaniem określenia wartości bitowej sumy logicznej są wykonywane ty­powe konwersje arytmetyczne. Bitowa suma logiczna może dotyczyć tylko danych całkowitych.

Składnia

wyrażenie-z-bitowym-operatorem-sumy-logicznej:

wyrażenie | wyrażenie

Rezultatem opracowania wyrażenia z bitowym operatorem sumy logicznej jest dana typu (int), której reprezentacja powstaje z sumy logicznej wyznaczonej równolegle na wszystkich bitach reprezentacji argumentów.

Przykład. Jeśli var jest zmienną typu (int) o wartości 10, to rezultatem opracowania wyrażenia

var | 12

jest dana typu (int) o wartości 14.


7.11. Operator koniunkcji

Operatorem koniunkcji jest dwuargumentowy operator &&. Operator ten wiąże argumenty od lewej do prawej i w odróżnieniu od operatora & gwarantuje opraco­wanie wyrażenia w takiej właśnie kolejności. Co więcej, jeśli lewy argument operacji ma wartość różną od zera, to rezygnuje się z opracowania prawego argumentu.

Argumenty operatora koniunkcji nie muszą być tego samego typu, a nawet mogą być typu wskazującego.

Składnia

wyrażenie-z-operatorem-koniwtkcji:

wyrażenie && wyrażenie

Rezultatem opracowania wyrażenia z operatorem koniunkcji jest dana typu (Int). Dana ta ma wartość 1, jeśli oba argumenty mają wartości różne od zera, w przeciw­nym razie ma wartość 0.

Przykład. Jeśli ptr jest zmienną wskazującą, to rezultatem operacji

ptr&& getchar() = = 'j'

jest dana o wartości 1, ale tylko wtedy, gdy wartością ptr nie jest wskazanie puste, a znakiem wprowadzonym ze standardowego urządzenia wejściowego jest j. Należy nadmienić, że gdyby ptr miało wartość 0, to rezultatem operacji byłaby dana o war­tości 0, a wywołanie funkcji getchar nie zostałoby zrealizowane.


7.12. Operator sumy logicznej

Operatorem sumy logicznej jest dwuargumentowy operator ||. Operator ten wiąże argumenty od lewej do prawej i w odróżnieniu od operatora | zapewnia opracowanie wyrażenia w takiej właśnie kolejności. Co więcej, jeśli lewy argument operacji ma wartość różną od zera, to rezygnuje się z opracowania prawego argumentu.

Argumenty operatora sumy logicznej nie muszą być tego samego typu, a nawet mogą być typu wskazującego.

Składnia

wyrażenie-z-operatorem-sumy-logicznej:

wyrażenie || wyrażenie

Rezultatem opracowania wyrażenia z operatorem sumy logicznej jest dana ty­pu (int). Dana ta ma wartość 1, jeśli przynajmniej jeden argument ma wartość różną od zera, w przeciwnym razie natomiast ma wartość 0.

Przykład. W zasięgu deklaracji

char vec[15], ind;

rezultatem opracowania wyrażenia

ind>14 || ind<0 || !vec[ind]

jest dana o wartości 1, jeśli wartość ind wykracza poza zakres dozwolonych indeksów wektora vec albo jeśli wartość ta mieści się we wspomnianym zakresie, ale element wektora ma wartość 0. Należy nadmienić, że gdyby ind miało np. wartość 20, to indeksowanie wektora vec nie byłoby realizowane. D


7.13. Operator warunkowy

Operatorem warunkowym jest ?:. Operator ten wiąże argumenty od prawej do lewej i jest operatorem trójargumentowym.

Składnia

wyrażenie-z-operatorem-warunkowym:

wyrażenie ? wyrażenie : wyrażenie

Opracowanie wyrażenia z operatorem warunkowym składa się z opracowania wy­rażenia poprzedzającego symbol ? i stosownie do jego wartości uznania za rezultat operacji rezultatu opracowania jednego z dwóch pozostałych wyrażeń. Jeśli wyraże­nie poprzedzające ? ma wartość różną od zera, to dokonuje się opracowania drugiego wyrażenia. W przeciwnym razie dokonuje się opracowania trzeciego. W ten sposób zawsze rezygnuje się z opracowania jednego wyrażenia, ale za typ rezultatu obiera się typ różnicy po obu stronach symbolu : chyba że są to wyrażenia wskazujące tego samego typu albo wyrażenie wskazujące i literał 0, kiedy to za typ rezultatu obiera się typ wyrażenia wskazującego.

Przykład. W zasięgu deklaracji zmiennej prostej var oraz funkcji

extern int f();

extern float g();

wyrażenie

var ? f(2) : g(4)

jest traktowane tak jak wyrażenie var ? (float)f(2) : g(4)

a jego opracowanie powoduje wywołanie dokładnie jednej z wymienionych tu funk­cji f i g.

Ponieważ operator warunkowy wiąże swoje argumenty od prawej do lewej, złożone wyrażenie warunkowe, takie jak np.

a ? b ? c : d : e ? f : g

jest interpretowane tak jak wyrażenie a ? (b ? c : d) : (e ? f : g)

a nie jak wyrażenie

(a? (b? c: d) :e)? f : g

(co miałoby miejsce gdyby operator warunkowy wiązał argumenty od lewej do prawej).

Przykład. Funkcja do wyznaczania największego z jej trzech argumentów max(a,b,c) int a,b,c;

{ return a>b ? a>c ? a:c : b>c ? b:c; }


7.14. Operatory przypisania

W języku C zdefiniowano 11 wymienionych dalej operatorów przypisania. Każdy z tych operatorów jest dwuargumentowy i wiąże argumenty od lewej do prawej.

Lewy argument operatora musi być l-wyrażeniem. Rezultatem opracowania wyra­żenia z operatorem przypisania jest dana przypisana lewemu argumentowi. Składnia

wyrażenie-z-operatorem-przypisania:

l-wyrażenle = wyrażenie

l-wyraźenie + = wyrażenie

l-wyrażenie - = wyrażenie

l-wyrażenie *= wyrażenie

l-wyrażenie /= wyrażenie

l-wyraźenie %= wyrażenie

l-wyrażenie >> = wyrażenie

l-wyrażenie <<= wyrażenie

l-wyrażenie &= wyrażenie

l-wyraźenie ^= wyrażenie

l-wyrażenie |= wyrażenie

(złożone operatory przypisania składają się z dwóch jednostek leksykalnych).

Jeśli oba argumenty operacji są typu arytmetycznego, to w przypadku pierwszego z wymienionych tu operatorów przypisania dana stanowiąca rezultat opracowania wyrażenia zostaje poddana konwersji na daną o typie zmiennej reprezentowanej przez l-wyraźenie.

W pozostałych przypadkach wyrażenie o postaci

e1 op= e2 jest traktowane tak jak wyrażenie

e1 =e1 op (e2) z tą różnicą, że opracowanie wyrażenia e1 jest jednokrotne.

Jeśli e1 reprezentuje zmienną typu wskazującego, to poza operatorem = jest dozwo­lone posługiwanie się tylko operatorami + = i -=. W takich przypadkach obo­wiązują zasady opisane podczas omawiania operatorów + i -.

Dopuszczone przez wiele implementacji i nie powodujące konwersji przypisywanie danych wskazujących zmiennym całkowitym i odwrotnie oraz niekontrolowanie zgodności typów wskazujących lewej i prawej strony należy uznać za praktykę sprzecz­ną z duchem języka C. Jedyną bowiem dopuszczalną formą automatycznej kon­wersji z typu (int) na typ wskazujący jest posłużenie się wyrażeniem, którego prawą stronę stanowi literał 0. Dana reprezentowana przez ten literał zostaje poddana kon­wersji na unikalną daną wskazującą, której wartością jest wskazanie puste.

Przykład. W zasięgu deklaracji

int Int;

char Chr;

instrukcja

Int += Char = Int = 258;

jest równoważna instrukcji

Int = 258, Char = Int, Int = Int + Char;

W tych implementacjach, w których dane typu (char) są reprezentowane za pomocą 8 bitów, zmienna Char otrzyma wartość 2, a zmienna Int otrzyma wartość 260.


7.15. Operator połączenia

Operatorem połączenia jest , (przecinek). Operator ten jest dwuargumentowy i wią­że argumenty od lewej do prawej.

Składnia

wyrażenie-z-operatorem-polączeiua:

wyrażenie , wyrażenie

Opracowanie wyrażenia z operatorem połączenia składa się z opracowania wyrażenia przed przecinkiem, a następnie opracowania wyrażenia po przecinku. Rezultatem opracowania całego wyrażenia jest rezultat opracowania wyrażenia po przecinku. Typem rezultatu jest typ tego wyrażenia.

Przykład. Funkcja realizująca przepisanie niepustego ciągu znaków różnych od znaku '\0'.

copy(target,source)

char *target,*source;

{ while(*target++ = *source++, *source);}

Użyta tu instrukcja while jest wykonywana tak jak instrukcja

do *target++ = *source; while(*source);

W tych kontekstach, w których przecinek ma znaczenie specjalne, a więc w listach wartości początkowych oraz w listach argumentów i parametrów, nie występuje on w znaczeniu operatora połączenia. Można mu nadać takie znaczenie, czyniąc wyra­żenie z operatorem połączenia wyrażeniem pierwotnym.

Przykład. Zmiennej Chr zostaje przypisana dana o wartości 'b'

char Chr = {('j','b')};

Symbol, występuje tu w znaczeniu operatora połączenia. Gdyby opuszczono nawiasy okrągłe, to deklaracja zmiennej Chr byłaby błędna, ponieważ symbol , miałby znaczenie separatora między parą literałów, z których co najwyżej jeden może okre­ślać wartość danej przypisywanej zmiennej Chr.


8. Deklaracje

Deklaracje dzielą się na definicyjne - nazywane również definicjami i odwoławcze. Pierwsze z nich służą do tworzenia i definiowania obiektów przetwarzania, drugie nawiązują do deklaracji definicyjnych powodując rozszerzenie zakresu dostępności identyfikatora.

Składnia

deklaracja:

oznaczenia-klasy-i-typu lista-deklaratorów;

oznaczenia-klasy-i-typu;

oznaczenie-klasy oznaczenia-klasy-i-typu

oznaczenie-typu oznaczenia-klasy-i-typu

Lista deklaratorów zawiera wykaz deklarowanych identyfikatorów, natomiast ozna­czenia klasy i typu określają ich klasę i typ. Wymaga się, aby oznaczenia klasy i typu nie zawierały sprzecznych ani powtórzonych określeń klasy i typu. Zaleca się, aby oznaczenie klasy występowało przed oznaczeniami typu.

Przykład. Kilka poprawnych deklaracji

extern int Counter;

char BigLetter,SmallLetter;

static long float Value,Size;

auto union name { int arr[3];

char chr;} *ptr[4],**ref;


Zakres obowiązywania deklaracji rozciąga się od jej punktu początkowego aż do końca bezpośrednio obejmującej ją instrukcji grupującej. W przypadku funkcji oraz jej parametrów zakres rozciąga się od punktu początkowego aż do końca definicji funkcji. Punkt początkowy jest w obu tych przypadkach definiowany jako miejsce pierwszego wystąpienia identyfikatora obiektu w jego deklaratorze. Dzięki takiemu ustaleniu poprawna jest m.in. deklaracja struktury str

struct list{

int var;

struct list *link;

}str;

w której deklaracja komponentu link może powoływać się na jeszcze nie w pełni zdefi­niowaną strukturę typu (struct list).

Ze względu na występowanie w programach deklaracji przesłaniających, zasięg de­klaracji może być mniejszy od zakresu. Ilustruje to następujący program, którego wykonanie powoduje wyprowadzenie napisu jb.

main(){

char var = 'b';

{ char var = 'j';

putchar(var);

}

putchar(var);

}

W programie tym zakres deklaracji pierwszego identyfikatora var rozciąga się od miejsca jego wystąpienia aż do końca programu, natomiast zasięg jest mniejszy od zakresu o tę instrukcję grupującą, w której zawarto deklarację drugiego identyfi­katora var.

Przykład. Następująca definicja funkcji jest błędna nie dlatego, że występują w niej kolidujące deklaracje identyfikatorów funkcji, parametru i zmiennej, ale dlatego, że wyrażenie inicjujące występujące z prawej strony operatora przypisania reprezen­tuje daną o wartości nieokreślonej.

char id(id)

char id;

{ char id = id;

return id;

}


8.1. Oznaczenia klasy pamięci

Oznaczenia klasy pamięci mogą dotyczyć zarówno identyfikatorów danych, jak i iden­tyfikatorów funkcji. Określają one zakres dostępności obiektu oznaczonego danym identyfikatorem, a w przypadku danych określają ponadto warunki utworzenia i sposób traktowania obiektu.

Składnia

oznaczenie-klasy:

auto

statlc

extern

register

typedef

Ostatnie z wymienionych tu oznaczeń klasy nie dotyczy obiektów, lecz służy jedynie do związania identyfikatora z wybraną rodziną obiektów określonego typu i klasy.

Przykład. W zasięgu deklaracji

static int Taiły;

typedef int *IntPtr;

identyfikator Taiły reprezentuje daną typu (int) klasy static, a identyfikator IntPtr odgrywa rolę stówa kluczowego, za pomocą którego można deklarować obiekty, którym można przypisywać dane wskazujące dane typu (int), np.

IntPtr ref[3];


Klasa auto

Obiektami klasy auto mogą być tylko zmienne proste i agregaty zmiennych. Obiekty te są tworzone i inicjowane podczas każdego wykonywania prologu instrukcji gru­pującej zawierającej deklarację identyfikatora i są usuwane podczas wykonywania epilogu tej instrukcji.

Przykład. Następujący program jest błędny;

main(){

int cnt = 2;

while(cnt- -)

{ auto int Var;

if(cnt)

putchar('j');

else

putchar(Var);

Var = 'b';

}

}

Mimo iż w większości implementacji wykonanie tego programu powoduje wyprowa­dzenie napisu jb, jest on programem błędnym, ponieważ w chwili drugiego wywo­łania funkcji putchar zmienna Var ma wartość nieokreśloną.


Klasa static

Obiektami klasy static mogą być stałe, zmienne proste, agregaty zmiennych i funkcje. Obiekty te są tworzone i inicjowane podczas wykonywania prologu programu i są usuwane podczas wykonywania jego epilogu.

Przykład.

Wykonanie następującego programu powoduje wyprowadzenie napisu jb

main(){

int cnt = 2;

while(cnt- -){

static char Chr = 'j';

putchar(Chr);

Chr = 'b';

}

}

Gdyby atrybut static zastąpiono atrybutem auto, to wykonanie programu spowodo­wałoby wyprowadzenie napisu jj.

Niezależnie od użytego atrybutu klasy, zakresem i zasięgiem deklaracji zmien­nej Chr jest instrukcja grupująca stanowiąca ciało instrukcji while.


Klasa extern

Obiektami klasy extern mogą być zmienne proste, agregaty zmiennych i funkcje. Jeśli obiekt zostanie zadeklarowany bez atrybutu klasy, a deklaracja występuje na poziomie definicji funkcji, to deklaracja taka jest uznawana za definiującą, a obiekt jest klasy extern przez domniemanie. Taka deklaracja obiektu o danym identyfi­katorze może w zbiorze sekcji programu wystąpić tylko jednokrotnie. Pozostałe de­klaracje tego samego identyfikatora z atrybutem extern mają jedynie charakter odwoławczy i mogą występować wielokrotnie.

Podobnie jak obiekty klasy static, obiekty klasy extern są tworzone jednokrotnie, podczas wykonywania prologu programu i są usuwane podczas wykonywania jego epilogu.

Przykład. Wykonanie następującego programu (składającego się z dwóch sekcji) powoduje wyprowadzenie napisu jb:

/* sekcja 1 */ main(){

extern char Chr;

extern fun();

fun();

putchar(Chr);

}

/* sekcja 2 */ int Chr = 'j';

fun()

{ putchar(Chr);

Chr = 'b';

}

Użycie drugiej deklaracji extern jest zbędne, ponieważ wystąpienie niezadeklarowanego identyfikatora w znaczeniu nazwy funkcji kontekstowo deklaruje ten identyfi­kator jako nazwę funkcji udostępniającej rezultat typu (int), co w danym przypadku prowadzi do domniemania deklaracji

extern int fun();


Klasa register

Obiektami klasy register mogą być tylko zmienne proste i agregaty zmiennych. Z punktu widzenia semantyki programu obiekty klasy register nie różnią się od obiektów klasy auto. Należy jedynie pamiętać, że ponieważ mogą być przechowy­wane w rejestrach, nie ma sensu wyrażenie, w którym operator & dotyczy podwyrażenia reprezentującego obiekt klasy register. Ponieważ liczba rejestrów komputera jest zazwyczaj ograniczona, w praktycznych implementacjach tylko dane pewnych typów, np. (int) i (char), mogą być przechowywane w rejestrach. Ponadto może okazać się, że tylko kilka pierwszych deklaracji obiektów klasy register jest traktowa­nych w omówiony tutaj sposób, zaś pozostałe są traktowane tak, jakby były dekla­racjami obiektów klasy auto.

Przykład. W następującej funkcji posłużono się klasą register dla przyśpieszenia ope­racji wykorzystujących parametr funkcji.

fun(ptr)

int *ptr;

{ register int *ref = ptr;

*ref = getchar();

putchar(*ref);

}

Ponieważ klasa register może dotyczyć także i parametrów funkcji, przytoczoną definicję można dodatkowo ulepszyć, nadając jej postać:

fun(ptr) register *ptr;

{ putchar(*ptr = getchar()); }


8.2. Oznaczenia typu

Oznaczenia typu mogą dotyczyć zarówno identyfikatorów danych, jak i identyfika­torów funkcji. Określają one typ obiektu oznaczonego danym identyfikatorem, a w przypadku identyfikatorów funkcji określają typ udostępnianego przez nią rezultatu.

Składnia

oznaczenie-typu:

char

short

int

long

unsigned

float

double

opis-struktury-lub-unii

identyfikator-typu

Słowa kluczowe long, short i unsigned są traktowane jak przymiotniki, co czyni sensownymi tylko takie połączenia, jak

short int

long int

unsigned int

long float

Przyjęto z definicji, że typ (long float) jest identyczny z typem (double). Poza przy­toczonymi połączeniami nie są poprawne inne pary oznaczeń typu, a więc nie istnie­je w języku np. typ (short float). Jeśli natomiast zostanie opuszczone oznaczenie typu, np. w deklaracji

static Yalue;

to zostanie domniemane oznaczenie typu (int).


8.3. Deklaratory

W obrębie deklaracji może wystąpić jeden deklarator albo lista deklaratorów. Każdy z takich deklaratorów może występować w postaci z inicjacją.

Składnia

deklarator-z-inicjacją:

deklarator = inicjator

Każdy z deklaratorów zawiera identyfikator deklarowanego obiektu, a ponadto może zawierać informacje świadczące o tym, że obiekt jest wektorem, zmienną wska­zującą albo funkcją. Ponieważ informacje takie mogą być łączone w złożone zestawy, zbiór deklaratorów języka jest nieskończony.

Składnia

deklarator:

identyfikator

( deklarator )

deklarator [ wyrazenie-stale0 ]

*deklarator

deklarator ()

Zasady wiązania użytych tu operatorów są takie same jak dla wyrażeń.

Przykład. Kilka poprawnych deklaracji:

a. Statyczna zmienna całkowita

static int var;

b. Automatyczna tablica wskazań na dane typu (char)

auto char *arr[3][4];

c. Rejestrowe wskazanie na tablicę typu (char)

register char (*ref)[3][4];

d. Statyczna funkcja udostępniająca rezultat, którym jest wskazanie na wektor ele­mentów typu (float)

static float(*fun())[3];

e. Zmienna wskazująca na wektor wskazań na funkcje udostępniające rezultaty typu wskazanie na daną typu (short int)

shortint *(*(*ptr)[3])();


8.4. Znaczenie przypisywane deklaratorom

Deklaratory służą do deklarowania identyfikatorów. Użycie deklaratora stanowi stwierdzenie, iż każde wystąpienie konstrukcji mającej taką samą postać jak dekla­rator stanowi odwołanie do obiektu o określonym typie i ustalonej klasie.

Jeśli deklarator składa się z samego identyfikatora, to identyfikator ten reprezentuje obiekt o typie i klasie wynikających z nagłówka oznaczenia.

Przykład. Deklaracja identyfikatora reprezentującego statyczną zmienną skalarną typu (int)

static var;

Nagłówek oznaczenia jawnie określa klasę. Typem domniemanym jest (int).

W przypadku bardziej złożonych deklaratorów konieczne jest niekiedy posłużenie się nawiasami okrągłymi dla określenia innej kolejności wiązań. Jeśli nawiasy są użyte w nadmiarze, to nie powoduje to zmiany semantyki deklaracji.

Przykład. Para deklaracji równoważnych

int *ref[3];

Int *(ref[3]);

Ponieważ priorytet operatora [] jest wyższy od priorytetu operatora *, użycie na­wiasów okrągłych jest zbędne.

Rozpatrzmy obecnie deklarację identyfikatora w ogólnej postaci T w której T jest oznaczeniem typu, a D jest deklaratorem. Deklaracja taka orzeka, iż identyfikator zawarty w D reprezentuje:

obiekt typu (T),

tablicę obiektów typu (7),

wskazanie na obiekt typu (T),

funkcję, której rezultatem jest obiekt typu (T).

Deklarator użyty w przytoczonym kontekście może zostać rozbudowany do po­staci (D), D[e], *D i D().


Postać (D)

Semantyka deklaracji nie ulega zmianie, a identyfikator zawarty w (D) reprezentuje taki sam obiekt, jak identyfikator zawarty w D. Posłużenie się nawiasami okrągłymi jest niezbędne jedynie wtedy, gdy ich opuszczenie powoduje zmianę kolejności wiązań operatorów.

Przykłady

a. Para deklaracji nisrównoważnych

char *ptr[3];

char(*ptr)[3];

b. Para deklaracji równoważnych

int(*ptr)[3][4];

int((*ptr)[3])[4];


Postać D[e]

Odpowiednio do tego co reprezentował identyfikator zawarty w D, identyfikator zawarty w D[e] reprezentuje:

tablicę obiektów typu (T),

tablicę tablic obiektów typu (T),

tablicę wskazań na obiekty typu (T),

tablicę funkcji, których rezultatami są obiekty typu (T).

Wymaga się, aby e było wyrażeniem stałym typu (int) albo aby było puste. Jeśli deklarator zawiera kilka następujących bezpośrednio po sobie napisów o postaci [e], to stanowi to deklarację tablicy wielowymiarowej.

Jeśli deklaracja ma charakter definiujący, to niezbędne jest określenie rozmiaru każdego z wymiarów tablicy. Wyjątek stanowią jedynie deklaratory z inicjacją, gdyż dla nich rozmiar pierwszego wymiaru może być określony na podstawie analizy inicjatora.

Ponieważ w języku C elementy tablic są uporządkowane wierszami, rozmiar pierw­szego wymiaru nigdy nie jest potrzebny do wyznaczenia położenia elementu tablicy. Z tego względu w deklaratorach tablic, których identyfikatory są parametrami funk­cji zawsze można pominąć rozmiar pierwszego wymiaru.

Przykłady. Użycie deklaratorów o postaci D[e]

a. Deklaracja zmiennej typu (int) oraz tablicy elementów typu (int)

int var;

int arr[3];

b. Deklaracja dwuwymiarowej tablicy o elementach typu (char)

char arr[3][4];

c. Deklaracje tablic za pomocą deklaratorów, w których pominięto rozmiary pierw­szych wymiarów

char vec[] = "vera";

int arr[][3] = { 5,6,7,8 }

W deklaratorze wektora vec zostanie domniemany rozmiar 5, a w deklaratorze tablicy arr zostanie domniemany rozmiar 2,

Postać *D

Odpowiednio do tego, co reprezentował identyfikator zawarty w D, identyfikator zawarty w *D reprezentuje

wskazanie na obiekt typu (T),

wskazanie na tablicę obiektów typu (T),

wskazanie pośrednie na obiekt typu (T),

wskazanie na funkcję, której rezultatem jest obiekt typu (T).

Przykłady. Użycie deklaratorów o postaci *D

a. Deklaracja zmiennej całkowitej typu (int) oraz zmiennej wskazującej obiekty typu (int)

int var;

int *ref;

b. Deklaracja zmiennej wskazującej tablicę elementów typu (char) oraz tablicy wskazań na obiekty typu (char)

char(*ref)[3][4];

char *ptr[3][4];

c. Deklaracja zmiennej wskazującej obiekty wskazujące obiekty typu

float) float **ref;


Postać D()

Odpowiednio do tego, co reprezentował identyfikator zawarty w D, identyfikator zawarty w D() reprezentuje

funkcję, której rezultatem jest obiekt typu (T),

funkcję, której rezultatem jest wskazanie na obiekt typu (T),

(dwa wiersze puste dotyczą przypadków niedozwolonych, tj. funkcji których rezul­tatami byłyby tablice albo funkcje).

Przykłady. Użycie deklaratorów o postaci D()

a. Deklaracja zmiennej typu (int) oraz funkcji, której rezultatem jest dana ty­pu (int)

int var;

int fun();

b. Deklaracja funkcji, której rezultatem jest dana wskazująca obiekt typu (char) oraz deklaracja zmiennej wskazującej funkcję, której rezultatem jest dana typu (char)

char *fun();

char(*ptr)();

c. Deklaracja zmiennej wskazującej funkcję, której rezultatem jest dana wskazują­ca tablicę elementów typu (float)

float (*(*ref)())[3][4];

Zabrania się, aby deklaracje struktur i unii zawierały deklaracje funkcji, jak również aby rezultatami funkcji były funkcje i agregaty, a komponentami agregatów funkcje. Przykład. Kilka błędnych deklaracji

int fun()[3]; — rezultatem funkcji jest tablica

int vec[3](); — komponentem wektora jest funkcja

int fun()(); — rezultatem funkcji jest funkcja

union

{ int fun();

} Soviet; — deklaracja unii zawiera deklarację funkcji


8.5. Deklaracje struktur i unii

Struktura jest obiektem składającym się z ciągu nazwanych komponentów. Kom­ponenty mogą być dowolnych typów, ale nie mogą być funkcjami. Unia jest obiektem, który w danej chwili składa się z jednego komponentu należącego do ustalonego i skończonego zbioru komponentów. Opisy struktur i unii różnią się tylko słowami kluczowymi struct i union.

Składnia

opis-struktury-lub-unii:

struct-lub-union { wykaz-komponentów }

stuct-lub-union oznacznik { wykaz komponentów )

struct-lub-union oznacznik struct-lub-union'.

struct union

oznacznik:

identyfikator

Wykaz komponentów składa się z ciągu deklaracji poszczególnych komponentów struktury albo unii, a oznacznik jest identyfikatorem, który po zdefiniowaniu może być używany do uproszczonego powoływania się na strukturę lub unię o określonej budowie. W szczególności oznaczenie typu o postaci

struct id { wykaz-komponentów }

deklaruje identyfikator id jako oznacznik struktury typu (struct { wykaz-komponentów }) dzięki czemu w dalszych deklaracjach struktur o budowie identyfikowanej przez id można ograniczyć się do opisu

struct id

Ponieważ deklaracja id następuje już w momencie jego wystąpienia jako oznacznika, a więc bezpośrednio po słowie kluczowym struct albo union, dopuszczalne jest deklarowanie agregatów, których komponenty odwołują się do typu właśnie de­klarowanych struktur albo unii.

Przykłady. Deklarowanie struktur, unii i oznaczników

struct{

int month;

Int day;

} date;

union tag

{ char *name;

char *address;

};

union tag jan,ewa;


struct status{

char married;

int year[3];

} kaja;

struct status vera;

struct{

struct ins{

int var;

}agr;

char chr;

} rec;

struct ins str;

Struktura date jest typu (struct{ int month,day; }) i składa się z komponentów date.month i date.day. Unie Jan i ewa są typu

(union tag) tj. typu

(union{ char *name,*address; })

Unia jan składa się z komponentu jan.name albo z komponentu jan.address, a unia ewa składa się z komponentu ewa.name albo ewa.address. Struktury kaja i vera są tego samego typu (struct status)

tj. typu (struct{ char married; int year[3]; })

Struktura str jest typu (struct ins), tj. typu (struct{ int var; })

Nazwy komponentów unii i struktur nie mogą pokrywać się z identyfikatorami oznaczników, ale mogą pokrywać się z nazwami zmiennych, struktur i tablic. Dwie różne struktury albo unie mogą składać się z komponentów o tych samych nazwach, byle tylko identyczność nazw i typów dotyczyła także komponentów poprzedzają­cych dane komponenty. To poważne ograniczenie języka jest w pewnych imple­mentacjach uznane za niebyłe.

Jak już częściowo wynika z przytoczonych ustaleń, wykazy komponentów zawarte w opisach struktury albo unii składają się z ciągu deklaracji.

Składnia

wykaz-komponentów:

opis-komponentów

opis-komponentów wykaz-komponentów

opis-komponentów:

oznaczenie-typu lista-deklaratorów-komponentów ;

deklarator-komponentu:

deklarator

deklarator : wyraźenie-stałe

: wyrażenie-stale

W typowych przypadkach komponentami struktur i unii są dane albo agregaty da­nych podstawowych typów. Jeśli deklarator komponentu zawiera symbol :, to kom­ponentem jest dana reprezentowana za pomocą takiej liczby bitów, jaką określa wartość wyrażenia stałego. Jeśli przed tym symbolem nie występuje deklarator, to komponent agregatu jest niedostępny, ponieważ jest pozbawiony nazwy.

Komponent, którego nazwa występuje w deklaratorze przed symbolem :, jest nazy­wany polem agregatu. Mimo iż w języku C nie wprowadzono żadnych ograniczeń dotyczących pól, w typowych implementacjach pola są danymi całkowitymi bez znaku, a tworzenie tablic pól oraz wykonywanie na polach operacji & jest za­bronione.

Przykłady

Deklaracja struktury

struct Complex{ float re,im;

}str;

struct Complex *ptr[3];

Struktura str składa się z komponentów str.re i str.im. Oznacznik Complex iden­tyfikuje struktury o budowie takiej jak struktura str.

W zasięgu przytoczonych deklaracji identyfikator str reprezentuje strukturę skła­dającą się z dwóch zmiennych typu (float), reprezentowanych przez str.re i str.tm, a ptr reprezentuje tablicę zmiennych wskazujących, których wartościami są wskaza­nia na struktury typu (Complex) zdefiniowanego podczas deklarowania struk­tury str. Ponieważ definiowanie oznacznika Complex może być rozłączne z dekla­rowaniem struktury str, przytoczone deklaracje mogłyby zostać zapisane w postaci równoważnej jako

struct Complex{ float re,im;};

struct Complex str,* ptr[3];

b. Deklaracja unii

union{

float Value;

int lndex;

} Trade;

W każdej chwili wykonywania programu zawierającego przytoczoną deklarację istnieje co najwyżej jedna z pary zmiennych Trade. Value i Trade.lndex.

c. Deklaracje pól

struct{ int paper:3,ink:3;

int bright:1;

char *text;

}display;

Nazwy display.paper, display.ink i display.bright reprezentują pola. Nazwa display.text reprezentuje daną wskazującą na dane typu (char).

Komponenty struktury są rozmieszczone w pamięci operacyjnej w kolejności ich wystąpienia w deklaracji struktury i z uwzględnieniem wymagań wynikających z ich typu. Z tego względu w tych implementacjach, w których dane typu (float) muszą być umieszczone na granicy słowa, posłużenie się deklaracją

struct{

float head;

char mid;

float tail;

}str[3];

spowoduje powstanie niewykorzystanych i niedostępnych obszarów pamięci ope­racyjnej.

Sposób rozmieszczania pól jest odmienny. Sąsiadujące pola są upakowywane w sło­wach maszynowych, a każde z pól musi się mieścić w jednym słowie. Pola mogą być umieszczone w pamięci w takim porządku, w jakim wystąpiły w deklaracji struktury, albo w porządku odwrotnym. Jeśli pole nie mieści się w pewnym słowie, to jest przenoszone do następnego. Przejście do następnego słowa można także wymusić, posługując się deklaracją pola bez nazwy i z wyrażeniem stałym o war­tości 0.

Przykład. Jeśli w pewnej implementacji słowo maszynowe jest 32-bitowe, a dane typu (int) są reprezentowane za pomocą 16 bitów i muszą być umieszczane na gra­nicy słowa, to dla struktur typu (struct StrType)

struct StrType{

int head;

int :0;

int tail:2;

};

między komponentami head i taił występuje nienazwane pole, które zajmuje 16 bitów.


8.6. Przypisywanie danych początkowych

Jeśli po deklaratorze identyfikatora występuje inicjator, to oznacza to, że obiektowi reprezentowanemu przez ten identyfikator ma zostać przypisana dana o określonej wartości początkowej. Przypisanie to następuje w momencie utworzenia obiektu.

Jeśli identyfikator reprezentuje zmienną prostą arytmetyczną albo wskazującą, to dana przypisywana tej zmiennej stanowi rezultat opracowania wyrażenia zawartego w inicjatorze. Jeśli identyfikator reprezentuje agregat, to inicjator składa się z ujętej w nawiasy klamrowe listy inicjatorów, a dana przypisywana agregatowi składa się z komponentów, których wartości są określone przez elementy wspomnianej listy. Zabrania się stosowania inicjatorów bezpośrednio po deklaratorach unii oraz po deklaratorach agregatów klasy auto.

Składnia

inicjator:

wyrażenie

{ wyrażenie }

{ lista-inicjatorów }

{lista-inicjatorów ,}

Jeśli wyrażenia występujące w inicjatorze dotyczą obiektów klasy static albo extern, to każde z nich musi być wyrażeniem stałym albo ograniczonym wyrażeniem wska­zującym. To ostatnie musi redukować się do wyrażenia wskazującego na uprzednio zadeklarowany obiekt lub jego komponent albo musi redukować się do takiego właśnie wyrażenia, od którego odjęto albo do którego dodano wyrażenie stałe.

W odróżnieniu od inicjatorów obiektów klasy static i extern inicjatory obiektów klasy auto i register mogą zawierać dowolne wyrażenia. Jeśli typy wyrażeń występujących w inicjatorze nie są identyczne z typami inicjo­wanych obiektów i ich komponentów, to przeprowadzane są typowe konwersje arytmetyczne oraz konwersje danych reprezentowanych przez liczby 0 na odpowied­nie dane wskazujące.

Przykłady

a. Przypisywanie danych początkowych zmiennym prostym

int Var =5*5;

static char arr[5],

*ref = (&arr[4] - &arr[2])/2 + &arr[2];

fun(par) int par;

{ auto res = Var * Var;

return res + par;

}

Zmiennej Var zostanie przypisana dana początkowa o wartości 25, zmiennej ref zostanie przypisana dana początkowa, której wartością jest wskazanie na arr[3], a zmiennej res zostanie przypisana dana początkowa o wartości 625.

b. Przypisywanie danych początkowych tablicom

static int arr[2][3] = { { 5, 8, 4 },{2,3,7}}, vec[3] = {-5, 3, -4};

Komponentom pierwszego wiersza tablicy arr zostaną przypisane dane początkowe o wartościach 5, 8 i 4, a komponentom drugiego wiersza tej tablicy zostaną przypi­sane dane początkowe o wartościach 2, 3 i 7. Komponentom wektora vec zostaną przypisane dane początkowe o wartościach -5, 3 i -4.

c. Przypisywanie danych początkowych strukturom

static struct{ char chr;

struct{ int var;

float arr[2] ;

} num[2];

int fix;

}str={‘j’,{{2,{3.0,4.0}}.

{5,{6e2,7e2}}

},8};

Komponentowi str.num[0].var przypisano daną o wartości początkowej 2, a kom­ponentowi str.num[1].arr[1] przypisano daną o wartości początkowej stanowiącej przybliżenie liczby 700.0.

Ponieważ zapis inicjatorów zawierających pełen zestaw nawiasów klamrowych jest niekiedy dość uciążliwy, dopuszczono w języku C kilka uproszczeń.

Uproszczenia te wynikają z faktu, iż w pełnym zapisie inicjatora każdemu skalar­nemu komponentowi agregatu odpowiada w liście inicjatorów pewne wyrażenie, a każdemu agregatowi odpowiada lista inicjatorów ujęta w nawiasy klamrowe. Te dwie zasady są stosowane rekurencyjnie, dzięki czemu kolejne poziomy nawiasów klamrowych wytyczają podział inicjatora analogiczny do hierarchicznego podziału agregatu na jego komponenty. Z tego względu z pełnego zapisu inicjatora mogą być usunięte wszystkie wewnętrzne nawiasy klamrowe.

Innym uproszczeniem jest możliwość usunięcia z dowolnych nawiasów klamrowych tych końcowych wyrażeń, które są równoważne liczbom 0, jako że o takie właśnie liczby zostaną przez domniemanie wydłużone zbyt krótkie listy inicjatorów.

Domniemanie liczb 0 jest stosowane także w odniesieniu do obiektów klasy static oraz extern, które wystąpiły w deklaracji bez inicjatora. Deklaracje takich obiektów są traktowane tak, jakby użyto inicjatora w pełnym zapisie z wszystkimi wyraże­niami o postaci liczb 0.

Dogodnym uproszczeniem jest także możliwość przedstawienia inicjatora jednowy­miarowej tablicy znakowej w postaci tekstu znakowego, który jest wówczas trakto­wany tak, jak ujęta w nawiasy klamrowe lista tworzących go znaków. W takim przy­padku kolejne znaki tego tekstu określają wartości początkowe danych przypisanych kolejnym elementom tablicy, a deklaracja taka jak np.

char vec[3] = "jb";

zostaje uznana za równoważną deklaracji char vec[3]= {'j'.'b','\0'};

Ostatnim uproszczeniem jest możliwość domniemania, na podstawie listy inicja­torów, rozmiaru pierwszego wymiaru. Za przykład takiego domniemania może służyć deklaracja

int arr[][3]={{1,2,3},{4,5,6}};

równoważna deklaracji

int arr[2][3] = { {1,2,3},{4,5,6} };

Biorąc pod uwagę większość przytoczonych tu uproszczeń, pozostaje wyjaśnić, że deklaracja z inicjatorem w pełnym zapisie, o postaci

char arr[2][3] = { {'1','2','\0'}'{'3','\0','\0'} };

jest równoważna każdej z następujących deklaracji

char arr[][3]={{‘1’,'2'},{'3'}};

char arr[2][3] = {"12","3"};

ale nie jest równoważna deklaracji

char arr[2][3] = "12\03\0\0";

ponieważ jest ona błędna (inicjator o postaci literału tekstowego nie dotyczy tablicy jednowymiarowej).

Przykłady. Uproszczenia zapisu inicjatorów

  1. Opuszczanie nawiasów klamrowych obejmujących listy inicjatorów oraz opusz­czanie wyrażeń równoważnych liczbom 0

struct{ int arr[2][3], vec[2];

}str= {{1,2,0}, {0,0,0}, {7,0}};

Użyty tu inicjator mógłby zostać zastąpiony inicjatorem

{1,2,0,0,0,0,7,0} jak również inicjatorem

{1,2,0,0,0,0,7} oraz inicjatorem

{{1,2},{0},7}

b. Domniemanie inicjatorów z liczbami zero dla obiektów klasy static i extern static struct{

int *ptr[2];

char chr;

float num[2];

} str;

Deklaracja struktury str jest traktowana tak, jakby wystąpił w niej inicjator

{ { 0,0 }, 0, { 0,0 } } równoważny w danym kontekście inicjatorowi

{0}

c. Przedstawienie inicjatora tablicy znakowej w postaci tekstu znakowego

char arr[3] = "jb";

Przytoczona deklaracja jest równoważna deklaracji

char arr[3]={'j','b','\0'};

oraz deklaracji

char arr[3]={'j','b'};

d. Domniemanie rozmiaru na podstawie analizy inicjatora

char arr[][4] » {"Jan","Ewa","lza"};

Przytoczona deklaracja jest równoważna deklaracji

char arr[3][4] = { {‘J','a’,'n’,’\0'},

{‘E','w’,'a’,’\0'},

{‘I','z’,'a’,’\0'}}


8.7. Nazwy typów

Nazwy typów występują w języku C w dwóch kontekstach:

w operatorach kon­wersji oraz w argumentach operatora sizeof.

Składnia

nazwa-typu:

oznaczenie-typu pseudodeklarator

pseudodeklarator:

puste

(pseudodeklarator)

*pseudodeklarator

pseudodeklarator()

pseudodeklarator [ wyrażenie-stale ]

puste:


Dla uniknięcia niejednoznaczności zabrania się ujmowania w nawiasy okrągłe pseudodeklaratora, którym jest puste. Dzięki temu założeniu można jednoznacznie określić takie miejsce w deklaratorze, że umieszczenie tam identyfikatora przekształci pseudodeklarator w deklarator. Typ reprezentowany przez nazwę typu uznawany jest wówczas za identyczny z typem wspomnianego identyfikatora.

Przykład. Jeśli nazwa typu ma postać

char *(*())[3] to przekształcenie jej w deklarację

char *(*id())[3];

umożliwia stwierdzenie, że id reprezentuje funkcję, której rezultatem jest dana o war­tości stanowiącej wskazanie na 3-elementowe wektory wskazań na dane typu (char). Stąd wnioskujemy, że obiekty typu (char *(*())[3]) są funkcjami, których rezultatami są dane wskazujące na 3-elementowe wektory wskazań na dane typu (char).


8.8. Oznaczenie klasy typedef

Deklaracje, w których oznaczenie klasy ma postać typedef nie określają sposobu przydzielania pamięci, a jedynie definiują identyfikatory, które mogą być później używane tak, jakby stanowiły słowa kluczowe identyfikujące podstawowe i pochodne typy danych.

Składnia

identyfikator-typu:

identyfikator

Zasadę definiowania identyfikatorów typów najlepiej wyjaśnić na przykładzie. Jeśli posłużono się przykładowymi deklaracjami nazw typów

typedef char (*PtrVec)[3];

typedef char Vec[2];

typedef struct{ float re,im; } Cplx;

to w zasięgu tych deklaracji konstrukcje

PtrVec vec,arr[4];

Vec *ref;

Cplx num;

są odpowiednio równoważne deklaracjom

char (*vec)[3], (*arr[4])[3];

char(*ref)[2];

struct{ float re.im;

} num;

(na szczególną uwagę zasługuje użycie nawiasów okrągłych w deklaracji identyfi­katora ref).

Ponieważ deklaracje nazw typów nie służą do tworzenia nowych typów, a jedynie związują arbitralnie wybrane identyfikatory z typami już istniejącymi, w zasięgu deklaracji typu takiej jak np.

typedef float real;

typy (float) i (real) są nieodróżnialne. Przykład. Jeśli w zasięgu deklaracji

typedefint *IntPtr;

IntPtr ptr;

int *ref;

posłużono się instrukcją

ptr = (lntPtr)(ref+2);

to może być ona uproszczona do

ptr = ref + 2;

jako że wyrażenie ref + 2 jest typu (IntPtr).



9. Instrukcje

Jeżeli opis nie stanowi inaczej, instrukcje są wykonywane w kolejności ich wystąpie­nia w programie.


9.1. Instrukcja składająca się z wyrażenia

Większość instrukcji programu ma postać wyrażenia, bezpośrednio po którym następuje średnik. Przykładami takich instrukcji są wywołania funkcji i przypisania.

Składnia

instrukcja-wyrażenie:

wyrażenie,

Wykonanie takiej instrukcji składa się z opracowania zawartego w niej wyrażenia i odrzucenia rezultatu tego opracowania.

Przykład. Jeśli var jest identyfikatorem zmiennej, to wykonanie instrukcji 3 + (var = 5);

składa się z przypisania zmiennej var danej o wartości 5 i odrzucenia danej o war­tości 8.


9.2. Instrukcja grupująca

Konstrukcję nazwaną tu instrukcją grupującą, której ciało stanowi sekwencja lo­kalnych deklaracji i instrukcji, wprowadzono do języka po to, aby w miejscach, w których składnia wymaga użycia jednej instrukcji, można było użyć ich więcej.

Składnia

instrukcja-grupująca:

{ zestaw-deklaracji zestaw-instrukcji }

zestaw-deklaracji:

deklaracja

deklaracja zestaw-deklaracji

zestaw-instrukcji:

instrukcja

instrukcja zestaw-instrukcji

Wykonanie instrukcji grupującej składa się z sekwencyjnego wykonania zawartych w niej instrukcji. W ramach wykonania prologu instrukcji grupującej następuje utworzenie i zainicjowanie obiektów klasy auto i register zadeklarowanych w zesta­wie deklaracji. W ramach wykonania epilogu następuje usunięcie tych obiektów.

Ponieważ instrukcja grupująca stanowi blok, ponowne deklaracje identyfikatorów, już zadeklarowanych w otoczeniu instrukcji grupującej, mają znaczenie lokalne i w zakresie instrukcji grupującej przesłaniają deklaracje z jej otoczenia.

Jeśli instrukcja grupująca zawiera deklaracje zmiennych klasy auto albo register, to ewentualne inicjacje tych zmiennych odbywają się każdorazowo podczas wykony­wania prologu tej instrukcji. W przypadku zmiennych klasy static inicjacje takie odbywają się jednokrotnie, w ramach wykonania prologu programu. Jeśli instrukcja grupująca zawiera deklaracje zmiennych klasy extern, to są to jedynie deklaracje odwoławcze, a jako takie nie mogą występować w postaci z inicjacją.

Przykład. Użycie instrukcji grupującej w kontekście, w którym dozwolone jest użycie tylko pojedynczej instrukcji

if(var){

register int num = var * 3;

printf("%d",num);

var = 0;

}


9.3. Instrukcja warunkowa

Instrukcja ta umożliwia warunkowe wykonanie dowolnej instrukcji języka. Składnia

instrukcja-warunkowa:

if( wyrażenie) instrukcja

if( wyrażenie ) instrukcja else instrukcja

Wykonanie instrukcji warunkowej składa się z opracowania wyrażenia, a następ­nie - jeśli rezultat jest daną o wartości różnej od zera - wykonania instrukcji występującej po nawiasie zamykającym. Jeśli rezultat jest daną o wartości 0, to w pierwszym przypadku wykonanie instrukcji warunkowej uznaje się za zakończo­ne, a w drugim wykonuje się instrukcję występującą po else.

Jeśli w instrukcji warunkowej jest zawarta inna instrukcja warunkowa, to uznaje się, że kolejne else jest skojarzone z ostatnim if, z którym nie skojarzono else.

Przykład. W instrukcji warunkowej

if (ref)

if(ptr)

var = 2;

else

var = 3;

else

var = 4;

skojarzenia else z if są takie jak pokazano za pomocą wcięć.


9.4. Instrukcja while

Instrukcja while jest instrukcją złożoną wytyczającą cykl. Składnia

instrukcja-while:

while( wyrażenie ) instrukcja

Wykonanie instrukcji while przebiega wg poniższego algorytmu:

1. Określany jest rezultat opracowania wyrażenia,

2. Jeśli rezultat ten jest daną o wartości 0, to wykonanie instrukcji while uważa się za zakończone.

3. Jeśli rezultat jest daną o wartości różnej od 0, następuje wykonanie instrukcji po nawiasie zamykającym, a następnie powtórzenie opisanych czynności od początku.

Przykład. Funkcja do sumowania elementów listy fun(ptr) struct list{ struct list *link;

int val;

} *ptr;

{ register sum = 0;

while(ptr)

sum += ptr->val, ptr = ptr->link;

return sum;

}


9.5. Instrukcja do

Instrukcja do jest instrukcją złożoną wytyczającą cykl.

Składnia

instrukcja-do:

do instrukcja while( wyrażenie),

Wykonanie instrukcji do przebiega wg poniższego algorytmu:

1. Wykonywana jest instrukcja występująca po słowie kluczowym do.

2. Określany jest rezultat opracowania wyrażenia.

3. Jeśli rezultat ten jest daną o wartości 0, to wykonanie instrukcji do uważa się za zakończone.

4. Jeśli rezultat jest daną o wartości różnej od 0, to następuje powtórzenie opisanych czynności od początku.

Przykład. Funkcja udostępniająca najbliższy znak różny od spacji

skip(par) char *par;

{ register *ref = par;

do *ref = getchar();

while(*ref==’ ‘);

return *ref;

}

Przytoczona funkcja może być także przedstawiona w postaci

skip(par) char *par;

{ register *ref;

do

while((*ref = getchar()) == ' ');

return *ref;

}


9.6. Instrukcja for

Instrukcja for jest instrukcją złożoną wytyczającą cykl. Składnia

instrukcja-for:

for (wyrażenie-a;

wyrażenie-b ;

wyrażenie-c) instrukcja

Jeśli w instrukcji for występuje wyrażenie-b, to jest ona równoważna parze in­strukcji

wyrażenie-a, while ( wyrażenie-b )

{

instrukcja

wyrażenie-c,

}

Jeśli wyrażenie-b zostanie opuszczone, to przez domniemanie przyjmuje się, że jest nim liczba 1.

Przykład. Funkcja do znajdowania wskazania na ostatnie wystąpienie znaku w za­danym ciągu znakowym.

char *last(string,chr) char *string,chr;

{ register char *ptr;

char *match;

for(match = 0, ptr == string; *ptr;)

if(*ptr+ + = = chr) match = ptr - - 1;

return match;

}



9.7. Instrukcja switch

Instrukcja switch jest instrukcją złożoną mającą charakter instrukcji wyboru.

Składnia

instrukcja-switch:

switch ( wyrażenie) instrukcja

przedrostek:

case wyrażenie-stale : instrukcja

default : instrukcja

Wykonanie instrukcji switch składa się z opracowania wyrażenia, a następnie kon­tynuowania wykonywania programu od tej instrukcji zawartej w instrukcji switch, która jest poprzedzona przedrostkiem

case wyrażenie-stale:

albo

default:

W ogólnym przypadku rezultat opracowania wyrażenia jest porównywany z rezul­tatami opracowań kolejnych wyrażeń stałych zawartych w przedrostkach należą­cych do danej instrukcji decyzyjnej, a kontynuowanie wykonywania programu roz­poczyna się od instrukcji następującej po pierwszym przedrostku, dla którego zo­stanie stwierdzona równość wartości wspomnianych rezultatów.

Jeśli równość taka nie wystąpi, to kontynuowanie wykonywania programu rozpo­czyna się od instrukcji następującej po przedrostku default:. Jeśli przedrostek taki nie występuje, to wykonanie instrukcji switch zostaje uznane za zakończone.

Należy zaznaczyć, że opisane tu wyrażenia występujące w instrukcji switch muszą być typu (Int), a rezultaty opracowań wyrażeń stałych muszą być różne. Jeśli in­strukcja switch ma postać

switch( wyrażenie) instrukcja-grupująca

to ewentualne inicjacje występujące w deklaracjach rozpoczynających ciało instrukcji grupującej nie są brane pod uwagę.

W odróżnieniu od wielu innych języków programowania, w języku C wykonanie ciągu instrukcji następujących po wybranym przedrostku nie wyklucza wykonania ciągu instrukcji związanych z następnym przedrostkiem. Aby tego uniknąć, można posłużyć się instrukcją break.

Przykład.

Wykonanie instrukcji

switch(val){

case2: printf("*");

default: printf("*");

case3: printf("*");

} jest równoważne wykonaniu instrukcji

switch(val){

case2: printf(" ***");

break;

case3: printf("*");

break;

default: printf("**");

)


9.8. Instrukcja break

Instrukcja break umożliwia zakończenie wykonywania bezpośrednio obejmującej ją instrukcji złożonej while, do, for albo switch.

Składnia

instrukcja-breuk :

break ;

Po wykonaniu instrukcji break dalsze wykonywanie programu przebiega tak, jakby właśnie zostało zakończone wykonywanie instrukcji while, do, for albo switch bezpośrednio obejmującej daną instrukcję break.

Przykład. Jeśli arr jest tablicą zadeklarowaną jako

int arr[5][5];

a zmienne sum, linę, col i fig są całkowite, to wykonanie instrukcji

for(sum=0, line=5; line - -;)

for(col=5; col - -;)

if(!arr[line][col])

sum++;

else break;

powoduje nadanie zmiennej sum takiej samej wartości, jak wykonanie instrukcji

for(sum==0, line=5; line - - ;)

for(flg=1,col=5;col - - &&flg;)

if(flg = !arr[line][col]) sum++ ;


9.9. Instrukcja continue

Instrukcja continue umożliwia przejście do wykonania czynności warunkujących ponowne wykonanie bezpośrednio obejmującej ją instrukcji while, do albo for.

Składnia

instrukcja-continue:

continue ;

W zależności od tego jaka instrukcja cyklu bezpośrednio obejmuje instrukcję continue, wykonanie instrukcji

continue;

może w każdym z trzech następujących przypadków być zastąpione wykonaniem instrukcji goto lab;

Cykl while: Cykl do: Cykl for:

while(...){ do { for(...){

... ... ...

lab: ; lab: ; lab: ;

} }while(...); }


Przykład. Funkcja do wyznaczania liczby spacji kończących w wektorze o zadanym rozmiarze

count(vec,size)

char vec[],size;

{ int sum = 0;

for( ;size- - ; sum + +)

{ if(vec[size] = = ‘ ‘)

continue;

break;

}

return sum;

}

Instrukcja for jest wykonywana tak, jak instrukcja

loop:

while (size - -){

if(vec[size] = = ‘ ‘)

{ sum++;

goto loop;

}

break;

}


9.10. Instrukcja return

Instrukcja return umożliwia zakończenie wykonywania funkcji.

Składnia

instrukcja-return:

return ;

return wyrażenie ;

Wykonanie instrukcji return powoduje zakończenie wykonywania funkcji. Jest ono poprzedzone wykonaniem epilogów wszystkich obejmujących ją instrukcji gru­pujących.

Jeśli instrukcja return ma pierwszą z wymienionych postaci, to rezultatem wywo­łania funkcji jest dana o wartości nieokreślonej. Jeśli instrukcja return zawiera wyrażenie, to rezultatem wywołania funkcji jest re­zultat opracowania tego wyrażenia poddany ewentualnej konwersji na daną typu zadeklarowanego w nagłówku funkcji.

Jeśli ostatnią instrukcją ciała funkcji nie jest instrukcja return, to przed nawiasem. klamrowym zamykającym definicję funkcji domniemywa się instrukcję return;

Przykład. Rezultatem wywołania funkcji

fłoat convert()

{ return 5; }

jest dana typu (float) o wartości będącej przybliżeniem wartości liczby 5.0.

9.11. Instrukcja goto

Instrukcja goto umożliwia odstąpienie od zasady sekwencyjnego wykonywania na­stępujących po sobie instrukcji programu.

Składnia

instrukcja-goto:

goto identyfikator;

Wykonanie instrukcji goto powoduje przejście do wykonywania instrukcji opa­trzonej etykietą identyczną z identyfikatorem wymienionym w tej instrukcji.

Wymaga się, aby identyfikatorowi odpowiadała etykieta zawarta w tej samej funkcji. w której występuje instrukcja goto. Nie zabrania się, aby wykonanie instrukcji goto spowodowało przejście do wnętrza instrukcji grupującej, w której nie jest zawarta dana instrukcja goto. W takim przypadku nie nastąpi przypisanie danych począt­kowych tym zmiennym, które są inicjowane w danym bloku.

Przykład. Sekwencja instrukcji

goto inside;

{ int var = 5;

inside:

printf("%d",var);

)

jest niepoprawna nie z powodu wykonania instrukcji goto wiodącej do wnętrza in­strukcji grupującej, ale z powodu próby odwołania się do danej o wartości nie­określonej.


9.12. Instrukcja opatrzona etykietą

Każda instrukcja programu może być poprzedzona etykietą albo ciągiem etykiet. Etykiety oddziela od siebie oraz od instrukcji programu symbol :.

Składnia

instrukcja-opatrzona-etykietą.

przedrostek-etykietowy instrukcja

przedrostek-etykietowy:

etykieta :

etykieta:

identyfikator

Wystąpienie w przedrostku etykietowym identyfikatora deklaruje go kontekstowo jako etykietę. Zakresem takiej deklaracji jest najszersza instrukcja grupująca defi­nicji funkcji obejmującej dany przedrostek etykietowy. Zasięgiem jest zakres po­mniejszony o zbiór tych instrukcji grupujących, w których wystąpiły deklaracje identyfikatorów o postaci rozpatrywanej etykiety.

Przykład. Instrukcja

label: {

label:

putchar('*');

}

jest niepoprawna, ponieważ w zawierającej ją funkcji występują dwie etykiety label. Należy zauważyć, że w większości innych języków programowania analogiczny fragment programu byłby poprawny.


9.13. Instrukcja pusta

Instrukcja pusta umożliwia określenie czynności, których wykonanie nie będzie miało żadnych skutków.

Składnia

instrukcja-pusta:

;

Przykład. Następująca instrukcja zawiera jedną instrukcję pustą.

for( ; count - - ;) ;


12. Dyrektywy preprocesora

Kompilator języka C zawiera preprocesor przystosowany do wykonywania makro-substytucji, kompilacji warunkowej i włączania zawartości wskazanych plików. Dyrektywy preprocesora są zawarte w wierszach rozpoczynających się od znaku #. Wiersze takie mogą wystąpić w dowolnym miejscu tekstu źródłowego, a zakres zde­finiowanych w nim identyfikatorów jest niezależny od zakresu identyfikatorów pro­gramu i w ogólnym przypadku obejmuje cały tekst źródłowy.

Jeśli pewna dyrektywa nie mieści się w wierszu, to może być kontynuowana wg zasad jak dla literałów znakowych i tekstowych.

Przykład. Jeśli kreska ukośna jest ostatnim znakiem wiersza, to w dwóch nastę­pujących wierszach

#define ewa "\

#define jan"""

zapisano dyrektywę

#define ewa"#define jan""" D


12.1. Makrosubstytucja

Wystąpienie w tekście źródłowym dyrektywy o postaci

#define identyfikator ciąg-jednostek-leksykalnych

powoduje, że każde następne użycie identyfikatora zostanie zastąpione ciągiem jed­nostek leksykalnych.

Przykład. Rozwinięciem tekstu źródłowego

main(){

#define put putchar(

#defme end )

put'*'end;

} jest tekst

main(){

putchar('*');

}

bardziej złożone makrodefinicje można tworzyć za pomocą dyrektyw o postaci

# define identyfikator(llsta-parametrów) cląg-jednostek-leksykalnych

Wystąpienie takiej makrodefinicji powoduje, że każde następne użycie identyfikatora w kontekście

identyfikator{lista-argumentów)

zostanie zastąpione ciągiem jednostek leksykalnych, w którym każdy identyfikator parametru zostanie zastąpiony odpowiadającym mu argumentem.

Dla odróżnienia makrodefinicji z listą parametrów od makrodefinicji bez takiej listy wymaga się, aby między identyfikatorem a nawiasem otwierającym nie występował odstęp.

Przykład. W zasięgu makrodefinicji

#define div(num,den) num/den rozwinięciem wiersza

printf("%d",div((a+b)*c,2));

jest wiersz

printf("%d",(a+b)*c/2);

natomiast w zasięgu makrodefinicji

#deflne div (num,den) num/den rozwinięciem tego samego wiersza jest wiersz

printf("%d",(num,den) num/den((a+b)*c,2)); D

Jak w każdej liście, tak i w liście argumentów makrowywołania, poszczególne argumenty są oddzielone przecinkami. Jeśli jednak przecinki znajdują się wewnątrz nawiasów okrągłych albo literałów znakowych i tekstowych, to nie są uznawane za separatory argumentów. Co więcej, w obrębie literałów znakowych i tekstowych nie przeprowadza się makrosubstytucji.

Przykład. W zasięgu makrodefinicji

#define comma(par1,par2) parł1==par2

rozwinięciem wiersza

if(fun comma((a,b),","))break;

jest wiersz

if(fun (a,b)==",")break; D

Niezależnie od formy użytej dyrektywy #defme, bezpośrednio po dokonaniu rozwi­nięcia wiersza źródłowego następuje przejrzenie go od początku dla dokonania. ewentualnych dalszych makrosubstytucji.

Przykład. W zasięgu makrodefinicji

#define add (x,y) div(x,y) + 2

#define div(x,y) x/y rozwinięciem wiersza

a = add(b + c,d) jest wiersz

a = b + c/d + 2 Jeśli intencją było uzyskanie rozwinięcia

a = (b + c)/d + 2 to definicja identyfikatora div powinna zostać zmieniona do postaci

#definediv(x,y) (x)/(y) a jeszcze lepiej do postaci

#define div(x,y) ((x)/(y))

jako że dopiero ta ostatnia gwarantuje uzyskanie zamierzonego rozwinięcia w każdym kontekście.

W tych sytuacjach, gdy pożądane jest ograniczenie zakresu makrodefinicji tylko do pewnego fragmentu tekstu źródłowego, można posłużyć się dyrektywą o postaci

#undef identyfikator

Bezpośrednio po wykonaniu takiej dyrektywy użyty w niej identyfikator przestaje być oznaczeniem makrodefinicji.

Przykład. Rozwinięciem tekstu źródłowego

main(){

int var =3;

#define var 5

printf("%d %d",var

#undef var

,var);

}

jest program, którego wykonanie powoduje wyprowadzenie liczb 5 i 3.


12.2. Włączanie zawartości plików

Wykonanie dyrektywy preprocesora o postaci

#include "nazwa-pliku"

powoduje zastąpienie jej całą zawartością pliku o podanej nazwie. Plik taki jest w pierwszej kolejności poszukiwany w tym katalogu, z którego pochodzi pierwotny tekst źródłowy, a jeśli go tam nie ma, to jest poszukiwany w ustalonym zbiorze miejsc standardowych. Jeśli poszukiwanie pliku ma ograniczyć się do miejsc stan­dardowych, to omawianą dyrektywę należy zastąpić dyrektywą

#include <nazwa-pliku>

Przykład. Jeśli tekst źródłowy programu ma postać

#include <stdio.h>

main(){

printf("%s","janb");

}

to program skierowany do kompilacji składa się z tekstu znajdującego się w pliku stdio.h oraz z tekstu stanowiącego definicję funkcji main.

Nic nie stoi na przeszkodzie, aby tekst zastępujący dyrektywę

#include także zawierał dyrektywy #include. Jedynym wymaganiem jest, aby ciąg takich włączań prowadził do wyeliminowania dyrektyw #include.


12.3. Kompilacja warunkowa

Dyrektywy kompilacji warunkowej są dyrektywami strukturalnymi składającymi się z poddyrektyw nagłówka, alternatywy i zakończenia. W ogólnym przypadku dyrek­tywy takie występują w kontekście

poddyrektywa-nagłówka

tekst-źródłowy-1

poddyrektywa-alternatywy

tekst-źródlowy-2

poddyrektywa-zakończenia

przy czym poddyrektywa-alternatywy wraz z następującym po niej tekstem źródłowym może być opuszczona.

Składnia

poddyrektywa-naglówka:

#ifdef identyfikator

#ifndef identyfikator

#if wyrażenie

poddyrektywa-alternatywy:

#else

poddyrektywa-zakończenia:

#endif

Poddyrektywy ifdef i ifndef służą odpowiednio do sprawdzenia, czy wymieniony w nich identyfikator jest, czy nie jest nazwą makrodefinicji.

Poddyrektywa if służy do sprawdzenia, czy występujące w niej wyrażenie ma war­tość różną od zera.

Jeżeli rezultatem sprawdzenia jest dana o wartości logicznej prawda, to cała dyrek­tywa kompilacji warunkowej zostaje zastąpiona tekstem-źródłowym-1, a w przeciw­nym razie tekstem-źródlowym-2 (albo tekstem pustym, jeśli nie użyto alternatywy).

Należy nadmienić, że nic nie stoi na przeszkodzie, aby teksty źródłowe zawarte w dyrektywie kompilacji warunkowej także zawierały dyrektywy kompilacji wa­runkowej.

Przykład. W zasięgu makrodefinicji

#define c -1

#define testA

tekst źródłowy

#if c+2

#ifdef testA

if(a)

#else

if(b) m = 5;

else

#endlf

m = 8;

#endlf

zostanie przekształcony w tekst

if(a)

m=8;


12.4. Dyrektywa line

Dyrektywa line ma postać

#line liczba identyfikator

Dyrektywa ta oznacza, że następujący po niej wiersz programu źródłowego znajdo­wał się pierwotnie w pliku określonym przez identyfikator oraz że był w nim wier­szem o numerze liczba.

Jeśli w dyrektywie #line zostanie opuszczony identyfikator, to zostanie domniemany identyfikator z poprzedniej takiej dyrektywy.

Mimo iż dyrektywa #line należy do repertuaru dyrektyw preprocesora, może wystę­pować także w tekście generowanym przez preprocesor. Sposób traktowania tej dyrektywy istotnie zależy od implementacji preprocesora i kompilatora.


19. Wybrane operacje wejścia/wyjścia

Środki umożliwiające wykonywanie operacji wejścia/wyjścia, a więc nawet proste makrodefinicje i funkcje znajdują się poza definicją języka C. W większości imple­mentacji są dostępne jednak pewne funkcje podstawowe, jak np. printf i scanf ułatwiające komunikowanie się z otoczeniem. Inne funkcje do realizowania operacji wejścia/wyjścia pozostają w ścisłym związku z użytym systemem operacyjnym i pod­legają jego wymaganiom i ograniczeniom.


Funkcja printf

Funkcja ta służy do wyprowadzania wartości zmiennych w postaci znakowej. Wy­wołanie funkcji ma postać

printf(arg0,arg1 ,arg2. ..,argn)

w której arg0 jest argumentem sterującym, określającym m.in. liczbę następujących po nim argumentów argn - których wartości podlegają wyprowadzaniu.

Argument sterujący reprezentuje ciąg znaków składający się z dowolnych napisów oraz z wzorców konwersji. Wzorce rozpoczynają się od znaku %, a kończą się jedną z liter

d o x u c s e f g

z których każda określa sposób przekształcenia kolejnego argumentu. Jeśli znak występujący bezpośrednio po znaku % nie jest jedną z wymienionych liter, ani też minusem, cyfrą lub kropką, to podlega wyprowadzaniu.

Znaczenie poszczególnych znaków występujących we wzorcach konwersji jest na­stępujące

% rozpoczyna wzorzec konwersji,

- oznacza, że wyprowadzana dana ma być wyrównana w polu wyjściowym le­wostronnie,

. oddziela liczby występujące we wzorcu,

d powoduje wyprowadzenie argumentu w postaci liczby dziesiętnej,

o powoduje wyprowadzenie argumentu w postaci liczby ósemkowej (bez wio­dącego zera),

x powoduje wyprowadzenie argumentu w postaci liczby szesnastkowej (bez wiodących znaków Ox),

u powoduje wyprowadzenie argumentu w postaci liczby typu (unsigned int),

c powoduje wyprowadzenie pojedynczego znaku stanowiącego argument,

s powoduje wyprowadzenie ciągu znaków stanowiącego argument,

e powoduje wyprowadzenie argumentu jako liczby zmiennopozycyjnej o po­staci zm.nnnnnnEwyy, gdzie z jest znakiem liczby, w jest znakiem wykład­nika, a m.nnnnnn jest znormalizowaną mantysą (liczba cyfr n może być usta­lona dowolnie),

f powoduje wyprowadzenie argumentu jako liczby stałopozycyjnej o postaci zmmm.nnnnn (liczba cyfr n może być ustalona dowolnie),

g powoduje wyprowadzenie argumentu w postaci stało- albo zmiennopozycyj­nej, stosownie do tego, która postać zajmuje mniej znaków.

We wzorcu konwersji mogą wystąpić liczby. Pierwsza z nich określa szerokość po­la zewnętrznego, a druga - liczbę cyfr wyprowadzanych po kropce dla wzor­ców e, f, g albo liczbę wyprowadzanych znaków dla wzorca s. Jeśli szerokość pola jest wyrażona liczbą rozpoczynającą się od cyfry zero, to podczas wyprowadzania liczb wyrównanych w polu wyjściowym prawostronnie - zostanie zastosowane le­wostronne dopełnienie nie spacjami, lecz zerami.

Przykład

Argument Wzorzec Pole zewnętrzne

2+3 %3d __5

12 %3o _14

12 %1x c

2-3 %5u 65535

‘A’+1 %c B

”ewa” %s ewa

”ewa” %-5s ewa__

”ewa” %5s __ewa

”ewa” %3.1s __e

2-3.0e1 %-12.2e -2.80E+01___

2-3.0e1 %10.4f __-28.0000


Przykłady

a. Wyprowadzanie tekstów

printf("to be or not to be\n");


  1. Wyprowadzanie danych liczbowych

printf("fixed(a)=%7.2f",a);

- wyprowadzane są znaki tworzące tekst "fixed(a)=", a po nich wartość zmien­nej a w postaci zmmm.nn,

- jeśli a ma np. wartość -27.5, to zostanie wyprowadzony napis

fixed(a)= -27.50

c. Wyrażenia w argumencie arg,

printf(a>100.0 ? "a=%12.3e" : "a=%8.3f",a);

- wybór wzorca zależy od wartości zmiennej a.


Funkcja scanf

Funkcja ta służy do wprowadzania ciągów znaków i interpretowania ich jako za­pisu wartości, które są nadawane zmiennym wskazywanym przez argumenty funkcji. Wywołanie funkcji ma postać

scanf(aarg0,arg1,arg2...,argn)

w której arg0, jest argumentem sterującym, podobnie jak dla funkcji printf a argu­menty argn są l-wyraźeniami reprezentującymi zmienne, którym mają być nadane wartości.

Argument sterujący reprezentuje ciąg znaków składający się z odstępów (spacji, znaków tabulacji i znaków nowej linii) oraz ze znaków nie zawartych we wzorcach konwersji, którym powinny odpowiadać identyczne znaki w polu wejściowym.

Wzorce konwersji składają się ze znaku %, po którym następuje liczba określająca maksymalną szerokość pola zewnętrznego i litera określająca sposób interpretowa­nia tego pola. Dopuszczalnymi literami są

d o x h c s f

a ich znaczenie jest następujące

d w polu zewnętrznym jest spodziewana liczba dziesiętna,

o w polu zewnętrznym jest spodziewana liczba ósemkowa,

x w polu zewnętrznym jest spodziewana liczba szesnastkowa (bez wiodących znaków Ox),

h w polu zewnętrznym jest spodziewana liczba dziesiętna (krótka),

c w polu zewnętrznym jest spodziewany znak (w danym przypadku odstępy nie są ignorowane),

s w polu zewnętrznym jest spodziewany ciąg znaków (podczas nadawania war­tości zmiennej ciąg ten jest uzupełniany znakiem nul),

f w polu zewnętrznym jest spodziewana liczba stało- albo zmiennopozycyjna.

Jeśli bezpośrednio po znaku % występuje znak * to jego zinterpretowanie powoduje ominięcie pola zewnętrznego o strukturze określonej przez literę kończącą wzo­rzec konwersji.


Przykład

int var_i,var;

float var_f;

char chr, arr[3];

scanf("%2d %5f %*2d %c %2s",&var_i ,&var_f ,&var ,&chr ,arr);

- dla pola zewnętrznego 1234 56789

zmienna var_i otrzyma wartość 12, zmienna var_f otrzyma wartość 3.4e1 (pole ma maksymalną długość 5 znaków, ale kończy je spacja), wartość zmiennej var nie ulegnie zmianie, zmienna chr otrzyma wartość '7', a tablica arr zostanie wypeł­niona znakami '8', '9' i '\0',

- wszystkie argumenty funkcji scanf są typu wskazującego.

Ponieważ definicja języka C nie obejmuje zasad programowania operacji wej­ścia/wyjścia, faktyczny zestaw środków umożliwiających komunikowanie się pro­gramu z jego środowiskiem zależy od implementacji. Domniemywa się jednak, że w każdej implementacji będzie dostępny ograniczony zestaw funkcji standardowych (albo makrodefinicji o zewnętrznych cechach takich funkcji), który umożliwi prze­noszenie programów między systemami o odmiennych cechach funkcjonalnych.

Do zestawu takiego będą z pewnością należeć opisane w pracy makrodefinicje getchar i putchar oraz funkcje printf i scanf. Operacje takie zostaną włączone do biblioteki makrodefinicji lub do biblioteki funkcji standardowych, a zaprogramo­wane będą na podstawie funkcji podstawowych udostępnianych przez system ope­racyjny.


Wyszukiwarka

Podobne podstrony:
Bielecki - Visual C++, Visual C++ 6.0 Programowanie obiektowe, Jan Bielecki
Bielecki Visual C 6.0 (Podstawy programowania), Jan Bielecki
Visual C 6.0 Programowanie obiektowe, Jan Bielecki
Słowacki Jan Bielecki [WolneLek]
Jan Bielecki, Java od Podstaw
Visual C 6 0 Podstawy programowania Jan Bielecki(1)
Jan Bielecki Visual C 6 podstawy programowania
jan bielecki
Jan Bielecki Visual C 6 0 Podstawy programowania
jan bielecki
Juliusz Słowacki Jan Bielecki
Słowacki Juliusz, Jan Bielecki
Juliusz Słowacki Poematy (Beniowski, Podróż do , Anhelli, Jan Bielecki, Ojciec zadżumionych, Król
jan bielecki
Z cyklu „Od Mazowieckiego do Tuska” (7) – Jan Krzysztof Bielecki
Klemens Aleksandryjski Łukasiewicz Jan Paweł II Galileusz, Darwin, Dawkins Separacja doświadczenia,
Jan Krzysztof Bielecki
Język jako narzędzie paradoksy
Język w zachowaniach społecznych, Wykład na I roku Kulturoznawstwa (1)

więcej podobnych podstron