• Nie Znaleziono Wyników

Nie będzie tu pełnej składni języka C; osoby zainteresowane znajdą ją w książce Kerninghana i Ritchiego, natomiast na wykładzie przyjrzymy się sposobowi jej zapisywania. W zapisie występują symbole leksykalne, które pojawiają się w tekście programu, oraz symbole odpowiadające abstrakcyjnym pojęciom (analogicznym do pojęć zdanie, podmiot itd. w języku polskim), które

„wyprowadzając” tekst programu w C należy zastąpić przez inne symbole (w końcowym efekcie tekst będzie się składał tylko z symboli leksykalnych).

Punktem wyjścia jest jednostka-tłumaczenia. Mamy dla niej produkcje

jednostka-tłumaczenia:

deklaracja-zewnętrzna

jednostka-tłumaczenia deklaracja-zewnętrzna

Odczytujemy to tak: symbol jednostka-tłumaczenia może być zastąpiony przez ciąg symboli podany w jednej z występujących poniżej linii. Może to więc być jedna deklaracja-zewnętrzna lub też jednostka-tłumaczenia, po której występuje deklaracja-zewnętrzna. Zauważmy, że druga z podanych produkcji jest formułą rekurencyjną. Z obu produkcji wynika, że poprawne zdanie w języku C składa się z jednej lub większej liczby fragmentów, z których każdy jest jedną

deklaracją-zewnętrzną.

Symboli takich jak jednostka-tłumaczenia, czy deklaracja-zewnętrzna jest w sumie 65, dla każdego z nich jest od jednej do kilkunastu produkcji, tj.

alternatywnych sposobów zastępowania tego symbolu. Przedstawione niżej są tylko niektóre produkcje, przy czym część z nich jest uproszczona, bo nie chodzi tu o szczegóły, tylko o zasadę definiowania składni. Każdy symbol ma określone znaczenie, które opisuje się słowami.

deklaracja-zewnętrzna:

definicja-funkcji deklaracja

Deklaracja-zewnętrznajest to podprogram lub deklaracja np. zmiennych nie umieszczona wewnątrz podprogramu.

Symbol definicja-funkcji oznacza podprogram, którego budowę określają dalsze reguły gramatyczne. Symbol deklaracja może oznaczać m.in. deklarację zmiennych. Zatem, program w C może składać się z podprogramów i deklaracji

3.5

zmiennych, występujących w kolejności, która z punktu widzenia składni może być dowolna, ale ponieważ znaczenie każdego identyfikatora powinno być określone w miejscu jego użycia, więc np. deklaracje zmiennych napiszemy przed wyrażeniami, w których zmienne te występują. W ten sposób kolejność

deklaracji-zewnętrznychjest podporządkowana znaczeniu (semantyce) poszczególnych części programu.

definicja-funkcji:

specyfikatory-deklaracjiopc deklarator instrukcja-złożona ...

Indeksopcoznacza, że dany symbol można pominąć (co skraca zapis gramatyki), natomiast ... oznacza inne możliwości (tj. produkcje), które pominąłem. Dalej mamy produkcje

specyfikatory-deklaracji:

specyfikator-typu ...

specyfikator-typu: jeden z

void char short int long float double signed unsigned ...

Specyfikator-typuokreśla typ wyniku zwracanego jako wartość podprogramu. Ma on zasadnicze znaczenie podczas obliczania wartości wyrażenia, w którym

występuje wywołanie podprogramu. Specyfikator-typu może w definicji funkcji się nie pojawić — kompilator przyjmuje wtedy, że jest to int.

deklarator:

bezpośredni-deklarator ...

bezpośredni-deklarator:

identyfikator ( lista-typów-parametrów ) ...

Z podanych wyżej produkcji wynika możliwość zapisania nagłówka podprogramu złożonego ze specyfikatora-typu i deklaratora, który składa się z identyfikatora i listy parametrów (w nawiasach) — w programie omawianym na poprzednim wykładzie mamy tego przykłady. Aby nie obciążać zanadto pamięci, zobaczmy jeszcze tylko składnię instrukcji.

3.6

instrukcja:

instrukcja-etykietowana instrukcja-wyrażeniowa instrukcja-złożona instrukcja-wyboru instrukcja-powtarzania instrukcja-skoku instrukcja-wyrażeniowa:

wyrażenieopc ;

Wykonanie instrukcji wyrażeniowej polega na obliczeniu wartości wyrażenia (i odrzuceniu jej). Jego skutkiem są wszelkie efekty uboczne tego wyrażenia, które na przykład może być wywołaniem podprogramu. Wyrażenia i ich efekty uboczne omówimy za chwilę. Jeśli wyrażenie jest nieobecne (jest tylko średnik), to instrukcja wyrażeniowa jest instrukcją pustą — nic nie robi, ale jest w języku programowania tak potrzebna jak 0 w zbiorze liczb całkowitych.

instrukcja-skoku:

goto identyfikator ; continue ;

break ;

return wyrażenieopc ;

Instrukcje skoku służą do wskazania następnej instrukcji, która ma być wykonana, zwykle innej niż instrukcja następna w tekście programu.

Instrukcja powrotu (return) powoduje zakończenie wykonywania podprogramu i przekazanie sterowania do instrukcji, która ten podprogram wywołała.

Wyrażenie w instrukcji powrotu jest obliczane i obliczona wartość jest wartością podprogramu — wyrażenie musi być odpowiedniego typu. Instrukcja powrotu bez wyrażenia jest właściwa dla podprogramu typu void. Instrukcja powrotu bez wyrażenia jest też umieszczana automatyczne przez kompilator na końcu każdego podprogramu (więc nie musimy jej pisać), ale wartość podprogramu typu innego niż void dla instrukcji powrotu bez wyrażenia jest nieokreślona (kompilator w takich przypadkach zwykle wypisuje ostrzeżenia).

Instrukcja przerwania (break) może wystąpić w dowolnej pętli (while, do ... whilelub for) lub w instrukcji przełącznika (switch). Skutkiem jej wykonania jest zakończenie wykonywania tej pętli lub przełącznika i rozpoczęcie

3.7

wykonywania następnej instrukcji. Istotne jest, że instrukcja przerwania kończy działanie tylko jednej pętli lub przełącznika, tej, która ją bezpośrednio otacza.

W razie konieczności zakończenia działania pętli zagnieżdżonych trzeba użyć instrukcji goto albo (najlepiej) return.

Instrukcja kontynuacji (continue) może wystąpić w pętli; jej skutkiem jest pominięcie dalszych instrukcji wewnątrz pętli i, jeśli warunek wykonywania następnego przebiegu pętli jest spełniony, rozpoczęcie jego wykonywania. Jeśli warunek ten nie jest spełniony, wykonywana jest pierwsza instrukcja za pętlą.

Instrukcja skoku (goto) umożliwia przekazanie sterowania do dowolnej wskazanej instrukcji (w tym samym podprogramie), którą należy opatrzyć identyfikatorem (tzw. etykietą); w instrukcji skoku podaje się ten identyfikator. Za pomocą instrukcji skoku łatwo jest uczynić program całkowicie nieczytelnym i dlatego najlepiej jest unikać używania tej instrukcji w ogóle. Instrukcja ta przydaje się w razie konieczności zakończenia wykonywania zagnieżdżonej pętli, lub obsługi sytuacji wyjątkowych. Zawsze skoki powinny być „w przód”, tj. do instrukcji występujących dalej w tekście programu. Ponadto nigdy nie należy wskakiwać do środka pętli lub innej instrukcji strukturalnej.

instrukcja-etykietowana:

identyfikator : instrukcja

case wyrażenie-stałe : instrukcja default : instrukcja

Instrukcja może być opatrzona etykietą, i jeśli jest, to jej wykonanie nie musi następować po instrukcji poprzedzającej w tekście. Identyfikator i dwukropek przed instrukcją oznaczają, że do tej instrukcji może nastąpić skok spowodowany przez instrukcję goto. Pozostałe postaci etykiet występują w instrukcji

przełącznika i omówimy je dalej.

Instrukcje wyrażeniowe i skoku są tak zwanymi instrukcjami prostymi,

w odróżnieniu od instrukcji strukturalnych, których częściami składowymi mogą być inne instrukcje.

instrukcja-złożona:

{ lista-deklaracjiopc lista-instrukcjiopc }

lista-instrukcji:

instrukcja

lista-instrukcji instrukcja

3.8

Instrukcja-złożonajest ciągiem instrukcji ujętym w klamry; jeśli nie występują skoki, to instrukcje te są wykonywane kolejno. Na początku instrukcji złożonej można zadeklarować zmienne, które zostaną utworzone na początku wykonywania tej instrukcji i zlikwidowane na zakończenie.

instrukcja-wyboru:

if ( wyrażenie ) instrukcja

if ( wyrażenie ) instrukcja else instrukcja switch ( wyrażenie ) instrukcja

Instrukcje warunkowe if (...) ... oraz if (...) ... else ... zawierają warunek (wyrażenie), na podstawie którego instrukcja opatrzona warunkiem jest wykonywana lub pomijana, albo następuje wykonanie jednej z dwóch instrukcji.

Warunek jest uważany za spełniony wtedy i tylko wtedy, gdy wartość wyrażenia jest różna od 0.

Instrukcja przełącznika (switch) służy do zareagowania na jedną z wielu możliwości. Na przykład, mając zmienną kolor, przyjmującą jedną z wartości czerwony, zielony, niebieski lub inną, możemy napisać

switch ( kolor ) {

case czerwony: printf ( "czerwony\n" ); break;

case zielony: printf ( "zielony\n" ); break;

case niebieski: printf ( "niebieski\n" ); break;

default: printf ( "inny\n" );

}

Ten sam efekt moglibyśmy uzyskać, pisząc

if ( kolor == czerwony ) printf ( "czerwony\n" );

else if ( kolor == zielony ) printf ( "zielony\n" );

else if ( kolor == niebieski ) printf ( "niebieski\n" );

else printf ( "inny\n" );

ale w ten sposób wymuszamy określoną kolejność sprawdzania warunków.

Zwróćmy uwagę na instrukcje przerwania (break;) w instrukcji przełącznika.

Gdyby ich nie było, to dla zmiennej kolor o wartości czerwony program wypisałby cztery linie tekstu informujące o wszystkich kolorach. Instrukcja z etykietą default jest wykonywana wtedy, gdy żadna możliwość opisana przez

3.9

inne etykiety nie ma miejsca. Etykiety default nie musi być w instrukcji przełącznika i wtedy jeśli wartość zmiennej nie odpowiada żadnej innej etykiecie, to instrukcja przełącznika kończy działanie. Etykieta default nie musi być ostatnia w przełączniku. Oczywiście każda etykieta w przełączniku może wystąpić tylko raz.

instrukcja-powtarzania:

while ( wyrażenie ) instrukcja do instrukcja while ( wyrażenie ) ;

for ( wyrażenieopc ; wyrażenieopc ; wyrażenieopc ) instrukcja

Instrukcje powtarzania, czyli pętle, służą do wielokrotnego wykonywania tych samych czynności. Ich przykłady mieliśmy na pierwszym wykładzie. W tym miejscu wspomnimy o sposobach przerywania pętli: może ono nastąpić wskutek niespełnienia warunku (odpowiednie wyrażenie ma wartość 0), lub wskutek wykonania skoku (break, goto lub return).

Zagnieżdżanie instrukcji warunkowej wiąże się z pewną łatwą do uniknięcia pułapką, wynikającą z pewnej niejednoznaczności składni. Rozważmy dwa fragmenty programu:

if ( warunek1 )

if ( warunek2 ) instrukcja1 else instrukcja2

if ( warunek1 ) while ( warunek2 )

if ( warunek3 ) instrukcja1

else instrukcja2

W pierwszym przypadku zależy nam na tym, aby jeśli warunek1jest niespełniony, wykonać instrukcję2, a w przeciwnym razie zbadać warunek2i ewentualnie wykonać instrukcję1. W drugim przypadku, jeśli warunek1jest spełniony, wykonujemy pętlę, w której instrukcja1jest wykonywana lub nie, zależnie od warunku3. Jeśli warunek1jest niespełniony, to chcemy jednorazowo wykonać instrukcję2. Tymczasem w obu przypadkach kompilator uzna tekst else instrukcja2za część drugiej (wewnętrznej) instrukcji warunkowej. Aby program działał zgodnie z opisaną wyżej intencją, trzeba go inaczej zapisać. Mamy dwa wyjścia. W pierwszym możemy do wewnętrznej instrukcji warunkowej dołączyć else;(czyli użyć instrukcji pustej). Drugi sposób, chyba lepszy, to umieścić

3.10

wewnętrzne instrukcje w instrukcji złożonej:

if ( warunek1 ) {

if ( warunek2 ) instrukcja1

}

else instrukcja2

if ( warunek1 ) { while ( warunek2 )

if ( warunek3 ) instrukcja1

}

else instrukcja2