I. Metody i techniki programowania komputerów
1. Wstęp
Celem zajęć laboratoryjnych z przedmiotu „Informatyka” jest przekazanie
przyszłemu inżynierowi umiejętności korzystania w jego pracy z możliwości
programowania komputerów i tworzenia własnych procedur obliczeniowych.
Ucząc się programowania należy opanować zarówno język programowania (np.
język C) jak i zapoznać się z logiką programowania z użyciem komputera.
Poznanie języka programowania (lub nawet kilku) nie wystarczają do
samodzielnego opracowania programu, jeżeli nie opanowało się odpowiednich
metod i technik programowania.
Programowanie jest procesem, w ramach którego muszą być zrealizowane
następujące czynności:
a) analiza otrzymanego przez programistę zadania,
b) poszukiwanie i wybór odpowiedniej metody programowania,
c) opracowanie algorytmu opisującego sposób wykonania przez komputer
zleconej pracy,
d) zakodowanie algorytmu zapewniające jego realizację przez komputer, tzn.
zakodowanie go w wybranym języku programowania i uzyskanie w ten
sposób tzw. programu źródłowego,
e) przetłumaczenie programu źródłowego na wewnętrzny język komputera,
a tym samym otrzymanie programu roboczego,
f) sprawdzenie poprawności programu i poprawienie wykrytych błędów,
g) opracowanie opisu (tzw. dokumentacji) programu.
Od programisty wymagana jest więc nie tylko wiedza w zakresie zapisywania
we właściwy sposób poleceń dla komputera, lecz także umiejętność
konstruowania algorytmów.
2. Analiza zadania programistycznego
Wstępna analiza zadania programistycznego powinna doprowadzić do
wypracowania odpowiedzi na następujące pytania:
a) jakie dane wejściowe podlegają opracowaniu: jaka jest ich struktura logiczna
i fizyczna, na jakim nośniku są zapisane, jaki jest ich rodzaj i ilość ? Czy
mogą wystąpić błędy zakłócające proces przetwarzania danych i czy w
związku z tym trzeba podjąć odpowiednie kroki w celu wykrycia takich
błędów? Jakie wartości ekstremalne mogą przyjmować dane wejściowe ?
b) jakie wyniki, w jakiej postaci i ilości oraz na jakim nośniku musimy
dostarczyć odbiorcy ? Jaka powinna być dokładność tych wyników?
c) Jakie ograniczenia i uwarunkowania musimy uwzględnić podczas
programowania ? Które z nich trzeba traktować jako warunki konieczne, a
które – jako dodatkowe, pożądane ?
d) Czy może być potrzebna jakaś pomoc ? Czyja i w jakim zakresie ?
Ad. a)
Najczęściej
początkujący
programiści
zakładają,
ż
e
podczas
przetwarzania maszynie zostają dostarczone wyłącznie dane poprawne,
spełniające wszystkie stawiane w tym zakresie warunki i wymagania. W
konsekwencji takiego założenia brak jest w większości programów rozwiązań,
które zapobiegną uzyskaniu błędnych wyników w wypadku odczytania błędnej
danej. Należy więc przyjmować zasadę ograniczonego zaufania do ich
poprawności, a zatem przewidywać konieczność sprawdzania danych
wejściowych ze względu na te operacje, które w szczególny sposób mogą
zaważyć na działaniu programu, gdyby w owych danych znalazł się błąd nie
wykryty wcześniej.
Nieuwzględnienie czynnika ilości danych wejściowych i wyników
przetwarzania może spowodować, że zostanie opracowany program, którego
wykorzystanie w praktyce jest skomplikowane lub wręcz niemożliwe z
powodów technicznych. Jeżeli przetwarzaniu ma ulec duża ilość danych
wejściowych, to proces ten może trwać bardzo długo i należy liczyć się z
możliwością awarii sprzętu lub błędu człowieka, co zazwyczaj prowadzi do
konieczności powtórzenia całej pracy. W celu wyeliminowania tego zagrożenia
powinno
się
przewidzieć
w
programie
odpowiednie
rozwiązania
zabezpieczające.
Poznanie danych wejściowych powinno polegać na ustaleniu:
- treści (zawartości) informacyjnej tych danych,
- źródeł ich pochodzenia,
- procesu, w wyniku którego zostały one przygotowane do przetwarzania,
- ich jakości (czy były podejmowane działania w celu wykrycia w nich
ewentualnych błędów i jaki był zakres takiej kontroli),
- rozmiarów zbiorów danych i nośnika na którym zostały zapisane,
- struktury zbiorów danych (typów tzw. rekordów, treści danych zawartych w
każdym takim typie, budowy każdego typu rekordu, układu rekordów w
poszczególnych zbiorach).
Ad b)
Zbiory danych wyjściowych muszą być zebrane i zapisane wg. kryteriów
ustalonych w podobny sposób jak dane wejściowe. Trzeba jednak zauważyć, że
w tym wypadku programista ma zwykle większą swobodę w ustalaniu struktury,
treści i sposobu zapisu tych zbiorów, aniżeli w odniesieniu do danych
wejściowych. Przykładowo, jeżeli użytkownik życzy sobie, by wyniki były
drukowane na papierze to wymaganie to powinno być spełnione, a margines
swobody jaki pozostaje programiście może dotyczyć np. takiego sposobu
wydruku, aby zapisy były jak najbardziej czytelne.
Ad c)
Jednym z ważnych warunków ograniczających, jaki programista musi
uwzględnić, jest czas, w którym program ma być opracowany. Na przykład,
poprzez wybór określonej metody programowania lub określonego języka
programowania można uzyskać rozwiązanie, które w końcowym efekcie okaże
się korzystne ze względu na stosowany sprzęt i czas pracy. Jeżeli jednak terminy
są krótkie, to programista musi skupić całą uwagę na uzyskaniu rozwiązania w
czasie satysfakcjonującym użytkownika, nawet kosztem doskonałości programu.
Innym ograniczeniem mogą być żądania odnośnie zajmowanego przez program
obszaru pamięci operacyjnej i innych urządzeń zewnętrznych, czasu realizacji
programu przez komputer, możliwości interwencji ze strony człowieka w
przebieg pracy komputera przy wykonywaniu programu.
3. Elementarne wiadomości o algorytmach
Algorytm jest to termin określający pewien proces obliczeniowy jako zespół
precyzyjnych i jednoznacznych reguł działania. W informatyce algorytmem
nazywa się przepis określający skokowy proces przekształcania danych
wejściowych w wymagane wyniki. Algorytm musi spełniać następujące
warunki:
a) warunek jednoznaczności, tzn. musi jednoznacznie określać rodzaj i
kolejność operacji, jakie mają być realizowane przez komputer,
b) warunek masowości, tzn. musi charakteryzować się dużym stopniem
uogólnienia, aby umożliwiał rozwiązywanie różnych wariantów danego
zadania,
c) warunek skuteczności, tzn. algorytm musi prowadzić do uzyskania
rozwiązania zadania w skończonym czasie.
W strukturze algorytmu zawsze występują dwa typy jego elementów
konstrukcyjnych: operacje oraz dane, na których te operacje są wykonywane.
Dane można podzielić na dwa rodzaje:
a) dane stałe, tzn. liczby, symbole, teksty podawane w algorytmie w postaci
jawnej,
b) dane zmienne, tzn. takie wartości, które nie są znane lub nie mogą być
skonkretyzowane podczas opracowywania algorytmu.
Dane zmienne w algorytmie oznacza się pewnymi symbolami literowo –
cyfrowymi. Oznaczenia takie nazywa się nazwami zmiennych. Operacje, z
których budowany jest algorytm powinny mieć charakter czynności
elementarnych wykonywanych przez komputer. Przed konstruowaniem
algorytmu należy zdefiniować pewien zbiór operacji elementarnych, z których
można skorzystać. Zbiór taki może być określony z zewnątrz np. w postaci listy
rozkazów dostępnych w wybranym języku programowania.
Opis każdej operacji elementarnej złożony jest z pewnego kodu i operandu. Kod
służy do zidentyfikowania operacji, a także może być wykorzystany jako jej
oznaczenie, symbol. Operand operacji określa dane, które biorą udział w
przetwarzaniu oznaczonym przez funkcje rozpatrywanej operacji, lub inne
wielkości, które muszą wystąpić, by operacja mogła być poprawnie wykonana.
W związku z tym, poznanie każdej operacji polega na ustaleniu funkcji, jakie
ona wywołuje, oraz danych, które przetwarza, i wyników, do których prowadzi.
4. Proces budowania algorytmu
Praca nad zbudowaniem algorytmu dla danego zadania może być rozłożona
na pewne etapy. W pierwszym etapie można szkicować całe rozwiązanie w
ogólnym zarysie posługując się przy tym własną wiedzą i umiejętnościami, w
ten sposób, jak przygotowuje się instrukcję pracy dla człowieka. Następnie po
upewnieniu się, że opracowana wersja algorytmu jest merytorycznie poprawna,
modyfikuje się ją tak, aby wyeliminować operacje, których komputer nie może
wykonać, zastępując je przez operacje elementarne, np. dostępne w zbiorze
instrukcji danego języka programowania. Przekształcenie wstępnej wersji
algorytmu wiąże się z koniecznością uzupełnienia go o pewne operacje
dodatkowe, związane z tym, ostatecznie będzie on realizowany przez komputer.
Do operacji tych zaliczyć należy:
a) odczytywanie danych wejściowych z nośników technicznych,
b) wyprowadzania wyników przetwarzania na urządzenia zewnętrzne,
c) przerwania pracy komputera po zakończeniu rozwiązywania zadania,
d) przygotowania niezbędnych wartości początkowych zmiennych biorących
udział w tych operacjach arytmetycznych, których wynik zależy od tego, jaką
wartość te zmienne miały przed rozpoczęciem a wykonywania
5. Prezentacja algorytmów, schematy blokowe
W celu przejrzystego przedstawienia algorytmu zamiast słownych opisów
stosowane są rysunki, szkice oraz tzw. schematy blokowe. Schemat blokowy to
układ płaskich figur geometrycznych połączonych z sobą odcinkami prostych,
łamanymi lub krzywymi. Występujące w nim figury geometryczne noszą nazwę
skrzynek, natomiast łączące je linie nazywa się ścieżkami sterującymi. Skrzynki
służą do przedstawienia działań zaprojektowanych w algorytmie, zaś linie
wskazują kolejność wykonywania tych działań.
Główną zaletą schematów blokowych jest to, że graficznie prezentują one
algorytm zarówno od strony występujących w nim działań, jak i ich kolejności.
Każda skrzynka w schemacie blokowym prezentuje określony rodzaj operacji.
Wykaz stosowanych w schematach blokowych symboli przedstawia tablica nr 1.
Tablica 1 .Wykaz symboli stosowanych w schematach blokowych
Symbol
Znaczenie symbolu
Operacje, w wyniku których ulega zmianie wartość
danych w pamięci komputera
Operacja wprowadzania danych do pamięci operacyjnej
komputera
Operacja wyprowadzania danych z pamięci operacyjnej
komputera
Operacja warunkowa dotycząca konieczności wyboru
jednej z dróg: związanej z spełnieniem (T) lub nie
spełnieniem (N) zapisanego warunku
moduł
cykliczny
(pętla),
w
którym
operacje
wykonywane są wielokrotnie, (np. cykl powtarza się,
dopóki zmienna „i” przyjmująca wartości od 1, z
krokiem 1, nie osiągnie wartości n)
Proces określony poza danym algorytmem (podprogram)
nie wymagający zdefiniowania w rozpatrywanym
schemacie blokowym
Określenie kierunku przepływu danych lub kolejności
wykonywania działań
Łączenie dróg przepływu danych lub kierunków
wykonywania działań, kiedy drogi są z sobą związane
logicznie
Łącznik stronicowy (etykieta), wskazujący wejście lub
wyjście z wyodrębnionych fragmentów schematu
znajdujących się na jednej stronie (arkuszu papieru)
Łącznik międzystronicowy, wskazujący wejście lub
wyjście z wyodrębnionych fragmentów schematu
rozmieszczonych na różnych stronach
Oznaczenie miejsca rozpoczęcia działania programu
Oznaczenie miejsca zakończenia lub przerwania
działania programu
Oznaczenie miejsca komentarza
II. Opis języka programowania C/C++
1. Historia powstania i rozwoju języka C.
Język C został opracowany w połowie lat siedemdziesiątych. Jego
autorami byli Brian. W. Kernighan i Dennis M.Ritchie z Bell Telephone Labs,
gdzie powstał również system operacyjny UNIX. Dzięki pełnemu, jasnemu
opisowi języka w książce „The C Programming Language” w krótkim czasie
opracowano kompilatory dla wielu różnych komputerów. Od tej pory nastąpiło
jednak wiele zmian w języku C, których nie obejmował standard stworzony
przez Kernighana i Ritchie. W trosce o to aby C nie utracił sowej
uniwersalności grupa producentów i projektantów oprogramowania zwróciła
się do instytutu ANSI (American National Standard Institute) z petycją o
ustanowienie standardu dla tego języka programowania. Amerykański
Narodowy Instytut Standaryzacji przyjął petycję i utworzył komitet techniczny
X3J11 Technical Committee w celu opracowania standardu. Pod koniec roku
1989 prace zostały zakończone i opracowano standard języka C. Został on
opublikowany pod tytułem „Draft Proposed American National Standard –
Programming Language C (1984r). Standard ten różnił się nieco od propozycji
autorów języka. Dla odróżnienia obu wersji przyjęto oznaczenie K&R C dla
wersji autorskiej i ANSI C dla standardu ANSI.
ANSI C podobnie jak pierwowzór, jest jednak językiem umożliwiającym
tylko programowanie proceduralne. Wraz z rozwojem obiektowych elementów
języka powstał standard AT&T Bell Laboratories, który obejmuje oba rodzaje
elementów języka. Na początku lat osiemdziesiątych Bjarne Stroustrup
opracował język programowania C++ w Laboratoriach Bella firmy AT&T.
Język ten powstał jako rozwinięcie języka programowania C , który
rozszerzono w trzech ważnych kierunkach:
1. stworzono narzędzia pozwalające definiować i stosować abstrakcyjne
typy danych,
2. stworzono narzędzia projektowania,
3. wprowadzono wiele subtelnych ulepszeń do istniejących konstrukcji
języka C,
Język C++ jest już szeroko dostępny i często służy do pisania rzeczywistych
programów użytkowych i rozbudowania systemów. W 1988 roku
wyprodukowano pierwsze oryginalne kompilatory dla rynku oprogramowania
dla komputerów PC i stacji roboczych. Ponadto zaczęły się pojawiać duże
biblioteki programów i powstawało środowisko programistyczne języka C++.
2. Ogólne zasady pisania i uruchamiania programów
Tworzenie programu w C można podzielić na 4 etapy:
a) Napisanie kodu źródłowego w dowolnym edytorze tekstu
Kod źródłowy programu to tekst stanowiący pewien ciąg instrukcji dla
komputera zapisanych zgodnie z pewnymi zasadami określonego języka
programowania. Rozróżniane są języki tzw. „wysokiego poziomu” (w tym
m.in. język C) oraz języki wewnętrzne tzw. języki „niskiego poziomu”. Aby
wyjaśnić te pojęcia należy najpierw zrozumieć zasadę działania
mikroprocesorów komputerów.
Mikroprocesor, czyli układ scalony sterujący wszystkim co zachodzi w
komputerze, z technicznego punktu widzenia może wykonywać tylko cztery
operacje. Może przenieść dane z jednego miejsca pamięci do drugiego,
zmienić dane w podanym miejscu pamięci, sprawdzić, czy wskazane miejsce
w pamięci zawiera wybrane dane oraz zmienić kolejność wykonywanych
czynności. Wszystkie te działania są wykonywane za pomocą wysyłania,
otrzymywania lub sprawdzania impulsów elektronicznych. Impulsy mogą
znajdować się tylko w jednym z dwóch stanów (określanych przez poziom
napięcia) : włączony lub wyłączony. Aby zapisać instrukcje komputerowe na
tym bardzo podstawowym poziomie, korzysta się z 0 jako przedstawiciela
stanu wyłączonego oraz z 1, która oznacza włączenie. Nazywa się cyframi
binarnymi (bitami) lub instrukcjami binarnymi, opartymi na systemie
dwójkowym, w którym do przedstawienia wszystkich liczb wykorzystuje się
kombinację tylko zer i jedynek. W miarę rozwoju komputerów opracowano
język asemblerowy korzystający z kodów mnemonicznych do przedstawiania
zadań odnoszących się bezpośrednio do mikroprocesora. Każdy kod (np.
MOV – przesuń) przedstawia osiem lub więcej instrukcji binarnych.
Assembler przekłada te kody na pojedyncze sygnały elektroniczne, a
ponieważ każdy kod odnosi się bezpośrednio do wewnętrznych funkcji
mikroprocesora, assembler nazywany jest językiem programowania niskiego
poziomu.
Obecnie korzysta się z języków wysokiego poziomu, którego instrukcjami są
słowa zrozumiałe dla ludzi, a nie proste słowa mnemotechniczne. Każde
słowo przedstawia zwykle kompletną operację, a nie jedno zadanie dla
mikroprocesora. Języki programowania wysokiego poziomu są łatwe w
obsłudze, czytelne i łatwe do przenoszenia pomiędzy komputerami różnych
typów.
Przed wykonaniem danej instrukcji języka wysokiego poziomu (np. języka
C) należy ją przetłumaczyć na ciąg instrukcji binarnych. Proces ten
nazywany jest kompilacją.
b) Kompilacja kodu źródłowego
Tekst programu zapisany w pliku źródłowym o nazwie zakończonej zwykle
rozszerzeniem .c zostaje przetłumaczony na format dwójkowy (binarny).
Czynność tą wykonuje kompilator, program komputerowy, który dodatkowo
sprawdza, czy tekst źródłowy programu zgadza się z regułami języka C. Jeśli
kompilator wykryje błędy w tekście źródłowym programu, sygnalizuje to
odpowiednimi komunikatami. Komunikaty te pozwalają odnaleźć miejsce
wystąpienia błędu i poprawić go powracając do edycji pliku źródłowego.
Po poprawnym skompilowaniu programu do postaci tzw. modułu
relokowalnego (pliku wynikowego) kompilator wywołuje program linkera w
celu stworzenia programu wykonywalnego. W systemie UNIX takie pliki
zazwyczaj ma rozszerzenie *.o, w systemie DOS natomiast * .OBJ.
c) Łączenie za pomocą programu „linker”
Plik binarny skompilowany jest już przetłumaczony na 0 i 1, ale jeszcze nie da
się go uruchomić, ponieważ brak mu niektórych funkcji i pewnych fragmentów
kodu. Program typu linker (konsolidator) jest programem łączącym wszystkie
moduły programu z modułami bibliotecznymi w jeden program wykonywalny.
Do skompilowanego pliku programu użytkownika dodany zostaje z bibliotek
binarny kod gotowych funkcji i całość jest zapisywana na dysku w nowym pliku
– programie gotowym do wykonania. W systemie DOS takie pliki mają
rozszerzenie *.EXE. Pliku wykonywalnego nie można przenosić pomiędzy
komputerami różnych typów i z różnymi systemami operacyjnymi, nawet jeśli
sam język ANSI C, w którym powstał kod źródłowy jest w pełni przenośny.
d) Uruchomienie programu.
Ostatni etap pisania programu polega na jego testowaniu i usuwaniu błędów.
Pomimo usuwania błędów formalnych w programie przez kompilator i
konsolidator, często zdarza się, że wyniki programu są inne niż oczekiwano.
Błędy wykonania występują wówczas, gdy program zawiera instrukcję, której
nie może wykonać. Błędy wykonania dotyczą zwykle plików lub urządzeń
zewnętrznych. Błędy logiczne występują wówczas, gdy program postępuje
zgodnie z instrukcjami, ale za to instrukcje są błędne, co oznacza, że dają błędne
wyniki. Są to problemy najtrudniejsze do wykrycia, ponieważ programista może
nawet nie wiedzieć o ich istnieniu. Aby wyszukać takie „ukryte” błędy można
wykorzystać tzw. debuggera (dosł. „odpluskwiacza”). Debugger pozwala
wykonywać program krokowo – wiersz po wierszu – a programista może
uważnie prześledzić, co się dzieje w programie, mogąc zatrzymać program w
każdej chwili.
Po usunięciu błędów wykonania lub błędów logicznych należy ponownie
program skompilować i skonsolidować.
3. Struktura programu
Program napisany w języku C składa się z definicji funkcji oraz z deklaracji
funkcji i zmiennych. Każdy program musi zawierać definicję funkcji o nazwie
main, a wykonanie programu rozpoczyna się od tej właśnie funkcji. Najkrótszy
program będzie miał zatem postać;
main()
{
}
W programie tym main jest nazwą funkcji, nawiasy krągłe obejmują pustą listę
parametrów funkcji, a para nawiasów klamrowych stanowiących ciało (treść)
funkcji nie zawiera ani jednej instrukcji. Wykonanie takiego programu
oczywiście nie wywołuje żadnych skutków. Nawiasy okrągłe występujące po
nazwie funkcji main są częścią tej nazwy i są niezbędne. Informują one
kompilator, że odnosimy się do funkcji a nie do jakiegoś słowa main. Nawiasy
klamrowe
{}
oznaczają początek i koniec bloku lub sekcji kodu programu.
Każda wpisywana funkcja musi zaczynać się i kończyć nawiasami klamrowymi.
Pomiędzy nimi wpisywane są instrukcje, które mają być wykonane przez
komputer w celu realizacji określonego zadania.
Przykładem programu, który wywoła już jakiś określony efekt jest ten, od
którego zaczynał naukę prawie każdy programista:
/* Moj pierwszy program */
#include <stdio.h>
main()
{
puts(”Witaj!”);
return 0;
}
W programie tym jak poprzednio występuję tylko jedna funkcja: main, ale
pojawiła się w niej instrukcja puts(”Witaj !”) oraz funkcja return. Zapis
instrukcji puts(”Witaj !”) powoduje wyświetlenie na ekranie monitora napisu
„Witaj !”. W instrukcji tej wykorzystujemy jedną z wielu napisanych i
skompilowanych funkcji znajdujących się w bibliotece funkcji dostarczanych
wraz z kompilatorem. Zakres pełnej biblioteki tych funkcji został określony
przez komitet Amerykańskiego Narodowego Instytutu Normalizacji (ANSI).
Stosując zapis puts(”Witaj !”) nakazujemy wykonanie instrukcji stanowiących
standardową funkcję puts(). Dla funkcji tej instrukcje nakazują umieszczenie na
ekranie określonego napisu. Treść napisu zostaje przekazana funkcji jako
parametr zapisany pomiędzy nawiasami okrągłymi. Znaki cudzysłowu
wskazują, że wyświetlony będzie łańcuch liter tworzących napis Witaj !, a nie
zmienna lub stała.
Funkcja return zapisana w ostatniej linii programu przed znakiem }
nakazuje komputerowi powrót do systemu operacyjnego. W przypadku
większości kompilatorów języka C polecenie to jest opcjonalne. Jeśli go nie
będzie nie pojawi się komunikat o błędzie.
Funkcje w języku C, w tym także funkcja main () mogą zwracać wartość
(wynik). Zapis return 0 oznacza, że program wykonał się prawidłowo i do
końca. Wartość różna od zera zwrócona przez program do systemu
operacyjnego oznacza, że wystąpiły błędy. Im większa zwrócona wartość, tym
poważniejszy błąd. W kompilatorach C spełniających standard ANSI trzeba
określić typ funkcji nawet jeśli funkcja nie oblicza wartości. W tym przypadku
deklaruje się typ void – czyli pusty, np.
void mojafunkcja ()
W ten sposób kompilator otrzymuje informację, że funkcja nie zwraca żadnej
wartości do wywołującej ją funkcji. Jeżeli funkcja ma zwrócić jakąś wartość,
trzeba wskazać typ zwracanej wartości. Robimy to określając typ przed nazwą
funkcji (deklarując typ funkcji), np.:
float oblicz() /*float – to liczba rzeczywista*/
Zwracana wartość jest argumentem funkcji return(). Zdefiniowane w języku C
typy wartości funkcji będą omówione w dalszej części skryptu.
Nie wyjaśniony został jeszcze zapis w pierwszej (po komentarzu) linii
programu. Wiersz ten rozpoczyna się od znaku #(hash) po którym następuje
słowo include (dołącz). Polecenie to jest dyrektywą nakazującą kompilatorowi
wykorzystanie informacji zawartych w pliku nagłówkowym o nazwie stdio.h.
Nazwa stdio jest skrótem od słów standard input/output (standardowe wejście-
wyjście), a plik stdio.h zawiera instrukcje potrzebne kompilatorowi do pracy z
plikami na dysku i do wysyłania informacji do drukarki.
Nazwa pliku stdio.h jest w przykładzie ujęta w nawiasy < >. Oznacza to,
ż
e pliku nagłówkowego należy poszukiwać w katalogu innym niż bieżący.
Kompilator w takim przypadku rozpocznie szukanie tego pliku od
standardowego katalogu o nazwie include. Jeżeli chcemy aby kompilator szukał
pliku nagłówkowego w katalogu bieżącym to należy nazwę pliku
nagłówkowego ująć w cudzysłów:
#include ”stdio.h”
Katalogi zawierające pliki nagłówkowe są instalowane standardowo podczas
instalacji kompilatorów C/C++ na dysku użytkownika. Przykładowo, jeśli
zainstalowany został kompilator w katalogu C:\BORLANDC, to pliki
nagłówkowe będą znajdować się w katalogu C:\BORLANDC\INCLUDE.
Zapisując nazwy plików, funkcji lub zmiennych należy zwracać uwagę na
stosowanie małych i dużych liter. Język C rozróżnia małe i duże litery, czyli
inaczej mówiąc jest wrażliwy na wielkość liter. Dla przykładu: stdio.h i
STDIO.H to dla C dwie różne nazwy. Podobnie main() i Main() to dwie różne
nazwy dwóch różnych funkcji.
Programy w języku C mają format swobodny, co oznacza, że nie jest
istotny sposób zapisu programu, a jedynie kolejność występowania
poszczególnych jego elementów. Przytoczony więc na początku rozdziału
program można zapisać następująco:
main () {}
Pisząc program ważne jest jednak, aby był on zapisany w sposób czytelny i
przejrzysty. Program napisany przejrzyście łatwiej jest przeczytać, zrozumieć i
poprawić. Wskazane jest uzupełnianie programu komentarzami. Komentarz w
języku C, to dowolnej treści tekst ograniczony znakami
/*
i
*/
. Przedstawiony
powyżej program z objaśnieniami w formie komentarzy mógłby wyglądać tak:
/**********************************/
* Najprostszy program w jezyku C *
**********************************/
main() /* glowna funkcja bez argumentow */
{
/* pusty blok funkcji */
}
/* koniec programu*/
Programiści zwykle umieszczają komentarze na początku programu, aby
wyjaśnić jego ogólne zadanie. Komentarze pojawiające się wewnątrz programu
objaśniają wybraną instrukcję lub zastosowane działanie. Jeśli instrukcja i
komentarz znajdują się w tym samym wierszu, to zwyczajowo zostawia się
między nimi pewien odstęp. Ułatwia to czytanie zarówno instrukcji jak i
komentarza.
Wraz z opracowaniem C++ pojawiła się metoda poprzedzania tekstu
komentarza (ale tylko pojedynczego wiersza) podwójnym ukośnikiem. Pisząc
programy w Visual C++ i w Borland C++ dopuszczalny jest więc taki zapis:
// Komentarz ...
Należy zauważyć, że taki sposób zapisu komentarzy nie został włączony do
standardu ANSI C i przed jego stosowaniem powinno się sprawdzić czy
wybrany kompilator uznaje tą konwencję.
Dla czytelności programów napisanych w języku C przyjmuje się pewne
konwencje, których stosowanie nie jest konieczne, ale świadczy o staranności
autora o takie przedstawienie tekstu programu aby był on zrozumiały dla innych.
W zakresie struktury programu:
•
instrukcje zapisuje się z wcięciem co powoduje, że wraz z wydłużeniem się
programu uwidacznia się struktura logiczna programu,
•
funkcję main () pozostawia się samą w wierszu,
•
nawiasy klamrowe pisze się w oddzielnych wierszach.
W zakresie stosowania wielkich i małych liter:
•
polecenia i nazwy funkcji pisze się małymi literami,
•
dużymi literami piszemy nazwy stałych i nazw symbolicznych.
4. Podstawowe elementy programów w języku C
4.1. Jednostki leksykalne
Podana w rozdziale 3 definicja programu jako zestawu definicji i deklaracji
funkcji i zmiennych nie jest jedynym sposobem jego podziału na części.
Programy mogą być rozpatrywane również jako zestawy jednostek leksykalnych
i komentarzy oddzielonych odstępami: znakami spacji, tabulacji i przejścia do
nowego wiersza. Jednostkami leksykalnymi są: słowa kluczowe, identyfikatory,
literały, operatory i ograniczniki.
Słowami kluczowymi są napisy złożone z ustalonego ciągu małych liter
zastrzeżone w danym standardzie języka, i które mogą być używane tylko w
przypisanym im znaczeniu. Słowa kluczowe nie mogą być używane do
oznaczania obiektów programowych jak np. funkcje i zmienne. Jeżeli użyjemy
słowa kluczowego jako nazwy np. zmiennej, kompilator wygeneruje komunikat
o błędzie i zatrzyma proces kompilacji.
Poniżej przedstawiono słowa kluczowe standardu K&K języka C:
auto
goto
break
if
case
int
char
long
continue
register
default
return
do
short
double
sizeof
else
static
entry
struct
extern
switch
float
typedef
for
union
Ze standardu ANSI C pochodzą dodatkowo słowa kluczowe:
const
enum
signed
void
volatile
W języku C++ dodano następujące słowa kluczowe:
catch
inline
cin
new
class
operator
cout
private
delete
protected
friend
Identyfikatory są to ciągi literowo – cyfrowe rozpoczynające się od litery, różne
od słów kluczowych. Ponieważ w sensie języka C literą jest także znak
podkreślenia „_” to pełny zestaw znaków alfanumerycznych dopuszczalnych w
identyfikatorach jest następujący:
•
litery A...Z oraz a...z,
•
cyfry 0...9 (cyfra nie może być pierwszym znakiem identyfikatora),
•
znak podkreślenia _
Należy zapamiętać, że niedopuszczalnymi znakami w identyfikatorach są
symbole : @ # $ ^ & ! ? oraz znaki operatorów arytmetycznych : + - / * %
Identyfikator może mieć dowolną długość, lecz tylko pierwsze 31 znaków ma
znaczenie dla kompilatora. Ze względu na czytelność programu dobrze jest
używać długich nazw dokładnie opisujących znaczenie identyfikatora. Praktyka
nakazuje też, aby nazwy różniły się przynajmniej jednym znakiem na początku
ciągu znaków.
Literałami nazywamy dane bezpośrednio wpisywane do instrukcji języka C.
Występują trzy rodzaje literałów: literały liczbowe (liczby), literały znakowe
(znaki) i literały łańcuchowe (łańcuchy). Literał liczbowy składa się z ciągu cyfr
dziesiętnych i znaku kropki. Literał znakowy ma postać napisu „c” , w którym c
jest dowolnym znakiem różnym od znaków ‘ (apostrof) i \ (ukośnik). Literał
znakowy może być również złożony z opisem znaku.
Najczęściej używanymi opisami znaków są:
\n opis znaku nowego wiersza,
\r opis znaku tabulacji,
\r opis znaku powrotu karetki,
\t opis znaku tabulacji,
\’ opis znaku apostrofu,
\\ opis znaku ukośnika.
Literał łańcuchowy ma postać napisu ”s”, w którym s jest dowolnym ciągiem
znaków oraz opisem znaków. W odróżnieniu od innych języków
programowania, w języku C literał znakowy nie stanowi szczególnego
przypadku literału łańcuchowego.
Operatory są jednostkami leksykalnymi określającymi, w jaki sposób dane
(zmienne, stałe i wartości wyrażeń) mają być przetworzone. Operatory
arytmetyczne używane do wykonania działań matematycznych są to;
Operator
Funkcja
+
Dodawanie
- Odejmowanie
*
Mnożenie
/
Dzielenie
%
Reszta z dzielenia liczb całkowitych
Pozostałe operatory, bardzo istotne dla poprawnej składni instrukcji to przecinek
i średnik. Średnik jest stosowany w języku C do sygnalizowania końca
instrukcji, przecinek natomiast służy do rozdzielania elementów listy.
Ogranicznikami w języku C są znaki */ zamykające komentarz oraz symbol \0
stanowiący specjalny kod jaki wstawia się po łańcuchu znaków
alfanumerycznych. Ogranicznik ten oznacza koniec łańcucha, dzięki czemu np.
funkcja puts() wie, gdzie przerwać pobieranie znaków do wyświetlania.
4.2. Stałe i zmienne
Jak wynika z samej nazwy, stała (constant) nigdy nie zmienia swojej wartości,
natomiast zmienna (variable) jest używana do reprezentowania różnych
wartości.
Wyrażenie to kombinacja stałych, zmiennych i operatorów stosowanych do
zapisu operacji arytmetycznych. W języku C instrukcja najczęściej to wyrażenie
zakończone średnikiem.
Przed użyciem w jakimś wyrażeniu zmiennej należy wcześniej ją zadeklarować.
Deklarowanie zmiennych oznacza dostarczenie kompilatorowi informacji o
nazwie i typie zmiennej. Deklaracja każdej zmiennej jest niezbędna z dwóch
powodów. Po pierwsze język C musi zarezerwować wystarczającą dużo pamięci
do przechowywania każdego elementu danej (różne typy danych zajmują różną
ilość pamięci). Po drugie, nie możliwe jest wykonanie wszystkich funkcji języka
C na wszystkich typach danych.
Deklaracje wszystkich użytych zmiennych w programie muszą być
zdefiniowane przed pierwszym ich użyciem, zwykle na początku bloku funkcji.
Definicja składa się z określenia typu oraz listy tego typu. Przykładowo, typ int
oznacza liczby całkowite, natomiast float – liczby rzeczywiste. Rodzaje typów
danych oraz zakresy wartości dla różnych typów omówione są w dalszej części
podręcznika.
Deklarowanie stałej polega na wskazaniu kompilatorowi języka C nazwy stałej i
jej wartości. Przed utworzeniem kody wynikowego, kompilator zastępuje
wystąpienie nazwy stałej jej wartością. W konsekwencji, sama nazwa stałej
nigdy nie będzie zamieniona w kod wynikowy.
W większości programów korzysta się zarówno ze stałych, jak i zmiennych.
Pisząc program, jego autor musi zadecydować, jak będzie stosowany każdy
element informacji – jako stała czy jako zmienna. Korzystanie ze stałych ma
często na celu ułatwienie wprowadzania zmian do programu. Jeżeli jakieś dane
wykorzystywane są w programie wielokrotnie, a w całym programie posiadają
tą samą wartość to należy zastosować w tym przypadku stałe. W dalszym etapie
pisania i testowania programu ułatwi to modyfikowanie wartości tych danych,
ponieważ wystarczy dokonanie zmiany tej wartości tylko w jednym miejscu, a
nie w kilku czy kilkudziesięciu liniach tekstu źródłowego programu.
Wartość początkowa zmiennej zazwyczaj nie jest znana do chwili uruchomienia
programu. Przypisywanie wartości zmiennych może nastąpić albo w wyniku
przypisania jej wartości poprzez wpisanie jej z klawiatury, albo w wyniku
obliczeń. Można również nadać zmiennej jej wartość początkową poprzez
przypisanie jej w deklaracji albo w oddzielnej instrukcji. W przeciwieństwie do
stałej wartość ta może ulec zmianie w trakcie przebiegu programu.
Każdą stałą i zmienną w programie trzeba nazwać stosując ściśle określone
reguły sformułowane dla ogólnego pojęcia identyfikatora języka C (omówione
już w poprzednim rozdziale).
Maksymalna długość nazwy zmiennej i stałej zależna jest od stosowanego
kompilatora, zazwyczaj wynosi 32 lub 64 znaki. Nazwy zmiennych i stałych
mogą zawierać wielkie i małe litery oraz znaki podkreślenia (_). Przyjęto jednak
w języku C pewną konwencję, że nazwy zmiennych piszemy tylko małymi
literami, a nazwy stałych wielkimi. Można stosować dowolną kombinację liter i
liczb, ale wszystkie nazwy muszą zaczynać się od litery. Stosowanie znaku
podkreślenia ma zwykle na celu ułatwienie czytania nazwy identyfikatora i
uczynić ją bardziej znaczącą. Na przykład nazwa zmiennej kat_alfa jest
bardziej czytelna niż nazwa katalfa (w nazwach nie używamy znaku spacji).
Jako nazw zmiennych i stałych nie wolno stosować słów kluczowych języka C,
nazw funkcji bibliotecznych oraz poleceń (if ... else lub switch).
4.3. Wyrażenia arytmetyczne
Wyrażeniem jest ciąg operatorów i operandów. Operatorem jest symbol
mówiący, jak przetworzyć operandy dając w wyniku jedną nową wartość. Np.
znak + oznacza dodanie dwóch operandów.
Operandem jest stała, zmienna lub wartość wyrażenia. Wyrażenie może więc
być operandem w innym wyrażeniu. W języku C operacja przypisania ma
wartość, zatem przypisanie też jest wyrażeniem.
Podobnie jak zmienne i stałe, również wyrażenia mają swój typ. Jest nim typ
wynikający z typów poszczególnych operandów niezbędnych do wykonania
poprawnych obliczeń.
4.3.1. Operatory arytmetyczne
W języku C wyróżnia się operatory jedno-, dwu- i trójargumentowe (unarne,
binarne i ternarne), co oznacza, że jedną wartość wynikową uzyskuje się na
podstawie jednej, dwóch lub trzech innych wartości.
Wymienione wcześniej operatory arytmetyczne znajdują się w grupie jedno- i
dwuargumentowych. Jednoargumentowym operatorem jest znak – (minus).
Operator ten można stosować do operandów typów całkowitych lub
rzeczywistych. Operator oznacza zmianę znaku liczby na przeciwny, np:
j = - j;
W standardzie języka C nie ma jednoargumentowego operatora + (plus).
Operatory matematyczne dwuargumentowe (+) (-) (*) realizują odpowiednio
dodawanie, odejmowanie i mnożenie operandów typu całkowitego lub
rzeczywistego. Należy zauważyć, że dla mnożenia operandów nie ma kontroli
poprawności wyniku, więc może on mieć wartość przypadkową, gdy poprawna
wartość nie mieści się w zakresie typu wyniku.
Operator / (ukośnik) wykonuje dzielenie pierwszego operandu przez drugi.
Operandy mogą mieć typ całkowity lub rzeczywisty. Jeżeli przynajmniej jeden
operand ma typ rzeczywisty, wynik jest typu rzeczywistego. Jeśli oba operandy
są typu całkowitego, zaś wynik powinien mieć część ułamkową – jest ona
gubiona. Zaokrąglenie do liczby całkowitej dla wyniku dodatniego odbywa się
zawsze w dół. Jeśli wynik ma wartość ujemną, to sposób zaokrąglenia (w dół
lub w górę) zależy od implementacji. Wynik dzielenia przez zero jest
przypadkowy.
Operator % (procent) oblicza resztę z dzielenia pierwszego operandu przez
drugi, czyli realizuje funkcję modulo. Oba operandy muszą być typu
całkowitego. Jeśli oba mają wartość dodatnią, to wynik też jest dodatni. Jeśli
choć jeden operand ma wartość ujemną – znak wyniku zależy od implementacji.
Wynik obliczenia reszty z dzielenia przez jest niezdefiniowany.
4.3.2. Operatory przypisania
Podstawowa w języku C instrukcja przypisania wykorzystuje operatory
przypisania. Operator przypisania (=) w jednej operacji przetwarza prawy
operand i zapisują wynik do lewego operandu. Technicznie oznacza to, że
wartość prawego operandu zostaje zapisana w pamięci w miejscu, w którym
przechowywany jest operand lewostronny. Podobnie przypisanie:
a = b = 5
powoduje przypisanie wartości 5 zmiennej całkowitej a i następnie zmiennej
całkowitej b. Po zakończeniu wykonania tej instrukcji obie zmienne zawierają
wartość 5.
Operatory złożonego przypisania łączą w jednej operacji przypisanie i inną
operację arytmetyczną. Na przykład instrukcja:
a+=b;
jest rozumiana jako
a=a+b;
Poniżej zamieszczono listę złożonych operatorów przypisania:
Operator
Opis
Operator = jest równoważny zapisowi
+=
dodawanie i przypisanie
x += y; x = x + y;
-=
odejmowanie i przypisanie
x -= y; x = x – y;
*=
mnożenie i przypisanie
x *= y; x = x * y;
/=
dzielenie i przypisanie
x /= y; x = x / y;
%=
reszta z dzielenia i przypisanie
x %=y;
x = x % y;
4.3.3. Operatory inkrementacji i dekrementacji
Operatory inkrementacji i dekrementacji są stosowane gdy wartość operandu
należy zwiększyć lub zmniejszyć o 1 (jeżeli operand jest liczbą). Operator
inkrementacji ++ zwiększa wartość operandu o 1, zaś operator inkrementacji
zmniejsza wartość operandu o 1.
Operatory inkrementacji i dekrementacji mogą występować przed albo za
operandem. Położenie operatora względem operandu wpływa na wartość
wyrażenia. Jeżeli operator występuje przed operandem, np.:
++x;
lub
–-x;
(operator pre-inkrementacji)
to wyrażenie ma wartość operandu po zwiększeniu lub zmniejszeniu. Jeśli
natomiast operator występuje za operandem, np.:
x++;
lub
x--;
(operator post-inkrementacji)
to wyrażenie ma wartość operandu przed jego zmianą.
4.4. Instrukcje
Jednym z najważniejszych elementów każdego języka programowania są
instrukcje. Umożliwiają one zapis algorytmu, a co za tym idzie - służą do
sterowania przebiegiem programu. W języku C/C++ ich lista jest dość krótka.
Instrukcje te dzięki swej efektywności w połączeniu z różnorodnością typów
danych
i
operatorów
-
umożliwiają
tworzenie
nawet
najbardziej
zaawansowanego oprogramowania:
Instrukcje możemy podzielić na :
- instrukcje warunkowe,
- instrukcje cyklu,
- instrukcje sterujące,
- instrukcje złożone (grupujące),
- instrukcje puste.
4.4.1. Instrukcje warunkowe
W zakresie algorytmów, częstym problemem jest sprawdzenie, czy zachodzi
dana zależność i w zależności od tego wykonanie takich czy innych czynności.
Sprawdzenie warunków i przejście do wykonania odpowiednich operacji
umożliwiają tzw. instrukcje warunkowe. Do tej grupy zaliczamy instrukcję
warunkową if, oraz instrukcję wyboru switch.
Instrukcja warunkowa if ma postać:
if (wyrażenie) instrukcja1
else instrukcja2
lub
if (wyrażenie)
instrukcja1
Badana jest wartość wyrażenia. Jeśli wartość wyrażenia warunkowego jest różna
od zera (prawda), wówczas wykonywana jest instrukcja1, w przeciwnym razie
(fałsz) -instrukcja2.
Warunki w instrukcji if porównują wartości zmiennych lub stałych z literałem
lub inną zmienną lub stałą. Porównanie jest dokonywane z użyciem jednego z
operatorów relacji:
Operator
Znaczenie
= =
równe
>
większe niż
<
mniejsze niż
> =
większe lub równe
< =
mniejsze lub równe
! =
nie jest równe
Stosowanie instrukcji if jest łatwe ale wymaga zapamiętania następujących
zasad:
- badane wyrażenie warunkowe musi znajdować się w nawiasach okrągłych,
- po warunku nie stawiamy średnika (pojawia się dopiero po całej instrukcji),
- w języku C nie ma instrukcji then rozdzielającej warunek i instrukcję do
wykonania (tak jest w innych językach programowania),
- umieszczenie wyrażenia warunkowego i instrukcji w oddzielnych wierszach
z wcięciem ułatwia czytanie programu.
Poniżej przedstawiono kilka przykładów stosowania instrukcji if:
if (wiek > 18) puts(” Jestes ju
ż
dorosly !”);
if (i > 0)
x+=10;
else {
x=0;
y=i;
}
Przykład zagnieżdżonej instrukcji if:
if (a==b) if (a==0) b=2; else a=2;
Instrukcje if mogą być zagnieżdżone dowolną liczbę razy. Przy użyciu instrukcji
if... else należy pamiętać, że słowo kluczowe else jest związane ze słowem
ostatnim if. W ostatnim przykładzie, wbrew intencji programisty, słowo
kluczowe else będzie związane nie z pierwszą instrukcją if, lecz z drugą.
Przytoczona instrukcja warunkowa nie jest wykonywana tak jak instrukcja:
if (a==b)
{
if (a==0) b=2;
}
else
a=2;
Jest ona wykonywana tak jak instrukcja
if (a==b)
{
if (a==0)
b=2;
else
a=2;
}
W sytuacjach kiedy w danym fragmencie programu powinno się sprawdzać
występowanie jednocześnie dwu (lub więcej) warunków, można to wykonać
przy stosowaniu jednego z operatorów logicznych.
Operator && (dwa znaki ampersand) wykonuje operację iloczynu logicznego
(funkcja „i”, „and”) zaś operator || (dwie kreski pionowe) oznacza sumę
logiczną (funkcja „lub”, „or”). Operator && daje w wyniku wartość 1, gdy oba
operandy mają wartości różne od zera, w przeciwnym przypadku daje wartość 0.
Operator || daje w wyniku wartość 1, gdy przynajmniej jeden z operandów ma
wartość różną od zera, zaś wartość 0, gdy oba mają wartość 0.
Język C gwarantuje, że wyrażenie z operatorami logicznymi jest przetwarzane
kolejno od strony lewej do prawej. Jeśli wartość pierwszego operandu decyduje
już o wyniku operacji, to wartość drugiego operandu nie jest obliczana.
Przykładem zastosowania operatora && jest testowanie wartości zmiennej.
Poniższa instrukcja warunkowa określa, czy zmienna y znajduje się w pewnym
zakresie wartości.
if (y>=0 && y<=100) puts(”OK”);
Operatorem logicznym jest także unarny operator negacji ! (wykrzyknik).
Generuje on wartość 0, gdy operand ma wartość różną od zera i wartość 1, gdy
operand ma wartość 0 (wartość 0 jest fałszywa). Przykładem zastosowania jest
fragment programu sprawdzający wartość zmiennej całkowitej zero.
if (!zero) puts(”Zmienna zero jest fałszywa”);
Powyższy warunek działa tak samo jak:
if (zero==)0
.
4.4.2. Instrukcja wyboru
Instrukcja wyboru switch (przełącz) umożliwia dokonanie wyboru spośród
nieograniczonej ilości możliwych instrukcji posługując się wartością wyrażenia
warunkowego i wariantami (case). Uogólniona składnia instrukcji switch jest
następująca:
switch (wyrażenie)
{
case wyrażenie1: instrukcja1;
case wyrażenie2: instrukcja;
...
default:
instrukcja-domyślna;
}
Instrukcja switch przekazuje sterowanie programu do instrukcji wewnątrz
bloku, zależnie od wartości wyrażenia warunkowego. Wykonanie instrukcji
wyboru rozpoczyna się od wyznaczenia wartości wyrażenia sterującego
(występującego w nawiasach po słowie kluczowym switch). Wartość ta musi
być wyrażona jako liczba całkowita, literał znakowy w apostrofach lub jako
nazwa stałej całkowitej lub znakowej. Wyznaczona wartość wyrażenia
sterującego jest następnie porównywana kolejno z wartościami wyrażeń stałych
występujących w przedrostkach należących do danej instrukcji decyzyjnej
(nazywanych etykietami wariantów – case label). Przy definiowaniu wyrażeń
stałowartościowych używanych w instrukcji switch-case należy zwrócić uwagę,
aby zostały one tak dobrane, by były wzajemnie wykluczające się. W razie
„nakładania się” wariantów wykonany będzie pierwszy spełniający warunek
zgodności wyrażenia sterującego i wartości przedrostka case. (licząc od góry).
Po stwierdzeniu równości, wykonywana jest instrukcja poprzedzona danym
przedrostkiem case oraz wszystkie instrukcje po niej następujące, aż do
wykonania instrukcji break albo do wykonania ostatniej instrukcji wnętrza
instrukcji decyzyjnej. Jeśli równość nie zostanie stwierdzona, to wykonywana
jest instrukcja poprzedzona przedrostkiem default: i ewentualne instrukcje
występujące po niej. Jeśli nie stwierdzono równości i nie użyto przedrostka
default:, to wykonanie instrukcji decyzyjnej jest uznawane za zakończone.
Poniższy przykład ilustruje opisane powyżej cechy instrukcji switch:
switch (i)
{
case ‘1’:
case ‘2’:
puts(”i=1 lub i=2”);
case ‘3’:
puts(”i!=1”);
puts(”i!=2”);
puts(”i=3”);
break;
default:
puts(”i ma warto
ść
inna ni
ż
cyfry 1-3”);
}
4.4.3. Instrukcje cyklu
Instrukcje cyklu umożliwiają wielokrotne wykonywanie pewnych sekwencji
instrukcji czyli pętli, które jak wiadomo, są podstawą programowania. Pętle
programowe są nazywane także iteracją a służą do powtarzania tych samych
instrukcji wielokrotnie, aż do chwili, gdy zostaną spełnione określone warunki.
W języku C wprowadzono trzy instrukcje iteracyjne:
- instrukcję for (”dla”),
- instrukcję while (”dopóki”),
- instrukcję do – while (”wykonuj - dopóki” )
4.4.3.1. Instrukcja for
Instrukcja cyklu for ma postać:
For (wyrażenie_inicjujące; wyrażenie_warunkowe; wyrażenie_zwiększające)
instrukcja
Wyrażenie_inicjujące, wyrażenie_warunkowe i wyrażenie_zwiększające są
opcjonalne, to znaczy mogą być pominięte.
Wykonywanie pętli odbywa się w następujący sposób:
1. Obliczane jest wyrażenie_inicjujące, co najczęściej powoduje zainicjowanie
liczników pętli,
2. Obliczane jest wyrażenie_warunkowe, jeśli jest ono niezerowe (prawda) to
wykonywana jest instrukcja związana z instrukcją for,
3. Obliczane jest wyrażenie_zwiększające, co powoduje zwykle zwiększenie lub
zmniejszenie liczników pętli,
4. Ponownie obliczane jest wyrażenie_warunkowe, jeśli jego wartość jest różna
od zera - wykonywane są ponownie czynności z punktu 2.
Puste wyrażenie warunkowe ma wartość logiczną prawda, tzn. instrukcja:
For (;;)
{
... tu instrukcja
}
jest równoważna instrukcji,
For (;1;)
{
... tu instrukcja
}
Przykładem wykorzystania instrukcji for może być fragment programu, w
którym obliczana jest wartość iloczynu odpowiedniej liczby tych samych
czynników ”
x
” (odpowiada to obliczeniu naturalnej n-tej potęgi liczby x).
Występuje tylko jedna zmienna liczników : ”
i”
- typu całkowitego.
il=1;
For (i=0, i<n, ++i)
i1*=x;
4.4.3.2. Instrukcja while
Ogólna postać instrukcji pętli while ma postać:
while (wyrażenie_warunkowe) instrukcja
Badane jest wyrażenie_warunkowe i jeśli ma ono wartość różną od zera, to
wykonywana jest instrukcja, po czym znów badane jest wyrażenie_warunkowe.
Jeśli wyrażenie_warunkowe ma wartość zero – wykonywanie pętli kończy się.
Ciało
pętli
jest
wykonywane
zero
lub
więcej
razy,
dopóki
wyrażenie_warunkowe nie będzie miało wartości 0.
Przykład zastosowania:
i=5;
while (i>0) –-i;
4.4.3.3. Instrukcja do – while
Instrukcja cyklu do ma postać:
do
instrukcja
while (wyrażenie_warunkowe)
Instrukcja jest wykonywana do momentu, gdy wyrażenie_warunkowe osiągnie
wartość zero (fałsz). Instrukcję do - while można przetłumaczyć jako :wykonuj
instrukcję, dopóki wartość wyrażenia_warunkowego oznacza prawdę. Instrukcję
tą stosujemy zamiast instrukcji while wówczas, gdy zależy nam, aby wykonana
została przynajmniej jedna instrukcja.
Przykład:
a=b=0;
do {
a+=b;
b++;
}
while (b<10);
4.4.4. Instrukcje sterujące
Instrukcje te umożliwiają opuszczanie pętli, przeniesienie wykonywania
programu o kilku poziomów pętli niżej, zakończenie wykonywania i zwrócenie
wartości funkcji. W języku C można wyróżnić następujące instrukcje sterujące:
- instrukcja break (”przerywaj”),
- instrukcja continue (”kontynuuj”),
- instrukcja return (”zwróć wartość i powróć”),
- instrukcja goto (”skoku”).
4.4.4.1. Instrukcja break
Instrukcja „zaniechania” break może być użyta tylko w instrukcjach cyklu (for,
while, do...while) oraz w instrukcji wyboru (swith). Powoduje ona opuszczenie
aktualnego poziomu pętli lub instrukcji wyboru. Należy pamiętać, że instrukcje
iteracyjne oraz instrukcje wyboru mogą być wielokrotnie zagnieżdżone, przed
użyciem instrukcji break należy zastanowić się, czy opuszczany jest właściwy
poziom. W celu przeniesienia wykonywania programu o kilka poziomów pętli
niżej należy użyć instrukcji skoku (goto).
4.4.4.2. Instrukcja continue
Instrukcja continue może być używana tylko wewnątrz instrukcji iteracyjnych.
Przerywa ona wykonywanie bieżącej iteracji pętli i przenosi sterowanie do
następnej iteracji (do następnego kroku wykonywania pęteli).
Jej użycie powoduje:
- w przypadku pętli while i do - while - przeniesienie sterowania z wnętrza
pętli do badania wyrażenia warunkowego.
- w przypadku pętli for - przeniesienie sterowania do wyrażenia
zwiększającego liczniki pętli, a następnie badane jest wyrażenie warunkowe.
- w przypadku wielokrotne zagnieżdżonych pętli instrukcja ”kontynuuj” jest
wiązana z najbliższą instrukcją iteracyjną.
4.4.4.3. Instrukcja return
Instrukcja return (”zwróć wartość i powróć”) ma postać:
return wyrażenie;
Napotkanie tej instrukcji powoduje zakończenie wykonywania funkcji w której
występuje i przeniesienie sterowania do funkcji wywołującej. Jeśli zwraca ona
wartość, wykonanie omawianej instrukcji spowoduje zwrócenie wartości
funkcji.
4.4.4.4. Instrukcja goto
W języku C istnieje instrukcja skoku umożliwiająca przekazanie sterowania do
określonego miejsca wewnątrz aktualnie wykonywanej funkcji programu.
Instrukcja skoku ma postać:
goto etykieta;
Etykieta określa miejsce w programie, do którego ma nastąpić skok; należy przy
tym pamiętać, że niemożliwe są skoki do miejsc znajdujących się poza funkcją,
do której należy instrukcja skoku. Etykieta składa się z identyfikatora (nazwy),
bezpośrednio po którym powinien znajdować się znak dwukropka (:),
Przykład użycia:
if (jestblad)
goto koniec;
...
/*tu normalne instrukcje*/
koniec:
return jestblad;
Należy pamiętać, że instrukcji goto nie należy nadużywać, gdyż jej stosowanie
zmniejsza czytelność programu i jest uzasadnione tylko w wyjątkowych
okolicznościach.
4.4.5. Instrukcja grupująca
Instrukcja grupująca (złożona), to deklaracje i instrukcje w nawiasach
klamrowych:
{
deklaracja
....
instrukcja
....
}
Zwykle instrukcja złożona występuje jako ciało innej instrukcji, w miejscu,
gdzie powinna być jedna instrukcja. Deklaracje można pominąć, natomiast w
nawiasach musi wystąpić przynajmniej jedna instrukcja,
4.4.6. Instrukcja pusta
W języku C wyróżnia się tzw. instrukcje pustą, tj. taką, która nie powoduje
wykonania żadnych czynności. Instrukcję taką zapisuje się w postaci średnika
nie poprzedzonego żadnym słowem kluczowym, ani wyrażeniem.
Na przykład:
while (...)
;
/* to jest instrukcja pusta */
Jak widać, instrukcja pusta jest zwykle związana z instrukcją iteracyjną.
4.5. Deklaracje
Dane podlegające przetwarzaniu są reprezentowane przez nazwy. Najprostszymi
nazwami danych są literały i identyfikatory. Pierwsze nich reprezentują w
programach stałe, a drugie – zmienne. Pojęcia stałych i zmiennych zostały już
wyjaśnione w rozdziale 4.2. podręcznika, należy jednak wyjaśnić teraz zasady
ich deklarowania.
4.5.1. Deklarowanie zmiennych prostych
Zmienną prostą jest zmienna, której mogą być przypisywane jedynie dane
skalarne. W języku C dane te należą do jednego z następujących typów:
char
(znakowy)
int
(całkowity)
short
(całkowity krótki)
long
(całkowity długi)
unsigned
(całkowity długi)
float
(zmiennopozycjny krótki)
double
(zmiennopozycyjny długi)
4.5.1.1. Zmienne znakowe
Zmienną znakową (character variable) deklaruje się poprzez użycie słowa
kluczowego char i nadanie zmiennej jakiejś wybranej nazwy (identyfikatora
zmiennej), np.:
char nazwa_zmiennej;
char x,y,z;
Drugi przykład dotyczy tzw. grupowej deklaracji zmiennych.
Język C pozwala na zainicjowanie zmiennej (czyli przypisanie jej początkowej
wartości) jednocześnie z jej zadeklarowaniem. Poniższa deklaracja jednocześnie
przypisuje zmiennej
znak
wartości – kod dużej litery A:
char znak = ‘A’;
Należy zauważyć, że dana typu char może być także pojedyńczą cyfrą, np.:
char z = ‘7’;
Jednak w języku C istnieje rozróżnienie między znakiem ”7” a cyfrą 7. Zmienna
znakowa z powyższego przykładu będzie miała wartość kodu ASCII znaku
cyfry ‘7’, czyli liczby dziesiętnej 55.
4.5.1.2. Zmienne typu całkowitego
Liczba całkowita int (integer)– nie ma miejsc dziesiętnych. Może być to liczba
dodatnia lub ujemna, a także zero, ale nie może mieć żadnych miejsc po kropce
dziesiętnej. Dlatego wynik dzielenia w zbiorze liczb całkowitych podlega
obcięciu (truncate) do części całkowitej. W zależności od stosowanego
kompilatora języka C długość liczb całkowitych deklarowanych jako typ C
może być różna. Kompilator Borland C++ 3 przystosowany do 16 bitowego
ś
rodowiska MS DOS posługuje się 16-bitowymi liczbami typu int. Oznacza to,
ż
e zmienne zadeklarowane w jednym z wymienionych typów całkowitych mogą
przyjmować wartości:
short int
Liczba całkowita między 0 a 255
int
Liczba całkowita między –32 767 a 32 768
long int
Liczba całkowita między –2 147 483 648
a 2 147 483 648
unsigned long
Dodatnia liczba całkowita między 0 a 4 294 967 295
Wersje kompilatorów dla 32-bitowego środowiska Windows 95/NT np.
VISUAL C++ posługują się już liczbami typu int z zakresu:
od –2 147 483 648 do 2 147 483 647.
Format deklaracji zmiennej całkowitej jest następujący:
int nazwa_zmiennej;
Przykłady deklaracji zmiennych typu całkowitego:
short int s=255;
/* deklaracja zmiennej z
przypisaniem wartosci */
int liczba1,liczba2; /*grupowa deklaracja zmiennych*/
unsigned int r; /* liczba całkowita bez znaku */
long k;
/ * długa liczba całkowita ze znakiem */
long unsigned int m; /* długa liczba całkowita bez
znaku */
4.5.1.3. Zmienne typu zmiennopozycyjnego
Liczby zmiennoprzecinkowe float (skrót od: floating point number) w
przeciwieństwie do liczb całkowitych zawierają przecinek dziesiętny. Liczby te
są inaczej nazywane liczbami rzeczywistymi (real number).
Ponieważ liczby zmiennopozycyjne mogą być bardzo małe lub bardzo duże,
więc zakres ich wartości często wyraża się jako liczby wykładnicze (scientific
notation). W takim formacie liczba przedstawiona jest w postaci mantysy i
wykładnika. Cecha i mantysa są rozdzielone literą E lub e.
[mantysa] e [wykładnik] lub [mantysa] E [wykładnik]
Podobnie jak w przypadku liczb całkowitych, liczby zmiennoprzecinkowe typu
float posiadają ograniczony zakres dopuszczalnych wartości. Standard ANSI C
wymaga, aby zakres ten wynosił o najmniej +/- 1.0*10
37
. Normalnie do
reprezentowania liczb typu float używane są 32 bity (4 bajty). Wynika z tego, że
liczba zmiennoprzecinkowa w C może mieć co najmniej sześć cyfr znaczących
dokładnych. Dodatkowo, w języku C można zadeklarować zmienną typu
double, do której przechowywania wykorzystywane jest dwukrotnie więcej
bitów, niż do przechowywania liczby typu float. Dlatego liczba
zmiennopozycyjna typu double to liczba, o podwójnej precyzji, posiadająca co
najmniej 10 cyfr znaczących dokładnych. Aby komputer prawidłowo
rozpoznawał format liczb, liczby zmiennoprzecinkowe mogą być wyposażone w
przyrostek (suffix) f lub F na końcu, np.: 7.01f, -3.14F.
Liczba rzeczywista bez takiego przyrostka jest domyślnie traktowana jako liczba
typu double.
Format deklaracji zmiennej numerycznej, zmiennoprzecinkowej:
float nazwa_zmiennej;
Przykłady deklaracji zmiennych rzeczywistych:
float x;
/*liczba pojedynczej precyzji */
DOUBLE z;
/*liczba podwójnej precyzji */
float a=3.14; /*deklaracja z przypisaniem wartosci*/
4.5.2. Deklarowanie zmiennych typu wskaźnikowego
Wartością zmiennej wskaźnikowej jest wskaźnik czyli adres obszaru pamięci
zajmowanego przez jakiś obiekt. Przez wskaźnik można w sposób pośredni
odwołać się do tego obiektu. Charakterystyczne przykłady zastosowania
wskaźników wiążą się z tworzeniem list, oraz z zarządzaniem obiektu, dla
których pamięć jest przydzielana dynamicznie podczas wykonywania programu.
Zrozumienie istoty wskaźników i opanowanie technik ich stosowania ma istotne
znaczenie dla skutecznego programowania w języku C. Dotychczas
odwoływaliśmy się do zmiennych bezpośrednio, to znaczy używając ich nazw –
identyfikatorów. Rozpatrując zmienne typu wskaźnikowego należy wprowadzić
pojęcie pośredniego odwoływania się do obiektów (wskaźnikiem można
wskazywać nie tylko zwykłe zmienne, ale także np. funkcje). Takie pośrednie
odwoływanie się do obiektów ma nazwę indirection.
Zamiast bezpośredniego przyporządkowania zmiennej jakiejś wartości, można
zmiennymi i ich zawartością manipulować pośrednio, posługując się
wskazywaniem określonych adresów w pamięci RAM, bez używania
identyfikatorów, które tym „komórkom pamięci” zostały uprzednio nadane.
Utworzenie nowej, specjalnej zmiennej zwanej wskaźnikiem (pointer variable)
powoduje, iż możemy w pamięci RAM przechowywać adres innej zmiennej.
Znajomość adresu danych w pamięci i odwołanie się do nich poprzez wskazanie
adresu – to często najszybsza i najbardziej efektywna metoda sięgania do tych
danych.
Podsumowując powyższe rozważania można podać następującą definicję
wskaźnika:
Wskaźnik (pointer) to zmienna używana do wskazywania innej zmiennej.
Zgodnie z powyższą definicją wskaźnik można określić również jako zmienna
adresowa (pointer – adress variable).
W chwili deklarowania dowolnej zmiennej, pewien fragment wolnej do tej pory
pamięci musi zostać „zarezerwowany” dla tej zmiennej, a do nazwy –
identyfikatora zmiennej zostaje przyporządkowany adres miejsca w pamięci.
Adres przyporządkowany zmiennej jest nazywany (wartością lewostronną –
variable left value). Przykładowo, po zadeklarowaniu i zainicjowaniu zmiennej:
int x;
x=7;
zmienna x ma dwie wartości:
wartość lewostronna: 1000
wartość prawostronna: 7
W tym przypadku wartość lewostronna to adres (numer bajtu) w pamięci, gdzie
przechowywana jest zawartość zmiennej, jej wartość prawostronna – 7.
Posługując się lewostronną wartością zmiennych, kompilator C może łatwo
zlokalizować odpowiednie miejsce w pamięci zarezerwowane dla danej
zmiennej, a następnie posługiwać się tym adresem do poprawnego odczytu i
zapisu prawostronnej wartości zmiennej.
Język C zawiera specjalny operator & zwracający wartość lewostronną
zmiennej, nazywany operatorem adresowym (adress-of-operator). Dla
przykładu fragment programu:
long int x;
y=&x;
powoduje przypisanie zmiennej y adresu w pamięci zmiennej całkowitej x.
Działanie odwrotne powoduje operator deferencji
*
. Zapis:
long int a;
a=
*
y;
oznacza:
- pobierz zawartość spod adresu pamięci wskazywanego przez zmienną
(wskaźnik) y i przyporządkuj ją, jako nową wartość zmiennej a.
Wskaźnik jako zmienna także ma swoją wartość lewostronną i wartość
prawostronną, jednakże w przypadku wskaźnika obie te wartości oznaczają
adresy w pamięci. Wartość lewostronna wskaźnika – jego własny adres – służy
do odwoływania się do samego wskaźnika. Wartość prawostronna wskaźnika –
zawartość wskaźnika ro adres jakiejś innej zmiennej w pamięci.
Uogólniony format deklaracji wskaźnika jest następujący:
typ_danych *nazwa_wskaźnika;
Symbol „gwiazdki” (
*
= ‘asterisk’) wskazuje, że zmienna jest wskaźnikiem.
Poniżej zamieszczono przykłady kilku deklaracji wskaźników, wskazujących
adresy danych różnych typów:
char ptr_c;
/*wskaznik do zmiennej typu char*/
int
*
ptr_int;
/*wskaznik do zmiennej typu int*/
float
*
ptr_c;
/*wskaznik do zmiennej typu float*/
4.5.3. Deklarowanie zmiennych typu tablicowego
Tablica jest zestawieniem obiektów tego samego typu (set of variables). W tym
przypadku poszczególne obiekty nie mają nazw, a dostęp do nich jest
możliwy tylko przez podanie położenia danego obiektu w tablicy. Ten rodzaj
dostępu do obiektu nazywa się indeksowaniem.
Uogólniony format deklaracji jest następujący:
typ_danych nazwa_tablicy[rozmiar_tablicy];
gdzie typ_danych to typ wszystkich zmiennych wchodzących w skład tablicy,
nazwa_tablicy to identyfikator nadawany zgodnie z regułami C, rozmiar_tablicy
określa, ile elementów zawiera tablica. Całkowitą ilość bajtów pamięci
zajmowanych przez daną tablicę można obliczyć posługując się wzorem:
sizeof(typ_danych)*rozmiar_tablicy
gdzie sizeof jest operatorem rozmiaru.
W języku C nie ma ograniczeń liczby wymiarów tablicy, pod warunkiem, że
całkowity rozmiar tablicy nie przekracza 64kB.
Przykłady deklaracji zmiennych tablicowych:
CHAR str[30] /* tablica 30 znaków */
CHAR *t[12] /* tablica 12 wska
ź
ników znaków */
DOUBLE tabdb1[30] /* tablica 30 liczb DOUBLE */
DOUBLE *tabdb1p[10] /* tablica 10 wska
ź
ników liczb
typu DOUBLE */
W języku C można również deklarować tablice o większej ilości wymiarów
(macierze wielowymiarowe). Uogólniony format deklaracji tablicy n -
wymiarowej jest następujący:
typ_danych nazwa_tablicy[wymiar_1] [wymiar_2] ... [wymiar_n]
gdzie n jest dodatnie i całkowite.
Przykład deklaracji:
INT tab[10][20] /*dwuwymiarowa tablica liczb
całkowitych */
Deklarację tablicy można połączyć z jej inicjacją. Polega ona na wymienianiu
bezpośrednio po deklaracji zmiennej listy inicjatorów tablicy ujętych w nawiasy
{}. Przy inicjowaniu tablic obowiązują zasady:
- lista inicjatorów nie może zawierać więcej elementów niż jest wstanie
pomieścić tablica,
- w języku C inicjator musi być stałą,
- jeśli inicjatorów jest mniej niż elementów tablicy, wówczas pozostałe
elementy tablicy są inicjowane zerami w przypadku tablicy liczb lub znaków
oraz wskazaniami pustymi NULL w przypadku tablicy wskaźników.
Przykłady deklaracji tablic z inicjowaniem wartości:
LONG DOUBLE tabdb1[30]={10.01,20,30};
DOUBLE tab1[2][3]={{3,1},{-10,90},{2,-2}};
W tablicy tabdb1 zainicjowano pierwsze trzy elementy, pozostałym zostanie
domyślnie przypisana wartość zero. Deklaracja tablicy tab1 pokazuje możliwość
użycia nawiasów klamrowych dla wygodniejszej orientacji w strukturze
inicjowanej tablicy.
W języku C istnieje również możliwość zadeklarowania tablicy o
nieokreślonych rozmiarach. W pierwszym specyfikatorze rozmiaru tablicy nie
podaje się wówczas wymiaru, nawiasy klamrowe pozostawiamy puste.
Na przykład:
INT n[][s][10];
CHAR *txt[];
4.5.4. Deklaracje łańcuchów znakowych
Łańcuch (napis) w języku C jest tablicą znaków zakończoną znakiem o kodzie
zero: /0 (ogranicznikiem łańcucha). Łańcuchy deklarujemy tak samo, jak tablice
znaków np.:
CHAR str[100]
/* ła
ń
cuch o rozmiarze 100 znaków */
Biorąc pod uwagę fakt, że ostatni element jest zarezerwowany dla ogranicznika
łańcucha, trzeba zawsze deklarować tablicę o jeden element większą niż wynosi
maksymalna liczba znaków.
Deklarację łańcucha można połączyć z jego inicjacją:
CHAR[8]=”/n/f(ADAS)”;
CHAR str [8]={‘/n’,’/f’,’(’,’A’,’D’,’A’,’S’,’/0’};
CHAR *str1=”ADAS”;
Inicjowanie łańcuchów znakowych może odbywać się przez podanie jego
zawartości w cudzysłowach (tak jak w pierwszej deklaracji) lub przez
wymienienie kolejno stałych znakowych określających zawartość łańcucha (tak
jak w drugiej deklaracji). Jest jednak znaczna różnica, między tymi sposobami
inicjacji, gdyż w pierwszym przypadku kompilator sam uzupełnia deklarację o
znak ‘/0’ oznaczający koniec łańcucha znaków, w drugim zaś musi zrobić to
programista. Trzeci z powyższych deklaracji pokazuje, w jaki sposób można
deklarować łańcuchy z wykorzystaniem wskaźników. W takim przypadku
kompilator przydziela pamięć łańcuchowi wymienionemu w części inicjacyjnej
deklaracji, a zmiennej wskaźnikowej przypisuje adres jego początku.
4.5.5. Deklarowanie struktur
Tablice (macierze) służyły do zgrupowania wielu zmiennych tego samego typu,
natomiast w języku C można łączyć zmienne różnych typów w tzw. struktury.
Struktura to połączenie w jedną całość danych różnego typu w taki sposób, że
mogą one być traktowane jako jeden moduł (unit). Pomiędzy tablicami i
strukturami jest wiele różnic. Oprócz faktu, że w obrębie jednej struktury
znajdują się dane różnych typów, każdy element danych w strukturze ma swoją
nazwę zamiast numeru indeksu, jak to było w tablicach.
Deklaracje zmiennych typu strukturalnego mogą mieć dwie formy. W pierwszej
deklaruje się zmienne równocześnie ze składnikami struktury:
struct {
deklaracja_składowych
} identyfikator_struktury1,identyfikator_struktury2,...
Wyróżnikiem deklaracji struktury jest słowo kluczowe struct. Po nim w
nawiasach klamrowych wymienione są poszczególne składniki. Lista
składników jest podobna do deklaracji zmiennych, gdyż zawiera ich typy i
identyfikatory (bez wyrażenia inicjującego). Deklarację kończy lista zmiennych
typu strukturalnego.
Przykład:
Struct {
char nazwisko[15];
char imie[19];
unsigned short wiek;
it telefon, pensja;
} osoba;
Deklarację można uzupełnić o nazwę typu strukturalnego. Umieszcza się ją po
słowie kluczowym struct. W deklaracji z nazwą typu strukturalnego można
pominąć nazwy zmiennych i wówczas jest ona deklaracją typu.
struct typ_structur {
typ1 zmienna1;
typ2 zmienna2;
typ3 zmienna3;
.......
};
Identyfikator typ_structur może być wybrany dowolnie, ale z uwzględnieniem
reguł nazewnictwa w języku C. Po nim w nawiasach klamrowych wymienione
są poszczególne składniki tj. pola struktury. Deklaracja musi być zamknięta
nawiasem klamrowym i średnikiem.
Jeżeli w programie zadeklarowany jest typ strukturalny, czyli struktura ma
nadaną nazwę, można użyć deklaracji zmiennych wykorzystującej tę nazwę, np.:
Struct typ_osoba ojciec,matka;
Deklaracja w tej formie używa zdefiniowanego wcześniej typu strukturalnego
jako typu definiowanych zmiennych.
Inicjacja struktury polega na umieszczeniu w nawiasach klamrowych
inicjatorów poszczególnych pól zgodnie z kolejnością ich występowania w
definicji typu struktury. Liczba inicjatorów nie może przekraczać liczby
składowych struktury, jeśli zaś jest mniejsza od tej liczby, wówczas pozostałe
pola są inicjowane zerami (pola rzeczywiste i całkowite) lub adresami pustymi
(pola wskaźnikowe).
Struktura jest inicjowana przy pomocy listy danych, nazywanych listą
inicjującą. Poszczególne pozycje danych są na takiej liście rozdzielane
przecinkami.
Przykład inicjowania struktury:
struct liczba_urojona {
double x,c;
}
struct liczba_urojona a={3.14,2.75}
4.5.6. Deklarowanie unii
Unia jest to blok pamięci do przechowywania różnych elementów. Deklaracje
unii są identyczne jak deklaracje struktur, a wyróżnia je słowo kluczowe union,
np.:
union {
char c;
int i;
long l;
} przykład;
Unię od struktury odróżnia to, że w każdej chwili może ona zawierać jedną
warstwę spośród wymienionych w deklaracji jako składniki. Unia ma
zarezerwowane w pamięci miejsce na swój największy składnik, a nie na
wszystkie. Stosowanie unii pozwala na wykorzystanie tego samego miejsca w
pamięci do różnych celów. Ilustruje to następujący przykład:
struct znajomi {
char nazwisko[15], imie[10];
long data_urodzenia;
int telefon;
char plec; /* ‘m’==on, ‘k’==ona */
union {
char nazw_rod[15];
int stopien;
} xx;
} przyjaciele;
W przykładzie tym składnikiem struktury jest unia o nazwie xx zawierająca
bądź tablice znakową nazw_rod, w której jest zapisane nazwisko panieńskie,
bądź liczbę całkowitą stopien, będącą indeksem w tablicy stopni wojskowych.
Rozpoznawanie, która wartość jest zapisana w unii następuje po sprawdzeniu
wartości składnika plec określającego płeć osoby.
4.5.7. Deklarowanie stałych
Zadeklarowanie stałej oznacza wskazanie kompilatorowi nazwy stałej i jej
wartości. Wykonuje się to przed funkcją main() za pomocą dyrektywy
kompilatora #define mającej ogólną składnię:
#define nazwa wartość
W języku C rozróżniane są cztery rodzaje stałych: całkowite, rzeczywiste, znaki
i łańcuchy znaków.
Stała całkowita może być zapisana jako liczba dziesiętna, oktalna lub
heksydecymalna. Stała dziesiętna, to liczba złożona z cyfr od 0 do 9. Stała
oktalna (ósemkowa) rozpoczyna się zawsze od cyfry 0, po której następują cyfry
oktalne (od 0 do 7). Stałą heksadecymalną (szesnastkową) rozpoczyna cyfra 0,
po której następuje litera x lub X i dalej cyfry heksadecymalne (cyfry od 0 do 9 i
duże lub małe litery od a do f).
Stałe całkowite dziesiętne mają typ int lub long, zależnie od wartości stałej.
Jeżeli stała mieści się w zakresie liczb typu int – otrzymuje ten typ, a jeśli nie –
przyjmuje typ long.
Stałe rzeczywiste składają się z:
- dziesiętnej części całkowitej,
- kropki oddzielającej część całą od ułamkowej,
- dziesiętnej części ułamkowej,
- litery E lub e, po której występuje całkowity wykładnik potęgi,
- przyrostów F, f, L, l (opcjonalnie)
Standardowo stałe rzeczywiste są przechowywane w postaci liczb typu
DOUBLE, a nadanie zmiennej należącej do jednego z pozostałych typów
rzeczywistych wymaga wykonania konwersji z typu DOUBLE. Aby tego
uniknąć należy na końcu stałej rzeczywistej dodać przyrostek F lub f dla typu
FLOAT zaś dla liczb LONG DOUBLE przyrostek L lub l.
W przypadku stałych znakowych przypisywana wartość musi znaleźć się w
apostrofach. Podobnie w celu deklaracji łańcucha należy wartość ująć w
cudzysłowy.
Przykłady deklaracji stałych:
#define liczba_calkowita 3
#define liczba_rzeczywista 3.14
#define znak ‘A’
#define lancuch ”ABC”
Kompilator języka C++ pozwala na deklarowanie stałych wewnątrz funkcji, a
nie jak dyrektywę #define przed main(). Przy pomocy modyfikatora const
deklaruje się stałą, określa jej typ i przypisuje wartość.
Przykłady deklaracji stałych w języku C++:
const int liczba=8;
const char znak=’a’;
4.6. Funkcje
Funkcja jest zestawem deklaracji i instrukcji, zwykle utworzonym do wykonania
jakiegoś zadania. Deklaracje i instrukcje jednej funkcji są niezależne od
deklaracji i instrukcji w innej funkcji. W dużych programach celowe jest
podzielenie całości na mniejsze zadania, co upraszcza tworzenie i testowanie
programu, a także czyni go czytelniejszym. Inna zaleta stosowania funkcji to
oszczędność czasu i pamięci. Raz napisana i skompilowana funkcja może być
używana wielokrotnie w różnych programach. Funkcja jest zwykle umieszczana
w jednym miejscu pamięci, do którego następują odwołania w momencie, gdy
wywoływana jest funkcja. Kolejne wywołania funkcji zwiększają długość kodu
wynikowego programu nieznacznie w porównaniu z długością funkcji.
4.6.1. Deklaracja funkcji
Deklaracja w odróżnieniu od definicji, jest pojęciem logicznym, gdyż stanowi
tylko informację dla kompilatora, że funkcja o określonej nazwie, typie wartości
i parametrów może zostać użyta gdzieś w danym module programu. Definicja
funkcji określa natomiast, co funkcja właściwie robi. Stanowi więc ona zapis
jakiegoś algorytmu lub jego części. Definicję funkcji powoduje, w odróżnieniu
od deklaracji, przydzielenie obszaru pamięci, w którym znajduje się kod
wynikowy funkcji.
W standardzie ANSII oraz w C++ deklaracja funkcji nazywana jest prototypem i
wygląda tak:
specyfikator_typu_danych identyfikator_funkcji (
specyfikator_typu_danych1 argument1,
specyfikator_typu_danych2 argument2,
...
specyfikator_typu_danych_n argument_n
);
W deklaracji powyższej specyfikator_typu_danych określa typ wartości funkcji.
Jeśli nie został on określony w sposób jawny, przyjmowany jest typ INT,
natomiast lista deklaracji parametrów składa się z specyfikatorów typów
parametrów.
Przykłady prototypów funkcji:
void funkcja (void); /*bezparametrowa funkcja nie
zwracajaca wartosci */
fun (int); /* funkcja o wartosci typu INT(domyslnie)
i jednym
parametrze calkowitym (INT) */
void fun2 (int); /* funkcja o jednym parametrze
calkowitym, nie zwracajaca wartosci */
DOUBLE max(double l1, l2);
/* funkcja o parametrach
i wartosci typu rzeczywistego (DOUBLE) */
W języku C/C++ nie rozdziela się pojęcia funkcji i procedury - odpowiednikiem
procedur są funkcje o wartościach typu void, czyli nie zwracające wartości.
Zaleca się sygnowanie braku parametrów słowem kluczowym void, np.:
int func(void);
4.6.2. Definicja funkcji
Definicja stanowi o tym, co dana funkcja wykonuje. Definicja składa się zwykle
z 5 występujących kolejno po sobie elementów:
1. opcjonalnych specyfikatorów extern, static, inline (C++),
2. typu wartości funkcji,
3. identyfikatora (nazwy) funkcji,
4. listy deklaracji parametrów funkcji, która może być pusta, co oznacza
funkcję bezparametrową (dla oznaczania braku parametrów, lepiej używać
słowo kluczowe void),
5. ciała funkcji umieszczonego w nawiasach klamrowych.
Ad. 1.
Specyfikator extern informuje, że funkcja ma być dostępna również poza
modułem programu, w którym znajduje się jej definicja. Użycie static
powoduje, że funkcja będzie dostępna tylko w obrębie modułu programu, w
którym ją zadeklarowano. Funkcję taką nazywamy statyczną. Domyślnie
przyjmowany jest specyfikator extern .
Ad. 2.
Typ wartości - te same reguły co w przypadku deklaracji - może być on
dowolny z wyjątkiem funkcji i tablicy. Dozwolony jest więc specyfikator void,
kiedy pozwala definiować funkcję nie zwracającą wartości. Domyślnie
przyjmowany jest int.
Ad. 3.
Przy tworzeniu nazw funkcji obowiązują te same zasady, co w przypadku
identyfikatorów zmiennych. W języku C w obrębie danego modułu nazwy
funkcji nie mogą się powtarzać.
Ad. 4.
Lista deklaracji parametrów składa się z deklaracji parametrów rozdzielonych
przecinkami. Deklaracja parametru wygląda identycznie, jak deklaracja
zmiennej typu takiego samego, jak parametr: np.:
INT f(int i) {...}
Funkcja f posiada parametr całkowity i oraz parametr pr wskazujący funkcję
Ad. 6.
Ciało funkcji zbudowane jest z deklaracji i instrukcji, całość ujęta w nawiasy
klamrowe. Przed nawiasem zamykającym } zapisuje się instrukcję return.
Wykonanie instrukcji return w bloku funkcji wywoływanej lub wykonanie
ostatniej instrukcji w tym bloku zwraca sterowanie do funkcji wywołującej.
Definiowanie funkcji zastępuje prototyp, tzn. dostarcza informacji niezbędnych
dla kontroli poprawności odwołań do funkcji, ale nie odwołanie - prototyp nie
pełni roli definicji.
4.6.3. Wywołanie funkcji
Wywołanie funkcji ma postać:
identyfikator_funkcji (lista_parametrów_wywołania)
Na listę parametrów wywołania składają się oddzielone przecinkami wyrażenia
typu zgodnego z typem parametru funkcji.
Przykładowe wywołanie funkcji func zdefiniowanej jako:
INT func(INT i, INT j, CHAR *str)
{
...
}
może mieć postać:
INT a=10, b=20;
CHAR *tekst=”Programowanie w jezyku C”;
func(a, b, tekst);
/* wywołanie funkcji func */
W tym przykładzie typy parametrów wywołania są identyczne z typami
określonymi w definicji funkcji.
5. Standardowa biblioteka funkcji
5.1. Standardowe wejście i wyjście
Wszystkie programy odwołujące się do funkcji ze standardowej biblioteki
języka C muszą mieć dołączony plik nagłówkowy tej biblioteki, zawierający
definicje wielu zmiennych oraz makrodefinicje. Plik ten nazywa się stdio.h i
jest dołączony do programu dyrektywą preprocesora:
#include <stdio.h>
W języku C wszystkie urządzenia zewnętrzne są traktowane tak jak pliki.
Wynika to z przyjęcia założenia, że wszystkie strumienie danych (do / z pliku) i
wszystkie pliki można traktować tak samo, chociaż niektóre strumienie / pliki
danych mogą przypływać z różnych kierunków, np. z dysku, z kasety, z
terminala, a nawet od drukarki. W pliku nagłówkowym stdio.h zdefiniowano
trzy strumienie plikowe (file stream) standardowo wstępnie związane z
urządzeniami zewnętrznymi:
•
stdin – standardowe wejście do odczytu (pre-opened input)
•
stdout – standardowe wyjście do zapisu (pre-opened standard output)
•
stderr – standardowe wyjście diagnostyczne, dla komunikatów o błędach
(standard error)
Zazwyczaj stdin zostaje powiązany z klawiaturą, a stdout i stderr zostają
powiązane z ekranem monitora.
Podstawowe funkcje obsługi wejścia / wyjścia, to getchar i putchar.
5.1.1. Funkcje getchar() i putchar()
Funkcja getchar() pobiera jeden znak ze standardowego wejścia i zwraca jego
kod jako swoją wartość, natomiast putchar wyświetla znak o podanym kodzie.
Obie funkcje zwracają wartość –1 w przypadku wystąpienia błędu odczytu lub
zapisu.
Funkcja getchar() jest wywoływana poprzez przypisanie jej (znakiem równości
= ) do jakiejś zmiennej. Funkcja ta nie ma argumentu. Oznacza to, że nic nie jest
przekazywane funkcji w nawiasach okrągłych. Specyfiką wprowadzania
wartości przy wykonywaniu funkcji getchar() jest to, że po naciśnięciu klawisza
i wyświetleniu wybranego znaku na ekranie nie potrzeba naciskać klawisza
ENTER. Program kontynuuje działanie natychmiast po wprowadzeniu znaku i
jest on przypisywany zmiennej już po naciśnięciu klawisza.
Funkcja putchar() wyprowadzająca znak na ekran wymaga podania jako
argument – znaku tj. zmiennej typu char albo zmiennej całkowitej - wartość
kodu znaku zwróconego przez funkcję getchar().
Poniżej przedstawiono program obrazujący sposób stosowania funkcji getchar()
i putchar():
#include <stdio.h>
main()
{
char znak;
puts(”Nacisnij klawisz alfanumeryczny:”);
znak=getchar();
putchar(‘\n’); /* nowy wiersz */
putchar(znak);
}
Z funkcji getchar() można również skorzystać także, aby zatrzymać działanie
programu w celu np. odczytania pojawiających się wyników obliczeń na
ekranie. Po wypełnieniu ekranu napisami (zwykle 25 wierszy) można zatrzymać
program do czasu naciśnięcia dowolnego klawisza przez użytkownika. Efekt ten
uzyska się stosując następujący zapis:
puts(”Nacisnij klawisz aby kontynuowa
ć
”);
getchar();
Naciśnięcie klawisza spowoduje uruchomienie programu, ale żadna wartość nie
zostanie przypisana zmiennej.
5.1.2. Funkcje gets() i puts()
Funkcja gets() pobiera linię (łańcuch) ze standardowego wejścia i zwraca
wskaźnik na bufor zawierający tę linię. Składnia funkcji gets() jest następująca:
#include <stdio.h>
char *gets(char *s);
gdzie s oznacza identyfikator tablicy znakowej, w której przechowywane są
znaki wczytane z standardowego wejścia. Funkcja gets() kończy swoje działanie
po wczytaniu znaku przejścia do nowego wiersza (po naciśnięciu ENTER), albo
po wczytaniu znaku EOF (koniec pliku). Funkcja dodaje do tablicy znakowej
znacznik ‘\0’ i zwraca s – wskaźnik do tablicy, do której zapisała znaki, albo
wskaźnik pusty (null pointer), jeśli operacja się nie udała.
Funkcja puts() wyprowadza linię znaków (łańcuch) do standardowego
strumienia wyjściowego stdout. Składnia funkcji puts() jest następująca:
#include <stdio.h>
char puts(char *s);
gdzie s oznacza tablicę (lub wskaźnik) odwołujący się do łańcuch znakowego,
który ma zostać wysłany do standardowego strumienia wyjściowego.
Funkcja puts() likwiduje znacznik końca tekstowego, dodając w jego miejsce
znak przejścia do nowego wiersza.
Poniżej przedstawiono program obrazujący sposób stosowania funkcji gets() i
puts():
#include <stdio.h>
main()
{
char imie[10];
puts(”Podaj swoje imie:”);
gets(imie);
putchar(‘\n’); /* nowy wiersz */
puts(”Twoje imie to:”);
putchar(‘\n’); /* nowy wiersz */
puts(imie);
return 0;
}
Podczas wprowadzania znaków możliwe jest usuwanie błędnych znaków
klawiszem Backspace i ponowne ich wpisywanie. Zaakceptowanie wpisanych
znaków następuje po naciśnięciu Enter. Zostają one wówczas uzupełnione o
znak ogranicznika łańcucha i przypisane danej zmiennej.
5.1.3. Funkcje scanf() i printf()
Funkcja scanf() jest uniwersalnym sposobem na wprowadzanie do komputera
danych wszelkiego typu. Funkcja śledzi (skanuje) klawiaturę badając, czy został
naciśnięty jakiś klawisz, a potem interpretuje dane wejściowe zgodnie ze
specyfikatorami formatowania. Składnia funkcji scanf() jest następująca:
#include <stdio.h>
int scanf(const char *format, [adres,...]);
Parametr funkcji jest dwuczęściowy – składa się z łańcucha sterującego (format
string) i listy danych. Łańcuch sterujący zawiera specyfikatory formatowania
wskazujące, jak będą interpretowane dane wejściowe.
Lista specyfikatorów formatowania dla funkcji scanf():
%d wprowadza liczbę całkowitą
%u wprowadza liczbę bez znaku
%f wprowadza daną typu float
%e wprowadza liczbę w zapisie wykładniczym
%g wprowadza liczbę dziesiętną w najkrótszym zapisie dziesiętnym lub
wykładniczym
%c wprowadza daną typu char
%s wprowadza łańcuch
Specyfikatory formatowania są nazywane znakami konwersji, ponieważ
przekształcają dane pierwotne w strumieniu wejściowym na dane zgodne z
wybranymi typami.
W przypadku użycia specyfikatora formatu %s wczytywanie danych zostanie
zakończone, jeśli w strumieniu znaków wejściowych pojawi się jeden z znaków
specjalnych: spacja, znak nowego wiersza, tabulator. Znaki wczytywane przez
funkcje są przechowywane w tablicy znakowej, która musi zostać wskazana
funkcji, jako jej argument. Po zakończeniu wczytywania łańcuch znaków jako
ostatni element tablicy znakowej zostaje automatycznie dodany znacznik ‘\0’.
W liście danych z każdym użytym specyfikatorem formatu musi być związany
kolejny argument będący wskaźnikiem na zmienne właściwego typu. W tym
celu wykorzystywany jest operator pobrania adresu &. Jest on używany do
wprowadzania danych numerycznych i typu char, nie stosuje się go w
przypadku zmiennych łańcuchowych. Poniżej zamieszczono program ilustrujący
wykorzystanie funkcji scanf() do wprowadzania zmiennych różnych typów:
#include <stdio.h>
main()
{
char nazwisko[15]
int wiek;
float stan_konta;
char plec;
puts(”Podaj nazwisko:”);
scanf(”%s”,nazwisko);
putchar(‘\n’);
puts(”Podaj wiek:”);
scanf(”%d”,&wiek);
putchar(‘\n’);
puts(”Podaj stan konta:”);
scanf(”%f”,&stan_konta);
putchar(‘\n’);
puts(”Plec: wybierz <m> lub <k>”);
scanf(”%c”,&plec);
putchar(‘\n’);
}
Funkcja printf() służy do wyprowadzania komunikatów i wartości zmiennych
na ekranie. Składnia funkcji printf() jest następująca:
#include <stdio.h>
int printf(const char *format_string, ...);
Argument format_string to łańcuch znaków zawierający specyfikatory formatu
analogiczne jak dla funkcji scanf(). Wielokropek ... oznacza sekcję
przeznaczoną dla ewentualnych wyrażeń, które powinny zostać sformatowane
zgodnie ze specyfikatorami formatu. Przykładem zastosowania funkcji printf()
może
być
fragment
programu
stanowiący
uzupełnienie
programu
przedstawionego powyżej -ilustrującego użycie funkcji scanf().
printf(”Nazwisko: %s\n”,nazwisko);
printf(”Wiek:%d\n”,wiek);
printf(”Stan konta:%f”,stan_konta);
printf(”Plec:%c”,plec);
5.2. Obsługa plików
Wyprowadzenie danych przez program w języku C np. na dyskietkę lub
drukarkę odbywa się poprzez przesłanie ich do specjalnego obszaru pamięci
nazywanego buforem. Aby przeniesienie informacji do i z buforu było możliwe,
musi istnieć łącze komunikacyjne między programem a systemem operacyjnym
komputera. Tym łączem jest plik. Pojęcie pliku w języku C może więc odnosić
się do pliku dyskowego (disk file), urządzenia peryferyjnego typu terminal lub
drukarki. Plik przyporządkowany jest do konkretnego urządzenia, z którym
program dokonuje wymiany informacji. Przed rozpoczęciem tej wymiany
należy otworzyć ten plik, a po zakończeniu wymiany należy koniecznie
zamknąć plik.
Zasób informacji przepływających z programu do pliku lub odwrotnie
nazywany jest strumieniem danych (stream). Strumień danych jest niezależny
od urządzenia (device-independent). Aby przeprowadzić operację Wejścia /
Wyjścia, należy skojarzyć plik ze strumieniem, by dokonywać odczytu danych z
pliku, bądź zapisu danych do pliku, przy czym może to być plik dowolnego
typu. Istnieją dwa formaty strumieni: tekstowy (text stream) i binarny (binary
stream). Strumienie tekstowe są stosowane do przesyłania danych tekstowych z
jednego środowiska do innego, strumienie binarne dotyczą danych nie-
tekstowych wymagających dokładnego przesłania zawartości pliku (z
zachowaniem różnych kodów specjalnych).
Aby zadeklarować plik korzysta się ze składni:
FILE *file_pointer;
Wskaźnik file_pointer jest nazywany wskaźnikiem plikowym i służy on do
odwoływania się do konkretnego pliku dyskowego. Słowo kluczowe file
oznacza, że zmienna wskazuje na strukturę pliku. Deklaracja może dotyczyć
więcej niż jednego pliku, np.:
FILE *plik_wejscia, *plik_wyjscia;
5.2.1. Otwieranie pliku
Otwarcie pliku i skojarzenie go z strumieniem danych dokonywane jest za
pomocą funkcji bibliotecznej fopen(). Funkcja ta wymaga podania dwóch
argumentów: trybu otwarcia pliku oraz nazwy konkretnego pliku,
przeznaczonego do otwarcia. Nazwa pliku musi spełniać konwencje związane z
nazywaniem plików obowiązujące w danym systemie operacyjnym.
Przykładowo, w systemie DOS długość nazwy nie może przekraczać 8 znaków
z opcjonalnym 3-znakowym rozszerzeniem. W celu wydrukowania danych
wyjściowych na drukarce (zamiast wysyłać je do pliku na dysku) należy jako
nazwy pliku użyć "PRN" (w cudzysłowach).
Uogólniony format funkcji fopen() jest następujący:
#include<stdio.h>
FILE *fopen(const char *filename, const char *mode);
gdzie:
filename – nazwa pliku do otwarcia, wskaźnik do stałego łańcucha znaków,
mode – tryb otwarcia pliku.
Poniżej przedstawiono listę dostępnych wartości łańcucha mode:
”r” - otwiera istniejący plik tekstowy, tylko do odczytu (read),
”w” - tworzy nowy plik tekstowy do zapisu (write),
”a” - otwiera istniejący plik tekstowy w trybie dopisywania (append),
”r+” - otwiera istniejący plik tekstowy do odczytu i/lub zapisu,
”w+” - tworzy plik tekstowy do zapisu i/lub odczytu,
”a+” - otwiera lub tworzy plik tekstowy do dopisywania na końcu,
”rb” - otwiera istniejący plik binarny do odczytu,
”wb” - tworzy nowy plik binarny do zapisu,
”ab” - otwiera istniejący plik binarny w trybie dopisywania do końca pliku,
”r+b” - otwiera istniejący plik binarny dla odczytu i/lub zapisu,
”w+b”- tworzy plik binarny do zapisu i/lub odczytu,
”a+b” – otwiera lub tworzy plik binarny do dopisywania na końcu.
Funkcja fopen() zwraca wskaźnik typu FILE. Jeśli wystąpi błąd podczas
otwierania pliku, funkcja zwraca wskaźnik pusty (null pointer);
Fragment programu otwierającego plik tekstowy tylko do odczytu
przedstawiono poniżej:
#include <stdio.h>
main()
{
FILE *plik;
if ((plik=fopen(”DANE.DTA”,”r”))==NULL)
{
printf(”Nie mozna otworzyc pliku \n);
exit(1)
}
}
Funkcja exit() zwraca do systemu operacyjnego wartość. Ponieważ jest to
wartość niezerowa, to do systemu zostaje przekazana informacja o
nieprawidłowym zakończeniu (abnormal termination / program aborted).
Przyczyną powstania błędu podczas otwarcia pliku może być brak miejsca na
dysku, niewłaściwa nazwa pliku lub próba odczytu nie istniejącego pliku.
5.2.2. Zamykanie pliku
Po wykonaniu operacji odczytu z pliku dyskowego, bądź zapisu do pliku
dyskowego należy koniecznie zamknąć plik. Zapewnia to zachowanie danych
aktualnie znajdujących się w buforze. Jeżeli plik nie zostałby prawidłowo
zamknięty w chwili odłączenia wskazanego strumienia danych od pliku, to
część danych może zostać bezpowrotnie utracona. Zamknięcia pliku wraz z
czyszczeniem bufora (flush) dokonuje funkcja biblioteczna fclose().
Uogólniony format funkcji fclose() jest następujący:
#include <stdio.h>
int fclose(FILE *file_pointer)
gdzie:
file_pointer – oznacza wskaźnik skojarzony ze strumieniem do otwartego pliku.
Jeśli operacja zamknięcia pliku powiedzie się, funkcja zwraca 0, w przeciwnym
razie zwraca kod EOF.
5.2.3. Funkcje zapisu i odczytu
Do odczytu danych z pliku i zapisu do pliku służą odpowiednio standardowe
funkcje biblioteczne fgetc() i fputc(). Składnia funkcji fgets() jest następująca:
#include <stdio.h>
int fgetc(FILE *plik);
gdzie:
plik – oznacza wskaźnik do pliku, który jest skojarzony ze strumieniem danych.
Funkcję getc() można wykorzystać do odczytu z pliku danych w trybie znak-po-
znaku.
Uogólniona składnia funkcji putc() jest następująca:
#include <stdio.h>
int fputc(int c, FILE *plik);
gdzie:
int c – oznacza wartość numeryczną – kod znaku c.
plik – oznacza wskaźnik do pliku, który jest skojarzony ze strumieniem danych.
Funkcja zwraca wyprowadzony do pliku znak, jeśli operacja zakończyła się
sukcesem, w przeciwnym razie zwrócony zostanie kod EOF. Po dokonaniu
zapisu znaku do pliku funkcja dodatkowo przesuwa o 1 pozycję w przód
skojarzony z danym plikiem znacznik pozycji.
Poniżej zamieszczono program, który otwiera istniejący na dysk plik tekstowy o
nazwie „Dane.dta”, wczytuje z niego kolejne znaki (aż do napotkania znaku
EOF) i zapisuje je do nowo utworzonego pliku o nazwie „Wyniki.wyn”.
#include <stdio.h>
main()
{
FILE *plik1,*plik2
char nazwa_pliku1[]=”dane.dta”;
char nazwa_pliku2[]=”wyniki.wyn”;
char znak;
if ((plik1=fopen(nazwa_pliku1,”r”))==NULL
{
printf (”Nie mo
ż
na otworzyc pliku danych \n”);
exit(1);
}
else
if ((plik2=fopen(nazwa_plik2,”w”))=NULL
{
printf(”Nie mo
ż
na utworzyc pliku wynikow \n);
exit(1);
}
else while((znak=fgetc(plik1))!=EOF)
{
putchar(znak); /*wydruk znaku na ekran */
fputc(znak,plik2);
}
fclose(plik1);
fclose(plik2);
return(0);
}
Do odczytu z pliku i zapisu w pliku jednocześnie całego wiersza tekstu służą
funkcje biblioteczne fgets() i fputs().
Uogólniona składnia funkcji fgets() jest następująca:
#include<stdio.h>
char *gets(char *s,int n, FILE *plik);
gdzie:
s- referencja do tablicy znakowej uzywanej do zapamiętania znaków
wczytanych z otwartego pliku wskazywanego przez wskaźnik,
n – maksymalna ilość elementów tablicy znakowej.
Funkcja zwraca wskaźnik char *s jeśli operacja wczytania zakończyła się
sukcesem. Jeżeli napotkany zostanie kod EOF, funkcja zwraca pusty wskaźnik i
pozostawia tablicę w stanie poprzednim. Jeżeli wystąpi błąd wczytywania
funkcja zwraca pusty wskaźnik, ale zawartość tablicy jest nieokreślona.
W przypadku napotkania znaku przejścia do nowego wiersza (‘\0’) funkcja
włącza go do wyjściowej tablicy znakowej.
Uogólniona składnia funkcji fputs() jest następująca:
#include <stdio.h>
int fputs(const char *s, FILE *plik);
gdzie: s – wskazuje tablicę znakową, w której znajduje się tekst przeznaczony
do zapisu w pliku dyskowym skojarzonym ze wskaźnikiem plikowym *plik.
Modyfikator const wskazuje, że zawartość tablicy znakowej wskazywanej przez
identyfikator s nie może się zmieniać. W przypadku błędu funkcja zwraca
wartość niezerową, w przypadku sukcesu – zero.
Poniżej zamieszczono program, którego wynik działania jest podobny do efektu
działania programu zamieszczonego poprzednio. Otwarty zostaje istniejący na
dysku plik tekstowy o nazwie „Dane.dta”, a następnie wczytywane są z niego są
kolejne linie tekstu (o długości maksymalnej 81 znaków), aż do napotkania
znaku EOF. Poszczególne wiersze zostają zapisane w nowo utworzonym pliku o
nazwie „Wyniki.wyn”.
#include <stdio.h>
#define linia 81;
main()
{
FILE *plik1,*plik2
char nazwa_pliku1[]=”dane.dta”;
char nazwa_pliku2[]=”wyniki.wyn”;
char tablica[linia];
if ((plik1=fopen(nazwa_pliku1,”r”))==NULL
{
printf (”Nie mo
ż
na otworzyc pliku danych \n”);
exit(1);
}
else
if ((plik2=fopen(nazwa_plik2,”w”))=NULL
{
printf(”Nie mo
ż
na utworzyc pliku wynikow \n);
exit(1);
}
else while((fgets(tablica,linia,plik1))!=NULL)
{
printf(”%s”,tablica); /*wydruk znaku na ekran */
fputs(tablica,plik2);
}
fclose(plik1);
fclose(plik2);
return(0);
}
Istnieje mozliwość jednorazowego odczytania i zapisania bloku danych z/do
pliku dyskowego. Do wykonania blokowych operacji wejścia/wyjścia służą
funkcje biblioteczne fread() i fwrite().
Uogólniona składnia funkcji fread() jest następująca:
#include <strdio.h>
size_t fread(void *ptr, size_t size, size_t n,FILE *plik);
gdzie ptr oznacza wskaźnik do tablicy, w której są przechowywane dane
wczytane. Liczba size określa rozmiar każdego elementu tablicy, n określa
liczbę elementów do odczytu, a plik oznacza wskaźnik skojarzony z plikiem
otwartym do odczytu. Typ size_t to typ numeryczny całkowity zdefiniowany w
pliku nagłówkowym stdio.h. Funkcja fread() zwraca liczbę elementów, które
wczytała.
Uogólniona składnia funkcji fwrite() jest następująca:
#include <strdio.h>
size_t fwrite(void *ptr, size_t size, size_t n,FILE *plik);
gdzie:
ptr oznacza wskaźnik do tablicy, w której przechowywane są dane, które należy
zapisać do otwartego pliku wskazywanego przez wskaźnik plik. Parametr size
określa wielkość elementów tablicy. Argument n określa ilość elementów
tablicy, które należy zapisać do pliku. Funkcja zwraca liczbę rzeczywiście
zapisanych do pliku elementów.
Stosując funkcje fread(), fwrite() można zapisywać w plikach dowolne dane.
Najczęściej z ich wykorzystaniem zapisuje się struktury.
Ponieważ obliczenie parametru size może być kłopotliwe, można skorzystać z
funkcji bibliotecznej sizeof().
Wykonanie funkcji
size(structure_variable)
spowoduje obliczenie
wielkości struktury automatycznie.
Poniżej przedstawiono program ilustrujący sposób zapisu i odczytu struktury u z
wykorzystaniem funkcji fwrite() i fread().
#include <stdio.h>
main()
{
FILE *plik;
int nr, lista_startowa;
struct zawodnicy
{
int numer;
char kraj[10];
char nazwisko[20];
float dlugosc_skoku;
} zawodnik;
char nazwa_pliku;
printf(”Ilu skoczkow startowalo ?”);
scanf(”%d,&lista_startowa);
printf(”Podaj nazwe pliku, który chcesz utworzyc: ”);
gets(nazwa_pliku);
if ((plik=fopen(nazwa_piku,”w”))==NULL)
{
printf(”Nie mo
ż
na otworzyc pliku \n”);
exit();
}
for(nr=1;nr<=lista_startowa;nr++)
{
zawodnik.numer=nr;
printf(”Podaj dane skoczka o numerze : %d”,&nr);
printf(”Kraj : ”);
gets(zawodnik.kraj);
printf(”Nazwisko skoczka: );
gets(zawodnik.nazwisko);
printf(”Dlugosc skoku:”);
scanf(%f,&zawodnik.dlugosc_skoku);
fwrite(&zawodnik, sizeof(zawodnik), 1, plik);
}
fclose(plik);
while(fread(&zawodnik, sizeof(zawodnik), 1, plik)==1)
{
printf(”Numer : %d\n”,zawodnik.numer);
printf(”Kraj: %s\n”,zawodnik.kraj);
printf(”Nazwisko: %s\n”,zawodnik.nazwisko);
printf(”Dlugosc skoku: %f\n”,zawodnik.dlugosc_skoku);
}
fclose(plik);
}
Odczyt lub zapis pliku odbywa się zawsze począwszy od bieżącego położenia
tzw. wskaźnika pliku. Położenie tego wskaźnika można zmieniać funkcją
fseek():
int fseek(FILE *plik, long offset, int relation);
gdzie:
plik – oznacza wskaźnik plikowy skojarzony z otwartym plikiem,
offset – oznacza przesunięcie w bajtach od punku określonego przez trzeci
argument,
relation – punkt odniesienia, od którego rozpoczyna się odliczanie.
Przykładowo, w celu przesunięcia wskaźnika pliku o pięć rozmiarów zmiennej
zawodnik względem początku pliku, czyli ustawienia go na szóstym zapisie tego
typu należy wykonać instrukcje:
fseek(plik, (long) (5*sizeof(zawodnik)), 0);
Aby odczytać bieżącą znacznika w pliku, można w każdej chwili posłużyć się
funkcją biblioteczną ftell(). Składnia funkcji ftell() jest następująca:
#include <stdio.h>
long ftell (FILE *plik)
Funkcja zwraca bieżącą wartość pozycji wskaźnika w pliku liczoną do bieżącej
pozycji w bajtach od początku pliku.
Przydatną w praktyce jest funkcja rewind(), która cofa znacznik na sam
początek pliku. Składnia tej funkcji jest następująca:
#include <stdio.h>
void rewind (FILE *plik)
Ostatnią parę funkcji odczytu/zapisu danych tworzą funkcje fscanf() i fprintf().
Pośród funkcji bibliotecznych C obsługujących operacje wejścia/wyjścia są one
odpowiednikami funkcji scanf() i printf(), ale dodatkowo pozwalają
programiście wybrać strumień danych wejścia i wyjścia. Ponieważ ich składnia
różni się tylko pierwszym argumentem oznaczającym wskaźnik plikowy
skojarzony z otwartym plikiem od składni funkcji omówionych wcześniej, nie
będą one szczegółowo omawiane.
5.3. Manipulowanie łańcuchami znakowymi
Funkcje operujące na łańcuchach znaków zostały zestawione w pliku
nagłówkowym string.h. Standard języka ANSII C zawiera następujące funkcje
biblioteczne obsługujące łańcuchy:
Funkcja
Opis
strcat
Dodaje znaki jednego łańcucha na końcu innego
strchr
Oblicza położenie wybranego znaku w łańcuchu
strcmp
Porównuje dwa łańcuchy
strcmpl
Porównuje dwa łańcuchy bez rozróżniania wielkich i
małych liter
strcpy
Kopiuje jeden łańcuch do innego
strdup
Tworzy kopię łańcucha
strlen
Oblicza długość łańcucha
strlwr
Zamienia litery w łańcuchu na małe
strncat
Dołącza na końcu jednego łańcucha wybrane znaki z
innego łańcucha
strncmp
Porównuje wybrane znaki w dwóch łańcucach
strncpy
Kopiuje wybrane znaki z łańcucha do łańcucha
strnset
Zamienia wybrane znaki w łańcuchu na inne
strrev
Odwraca kolejność znaków w łańcuchu
strstr
Odnajduje łańcuch w innym łańcuchu
strupr
Zamienia litery w łańcuchu na wielkie
Poniżej zamieszczono program, który tłumaczy sposób stosowania niektórych
funkcji bibliotecznych manipulowania łańcuchami znakowymi.
#include <stdio.h>
#include <string.h>
main()
{
char imie1[]={‘A’,’D’,’A’,’S’,’ ‘,’\0’};
char imie2[]=”Andrzej ”;
char *nazwisko1=”Kysiak”;
char nazwisko2[];
printf(”Dlugosc lancucha=%d”,strlen(imie1));
strcpy(nazwisko2,nazwisko1); /*kopiowanie lancucha*/
strlwr(imie1); /*zamienia litery lancucha na male*/
imie1[1]=imie2[1];
strcat(imie1,nazwisko1); /*laczy dwa lancuchy*/
strcat(imie2,nazwisko2);
printf(”%s\n”,imie1);/*wyswietla napis: Adas Kysiak*/
printf(„%s\n,imie2); /*napis: Andrzej Kysiak*/
return 0;
}
5.4. Funkcje matematyczne
Wszystkie funkcje matematyczne zostały zamieszczone w pliku nagłówkowym
math.h, który należy dołączyć do programu dyrektywą #include. Najczęściej
stosowane funkcje matematyczne należą do grupy funkcji trygonometrycznych i
hiperbolicznych oraz do grupy funkcji wykładniczych i logarytmicznych.
5.4.1. Funkcje trygonometryczne
Uogólniony format dla funkcji trygonometrycznych jest następujący:
#include <math.h>
double sin(double x);
double cos(double x);
double tan(double x);
Argumentem funkcji trygonometrycznej jest zmienna x o wartości typu double,
która wyraża kąt mierzony w radianach. Funkcje zwracają wartość również typu
double.
Przykład stosowania funkcji trygonometrycznych przedstawia program:
#include <math.h>
#define PI 3.141593
main()
{
double x;
printf(”Podaj wartosc kata w stopniach:”);
scanf(”%d,&x);
x*=PI/180.0; /*zamiana stopni na radiany*/
printf(”sinus kata %f wynosi: %f.\n”,x,sin(x));
printf(”kosinus kata %f wynosi: %f.\n”,x,cos(x));
printf(”sin hiperb. kata%f wynosi: %f.\n”,x,sinh(x));
return 0;
}
5.4.2. Funkcje wykładnicze i logarytmiczne
Najczęściej stosowane funkcje wykładnicze to: pow(),exp() i sqrt(). Składnia tyc
funkcji jest następująca:
#include <math.h>
double pow(double a, double b);
double exp(double b)’
double sqrt(double x);
gdzie: a – to podstawa w operacji potęgowania, b to wykładnik potęgi,
natomiast x to liczba podpierwiastkowa. Funkcja pow() wykonuje operację
podnoszenia do potęgi a
b
, funkcja exp() wykonuje operację e
b
, natomiast funkcja
sqrt() zwraca pierwiastek kwadratowy ze swojego nieujemnego argumentu x (w
przeciwnym przypadku zwraca błąd). Wyniki zwracane przez funkcje są w
formacie double.
Funkcje logarytmiczne log() i log10() wykonują operacje wyznaczania
logarytmów odpowiednio: naturalnego i dziesiętnego dla argumentu o wartości
typu double. Poniżej zamieszczono program ilustrujący sposób użycia
omawianych funkcji:
#include <stdio.h>;
#include <math.h>
main()
{
double x10 = 100.0;
double e=2.71828;
double wynik,wynik10;
wynik=log(e);
wynik10=log10(x10);
printf("Log natur. z e=%f wynosi %f\n",e,wynik);
printf("Log. dzies. z %f wynosi %f\n",x10,wynik10);
printf("e^x dla 1 wynosi %f\n",2.0,exp(1));
return 0;
}
5.5. Funkcje czasu i daty
Wszystkie funkcje odnoszące się do daty i czasu są zawarte w pliku
nagłówkowym time.h. Funkcje te mogą dać trzy rodzaje danych odnoszących
się do daty i czasu:
czas kalendarzowy – calendar time
czas lokalny – local time
czas zimowy/ letni – daylights savings time.
Funkcja time() w języku C zwraca czas kalendarzowy. Składnia funkcji jest
następująca:
#include <time.h>
time_t time(time_t *timer);
gdzie: time_t jest to tzw. typ arytmetyczny stosowany do zapisu czasu,
natomiast timer to zmienna typu wskaźnik – wskazująca miejsce w pamięci, w
którym powinny zostać zapamiętane dane dotyczące daty i czasu zwrócone
przez funkcję.
Funkcja localtime() zwraca czas lokalny po przekształceniu czasu
kalendarzowego.
#include <time.h>
struct tm *localtime(time_t *timer);
gdzie:
tm
oznacza
strukturę
danych,
zawierającą
składniki
czasu
kalendarzowego. Struktura ta jest zdefiniowana następująco:
struct tm {
int tm_sec; /* sekundy [0-59] */
int tm_min; /* minuty [0-59] */
int tm_hour; /* godziny [0-23] */
int tm_mday; /* dzien [1-31] */
int tm_mon; /* miesiac[0-11] ,
0 – stycze
ń
, itd. */
int tm_year; /* rok (od 1900) */
int tm_wday; /* dzien tygodnia[0-6],
0 – niedziela, itd. */
int tm_yday; /* dzien w roku [0-365] */
int tm_isdst;/* znacznik trybu liczenia godzin:
0 – dwunastogodzinny (am i pm)
1 - dwudziestoczterogodzinny] */
};
Aby przekształcić dane dotyczące daty i czasu przechowywane w strukturze tm
w łańcuch znaków – wygodny i bardziej czytelny dla użytkownika, należy
wywołać funkcję asctime() dokonującą konwersji na łańcuch znaków ASCII.
Uogólniona składnia tej funkcji jest następująca:
#include<time.h>
char *asctime(const struct tm *timeptr);
gdzie: timeptr oznacza wskaźnik odwołujący się do struktury tm zwróconej
przez funkcję localtime(). Funkcja asctime() zamienia te dane na łańcuch
znaków i zwraca wskaźnik do tego łańcucha. Przykład odczytu aktualnej daty i
czasu przedstawia program:
#include <time.h>
#include <stdio.h>
main()
{
time_ t czas;
time(&czas);
printf(„Aktualna data i czas: %s”,
asctime(localtime(&czas)));
}
5.6. Inne przydatne funkcje biblioteczne
Z bogatej biblioteki języka C wybrano kilka, które są używane w większości
programów:
exit() – powoduje zakończenie pracy programu. Zwykle funkcję tę wywołuje się
w sytuacjach błędnych, po wyświetleniu odpowiedniego komunikatu.
rand() – to generator liczb pseudolosowych. Funkcja zwraca wartość losową
typu int z zakresu od 0 do liczby max zadanej jako argument funkcji. Funkcja
srand() ustawia wartość początkową dla generatora.
qsort(ptr,cnt,size,fun) – powoduje sortowanie danych tworzących tablicę o cnt
elementach, której pierwszy bajt jest wskazywany przez ptr. Rozmiar elementu
jest określony przez size, a funkcja porównująca przez fun. Funkcja skojarzona z
parametrem fun musi być tak dobrana, aby dla elementów elm1 i elm2
rezultatem wywołania:
(*fun)(&elm1,&elm2)
była dana typu int o wartości –1 , 0 albo 1, zgodnie z następującym
zestawieniem:
-1 jeśli elm1<elm2
0 jeśli elm1=elm2
+1 jeśli elm1>elm2
6. Metodyka programowania strukturalnego
Przedstawione w poprzednich rozdziałach elementy programów w języku C :
jednostki leksykalne, instrukcje i funkcje umożliwiają tworzenie programów
komputerowych. Najlepszą metodą tworzenia programów wykorzystującą
specyfikę języka C jest tzw. programowanie strukturalne. Istnieją dwie
metodyki programowania strukturalnego:
•
metoda Top-Down, czyli „od góry w dół” oraz
•
metoda Bottom-Up, czyli „od dołu w górę”.
Opierając się na metodzie „bottom – up” należy rozpracowując jakiś problem
rozwiązywać możliwie jak najmniejsze elementy tego zadania. W tym celu
definiujemy i opracowujemy funkcje realizujące każde spośród takich zadań
cząstkowych. Po tym, jak każda z tych funkcji zostanie napisana, uruchomiona i
przetestowana – zaczynamy je łączyć razem tworząc program, który rozwiązuje
problem metodą zadań cząstkowych.
W metodzie top-down postępujemy odwrotnie. Tworzony jest główny program,
tzn. piszemy kod dla funkcji main(), deklarując jedynie funkcje, które mają
rozwiązywać kolejne zadania (ale jeszcze ich nie definiując). Dopiero po
opracowaniu struktury programu głównego przechodzi do definiowania
poszczególnych funkcji.
W praktyce często stosuje się postępowanie będące połączeniem tych dwóch
metod. Jednak zawsze działanie to oparte jest zasadzie podziału problemu na
zagadnienia cząstkowe - funkcje realizujące kolejne zadania. Dla pełnego
zrozumienia reguł stosowania funkcji w programowaniu w języku C konieczne
są jeszcze pewne wyjaśnienia związane z przekazywaniem parametrów i
zwracaniem wartości przez funkcje. Omówienia wymagają również pojęcia
zmiennych lokalnych i globalnych (zewnętrznych).
6.1. Zasady definiowania i korzystania z funkcji
Własne zdefiniowane funkcje umieszczamy po nawiasie zamykającym funkcji
main(). Każda definicja funkcji musi zawierać następujące elementy:
typ_danych_funkcji nazwa_funkcji (typ_danych1 argument1, ...)
{ /* nawias otwierający */
/* instrukcje tworzące tzw. ciało funkcji*/
return ( )
} /* nawias zamykający */
Przed pierwszym użyciem w programie funkcja powinna być zdefiniowana lub
co najmniej zadeklarowana. Deklaracja funkcji określa interpretację i atrybuty
zestawu zmiennych funkcji, definicja funkcji natomiast wymaga od kompilatora
zarezerwowania pamięci dla danej funkcji. Deklaracja funkcji odwołuje się do
definicji tej funkcji, która znajduje się gdzieś, w innym miejscu, określa
natomiast jakiego typu wartość zostanie zwrócona przez tę funkcję. Definicja
określa czynności wykonywane przez funkcję podając jednocześnie liczbę i typy
argumentów przekazywanych do funkcji.
Zadeklarowanie funkcji nie jest równoznaczne z jej definicją. Jeśli definicja
funkcji jest umieszczona w tekście programu przed miejscem pierwszego
wywołania tej funkcji, nie trzeba deklarować tej funkcji. W przeciwnym razie
należy przed pierwszym jej wywołaniem umieścić w programie jej deklarację.
Funkcja może być zadeklarowana i zwracać dane dowolnego typu, za wyjątkiem
tablicy. Wartość zwracana przez użycie funkcji return() musi odpowiadać
zadeklarowanemu typowi funkcji. Jeżeli przed nazwą funkcji w deklaracji nie
zapisano typu funkcji, to domyślnie przyjmowany jest typ liczby całkowitej.
Dla zadeklarowania funkcji, która pobiera zmienną liczbę argumentów, należy
określić co najmniej pierwszy argument, a następnie użyć wielokropka,
reprezentującego resztę argumentów przekazywanych do funkcji.
W deklaracji funkcji, która nie pobiera żadnych argumentów, konieczne jest
wskazanie typu danych void.
6.2. Zmienne lokalne i globalne
W programie składającym się z wielu funkcji występują zmienne, które
wykorzystywane są przez kilka funkcji oraz zmienne używane tylko przez
pojedyncze funkcje. Widoczność i dostępność tych zmiennych może być
ograniczona, a wartości przypisane tym zmiennym mogą być ukryte z punktu
widzenia wielu innych funkcji.
Jako zmienne globalne (program scope) przyjęto nazywać takie zmienne, które
można wykorzystywać w każdej funkcji programu. Zmienne, które są dostępne i
aktywne tylko pomiędzy początkiem, a końcem danej funkcji nazywa się
lokalnymi (function scope). Zasięg zmiennych globalnych obejmuje cały
program jeżeli są one zadeklarowane poza funkcją main(). Jeśli program będzie
składał się z wielu różnych plików to zmienne te będą również widoczne także
w tych pozostałych plikach. Taka grupa wielu plików dyskowych tworzących
razem zbiór plików źródłowych dla jednej aplikacji nazywa się projektem.
Zmienna lokalna jest zadeklarowana wewnątrz odrębnego bloku programowego.
W przypadku gdy zmienna jest zadeklarowana wewnątrz pewnej funkcji jej
zasięg obejmuje tylko ciało tej funkcji. Blok programowy może być określony
poprzez parę nawiasów klamrowych. W takim przypadku zmienna jest dostępna
od wiersza, w którym została umieszczona jej deklaracja – do końca bloku.
Zmienna lokalna istnieje tylko wtedy, kiedy jest wykonywana funkcja, w której
ją zadeklarowano. W chwili gdy rozpoczyna się wykonywanie funkcji, zmiennej
tej zostaje przydzielone miejsce w pamięci. Kiedy funkcja się kończy, pamięć
zajmowana przez zmienną jest zwalniana i zmienna przestaje istnieć. Można
uniknąć utraty wartości zmiennej przy zakończeniu wykonywania funkcji,
deklarując zmienną jako statyczną. Deklaracja przyjmuje wówczas postać:
moja_funkcja()
{
static int zmienna;
}
Zmienna statyczna jest przypisana do stałego adresu w pamięci, tak długo, jak
długo trwa wykonywanie programu. Kiedy funkcja się kończy, adres nie zostaje
zwolniony, lecz jest dalej przypisany do zmiennej. Pozwala to na odzyskanie
wartości, która znajdowała się pod tym adresem, mimo że funkcja się
zakończyła.
Ponieważ zmienna lokalna jest ważna tylko wewnątrz funkcji, w której została
zadeklarowana, to można korzystać z nazwy tej zmiennej w innych funkcjach.
Jeżeli zmienna globalna i lokalna noszą taką samą nazwę, to w funkcji jest
wykorzystywana zmienna lokalna. Niemożliwe jest wykorzystywanie obydwu.
Wartość zmiennej globalnej jest zatem przesłonięta przez wartość zmiennej
lokalnej.
6.3. Przekazywanie parametrów do funkcji
Przy przekazywaniu parametrów do definiowanej funkcji można wyróżnić trzy
przypadki:
•
funkcja jest bezargumentowa,
•
funkcja pobiera stałą, określoną liczbę argumentów,
•
funkcja pobiera zmienną liczbę argumentów.
Dla wszystkich funkcji bezargumentowych w ich deklaracji stosowany jest typ
void, umieszczony w nawiasach. Przy wywołaniu w programie takiej funkcji
pomiędzy nawiasy nie można wówczas wstawić żadnego argumentu.
Deklaracja funkcji o stałej liczbie argumentów należy podać typy wszystkich
argumentów i ich nazwy. W wywołaniu takiej funkcji liczba przekazywanych
argumentów i ich typy muszą być zgodne z zadeklarowanymi. Kompilator
sprawdza poprawność i zgodność pomiędzy deklaracją i definicją funkcji oraz
wartościami przekazywanych argumentów w wywołaniu funkcji. Należy
również
zwracać
uwagę
na
zachowanie
odpowiedniej
kolejności
przekazywanych wartości argumentów.
W języku C możliwe jest również deklarowanie prototypów funkcji o zmiennej
liczbie argumentów. Ogólny format deklaracji funkcji o zmiennej liczbie
argumentów jest następujący:
typ_danych nazwa_funkcji( typ_danych1 argument1, ...);
Wielokropek oznacza pozostałe argumenty, które nie zostały wymienione na
liście argumentów.
W pliku nagłówkowym stdarg.h zadeklarowano zostały trzy podprogramy
(routines), które pozwalają na tworzenie i obsługę funkcji o zmiennej liczbie
argumentów. Są to następujące makrorozkazy:
va_start()
va_arg()
va_end()
W pliku tym zdefiniowano również typ danych va_list, który określa typ tablicy
odpowiedni
do
przechowywania
danych
potrzebnych
do
działania
makrorozkazów.
Składnia makra va_start() obowiązuje składnia:
#include <stdarg.h>
void va_start(va_list tab, lastfix);
gdzie: tab – oznacza nazwę tablicy, która będzie zainicjowana w wyniku
działania makropolecenia va_start(). Identyfikator lastfix określa ostani
argument funkcji przed wielokropkiem w deklaracji funkcji.
Makropoloecenie va_arg() używane jest do pobrania kolejnego argumentu
przekazywanego do funkcji. Wywołania makra odbywa się wg. składni:
#include <stdarg.h>
void va_arg(va_list tab);
Aby zapewnić normalny i poprawny zwrot wartości przez funkcję o zmiennej
liczbie argumentów, należy zastosować makro va_end() po tym, jak wszystkie
argumenty zostaną przetworzone. Przykład zastosowania funkcji o zmiennej
liczbie argumentów przedstawia następujący program:
#include <stdio.h>
#include <stdarg.h>
double dodaj_liczbe(int x, ...);
main()
{
double d1=1.5;
double d2=2.5;
double d3=3.5;
puts(”Dodawanie zmiennej liczby argumentow”);
printf(”Dany jest argument : %2.1f\n”,d1);
printf(”Wynik dodawania:%2.1f\n”,dodaj_liczbe(1,d1));
printf(”Dane sa argumenty: %2.1f, %2.1f\n”,d1,d2);
printf(”Wynik:%2.1f\n”,dodaj_liczbe(2,d1,d2));
printf(”Dane sa argumenty: %2.1f, %2.1f, %2.1f\n”,
d1,d2,d3);
printf(”Wynik: %2.1f\n”,dodaj_liczbe(3,d1,d2,d3));
return();
}
double dodaj_liczbe(int x, ...);
{
va_list lista;
int i;
double wynik=0.0;
va_start(lista,x);
for (i=0;i<x;i++)
wynik+=va_arg(lista,double);
va_end(lista);
return(wynik);
}
7. Podstawy języka C++
7.1. Rozszerzenia C++ w stosunku do języka C
Język C++ jest obiektową pochodną języka C. Można przyjąć, że język jest
podzbiorem języka C++. Każdy program napisany w standardzie ANSI C jest
także programem w C++. Język C++ posiada wszystkie te funkcje jakie posiadał
standard języka C, ale wprowadza szereg nowych pojęć i związanych z tym
możliwości. Najważniejsze innowacje wprowadzone w języku C++ są
następujące:
•
wprowadzenie obiektów zdefiniowanych jako klasy zawierające nie tylko
definicje danych, które można w strukturach C, ale także deklaracje i
definicje funkcji, które działają na tych danych.
•
egzemplarze klas mogą być automatycznie inicjalizowane i usuwane za
pomocą konstruktorów i destruktorów.
•
dane zdefiniowane w klasie domyślnie są dostępne tylko funkcjom
składowym tej klasy. Kod zewnętrzny, który używa tej klasy, nie ma wpływu
na wewnętrzną implementację klasy.
•
dopuszczalne jest tzw. przeciążanie operatorów i funkcji. Oznacza to, że
można zdefiniować więcej niż jedną funkcję z tą samą nazwą, a dla danego
wywołania funkcji kompilator zidentyfikuje właściwą definicję.
•
charakterystyki typu klasy – dane i funkcje – mogą być dziedziczone przez
podklasy, nazywane także klasami pochodnymi.
•
C++ pozwala klasom na zdefiniowanie funkcji wirtualnych. Oznacza to
istnienie więcej niż jednej definicji funkcji, a decyzja o wyborze właściwej
funkcji jest podejmowana w trakcie pracy programu (tzw. polimorfizm).
•
mogą być zdefiniowane klasy szablonu, co pozwala na używanie różnych
egzemplarzy tej samej klasy z różnymi typami danych, ale przy nie
zmienionym kodzie.
•
w C++ deklaracje nie muszą występować na pierwszym miejscu w funkcji,
ale mogą znajdować się pomiędzy innymi instrukcjami. Na przykład zmienna
x może być zadeklarowana i zainicjalizowana jako część instrukcji for:
include <iostream.h>
/*plik iostream.h to w C++ alternatywa stdio.h */
int main()
{
for (int x=5;x<10;x++);
return(0);
}
•
w programach C++ każda funkcja, która jest wywoływana przed swoją
definicją, musi być zadeklarowana za pomocą prototypu przed wywołaniem.
W języku ANSI C nie było to konieczne (chociaż zalecane dla funkcji, któ®e
zwracały wartość typu int).
•
wprowadzono różnicę w stosunku do ANSCI C w interpretacji prototypu
funkcji typu :
int funkcja();.
W standardzie języka C zapis ten
oznaczał, że
funkcja()
pobiera zero lub większą liczbę parametrów, w
języku C++ oznacza, że funkcja nie pobiera żadnych parametrów.
•
język C++ dostarcza mechanizm wywołania funkcji przez referencję. Użycie
typu referencyjnego zmiennej w wywoływanej funkcji powoduje, że zmiana
wartości zmiennej przekazywanej do danej funkcji powoduje również efekt
zmiany wartości tej zmiennej w funkcji wywołującej, np.:
#include <iostream.h>
void funkcja(int&);
int main()
{
int x=5;
funkcja(x);
printf(”x=%d,x); /*wyswietla x=10*/
return(0);
}
void funkcja(int& x_ref);
{
xref=10;
}
•
w języku C wprowadzono kilka nowych słów kluczowych:
class
private
delete
protected
friend
public
inline
template
new
virtual
operator
śaden z używanych identyfikatorów zmiennych użytych w programie C++
nie może przyjmować nazwy zgodnej z jednym z wymienionych słów
kluczowych.
•
opracowano nową bibliotekę plikowego wejścia/wyjścia będącą alternatywą
dla standardowej biblioteki C w zdefiniowanej i zapisanej w pliku
nagłówkowym stdio.h.
7.2. Klasy i programowanie obiektowe
Pojęcie klasy wprowadzone w języku C++ jest uogólnieniem konstrukcji
struktury istniejącej w języku C. Podobnie jak struktura, klasa również posiada
składowe, które w odróżnieniu od struktury mogą być również funkcjami. W
sensie praktycznym pojęcie klasy łączy w sobie dane definiujące „przedmiot” i
metody określania jego właściwości. W ten sposób następuje integracja danych i
metod uprawnionych do ich wykorzystania. Przykładem deklaracji klasy może
być klasa KWADRAT, która zawiera zarówno dane definiujące kwadrat jak i
funkcję pozwalającą na jej narysowanie:
class KWADRAT
{
int wsp_x[4],wsp_y[4];
public:
void Narysuj(void);
};
Taki sposób opisu „przedmiotu” jest charakterystyczny dla tzw. programowania
obiektowego. Pozwala ono na tworzenie programowych modeli przedmiotów
poprzez łączenie danych i metod uprzywilejowanych do ich wykorzystania.
Przedmioty – obiekty stają się głównym przedmiotem zainteresowania
programisty i jednocześnie podstawowym elementem konstrukcji programu.
Ogólna postać definicji klasy jest następująca:
słowo_kluczowe_klasy identyfikator_klasy
ciało_definicji_klasy
Definicja klasy rozpoczyna się od słowa_kluczowego_klasy, którym może być:
class, struct lub union. W ciele definicji klasy mogą znaleźć się:
- deklaracje danych składowych,
- deklaracje funkcji składowych,
- definicje funkcji składowych.
Wymienione elementy rozmieszcza się w sekcjach definicji klasy wg. schematu:
class Klasa
public:
...
/*funkcje i dane publiczne*/
protected:
...
/*funkcje i dane składowe zabezpieczone */
private:
...
/*funkcje i dane składowe prywatne */
};
W sekcji prywatnej (private) składowe są widoczne tylko w obrębie funkcji
składowych danej klasy. Dane definiuje się jako prywatne wtedy, gdy zmiana
wartości składowej przez niepowołaną do tego funkcję zewnętrzną jest
ryzykowna lub zmianie tej powinno towarzyszyć wykonanie ściśle określonych
czynności. Funkcje definiuje się jako prywatne gdy ich wywołanie przez funkcje
zewnętrzne jest niewskazane lub gdy realizują pewien fragment algorytmu i ich
wywołanie z zewnątrz nie ma sensu.
W sekcji zabezpieczonej (protected) składowe są widoczne tylko w obrębie
funkcji składowych danej klasy i klas wyprowadzonych.
Składowe w sekcji publicznej (public) są widoczne w obszarze całego pliku.
Publiczne funkcje składowe służą zwykle do sterowania, zmiany parametrów i
komunikacji z obiektem. Dane składowe definiuje się jako publiczne w
przypadku prostych klas lub gdy zmiana zawartości pola nie musi być związana
z wykonaniem jakiś dodatkowych czynności.
Oprócz funkcji składowych definiowanych przez programistę w celu
opracowania jakiegoś obiektu łączącego w sobie dane „przedmiotu” i jego
właściwości, istnieją dwie dodatkowe funkcje specjalne: konstruktor i
destruktor. Funkcje te są albo definiowane albo generowane przez kompilator
(w przypadku ich braku). W C++ za pomocą konstruktorów i destruktorów
wykonywana jest automatyczna inicjalizacja i usuwanie obiektów.
Konstruktor to funkcja składowa klasy, która inicjuje zmienne tej klasy. Nazwa
funkcji konstruktora jest zawsze taka sama, jak nazwa klasy. Destruktor jest
funkcją składową klasy, która wykonuje operacje porządkujące, zanim
egzemplarz klasy ulegnie samozniszczeniu. Nazwa funkcji destruktora jest taka
sama, jak nazwa klasy z przedrostkiem w postaci znaku tyldy (~).
Funkcja konstruktora wywoływana jest jako część definicji egzemplarza klasy.
Destruktor wywoływany jest niejawnie, ale automatycznie, gdy zmienna
opuszcza zasięg. Poniżej przedstawiono definicję klasy z uwzględnieniem
użycia konstruktora i destruktora:
class konto_bankowe
{
private:
char nazwisko[30];
char adres[50];
double stan_konta;
public:
konto_bankowe();
~konto_bankowe();
void wplata(double);
void wyplata(double);
void dopisz_procenty(double);
void zmiana_danych();
};
7.2.1. Przeciążanie
Język C++ zawiera dwa rodzaje przeciążania : przeciążanie funkcji i
przeciążanie operatora. Używając przeciążania funkcji, można korzystać z
więcej niż jednej wersji funkcji z tą samą nazwą. Stosując przeciążanie
operatora, można nadawać nowe znaczenie standardowym operatorem C++.
W przykładzie zdefiniowane poprzednio klasy konto_bankowe można
wprowadzić zmiany w celu dołączenia przeciążonej funkcji i przeciążonego
operatora.
class konto_bankowe
{
private:
char nazwisko[30];
char adres[50];
double stan_konta;
public:
konto_bankowe();
~konto_bankowe();
void wplata(double);
void wyplata(double);
void dopisz_procenty(double);
void zmiana_danych();
void zmiana_danych(char wspolwlasciciel[30]);
bool operator-=(double debet);
};
W przykładzie tym funkcja zmiana_danych jest przeciążona. Zadeklarowane
zostały prototypy dwóch jej wersji. Odpowiednia wersja jest wybierana w
zależności od tego, czy w wywołaniu funkcji obecny jest argument. Operator -=
został przeciążony, tzn. słowo kluczowe operator oznajmia, że operator -= ma
nadane specjalne znaczenie, gdy używany jest łącznie z egzemplarzem klasy
konto_bankowe.operator.
7.2.2. Dziedziczenie
Dziedziczenie klas jest jedną z głównych właściwości programowania
obiektowego. W języku C++ jeżeli zdefiniowano już jedną klasę bazową, to
można także zadeklarować klasę pochodną, która przejmuje wszystkie atrybuty
klasy bazowej i dodaje swoje. Mówi się, że klasa pochodna dziedziczy z klasy
bazowej. Można budować hierarchię klas pochodnych o arbitralnej głębokości.
Opierając się na przykładzie przytaczanym wcześniej można zadeklarować klasę
konto_VISA, która będzie klasą pochodną klasy konto_bankowe. W tym celu
należy jednak zamienić słowo kluczowe private, które występowało w klasie
konto_bankowe na słowo kluczowe protected, aby funkcje składowe tek klasy
mogły być dziedziczone przez klasę pochodną konto_VISA.
Klasa bazowa:
class konto_bankowe
{
protected:
char nazwisko[30];
char adres[50];
double stan_konta;
public:
konto_bankowe();
~konto_bankowe();
void wplata(double);
void wyplata(double);
void dopisz_procenty(double);
void zmiana_danych();
void zmiana_danych(char wspolwlasciciel[30]);
bool operator-=(double debet);
};
Klasa pochodna:
class konto_VISA : public konto_bankowe
{
private:
double numer_karty;
public:
void obsluga_karty();
void wplata(double);
void wyplata(double);
}
Klasa konto_VISA dziedziczy wszystkie funkcje składowe klasy bazowej. Jeżeli
w tej klasie dziedziczone funkcje zostały ponownie zadeklarowane, mówimy o
tej ponownej deklaracji jako o przesłonięciu funkcji dziedziczonych (np. funkcje
wpłata i wypłata). Jednak dziedziczone funkcje nie muszą być przesłaniane.
Mogą one być zdeklarowane po raz pierwszy w klasie pochodnej i łączyć
dziedziczone dane i funkcje jako składowe tej klasy.
7.3. System wejścia/wyjścia w C++
Język C++ zawiera nową bibliotekę funkcji wejścia/wyjścia opartą na
deklaracjach zawartych w pliku nagłówkowym iostream.h. Deklaracje zawarte
w tym pliku przeciążają operatory przesunięcia >> oraz <<, nadając im
znaczenie operatorów wejścia i wyjścia. Można używać tych operatorów w
połączeniu z czterema standardowymi strumieniami wejścia i wyjścia:
cin
Standardowy strumień wejściowy
cout
Standardowy strumień wyjściowy
cerr
Standardowy strumień wyjściowy komunikatów o błędach
clog
Buforowany odpowiednik cerr, stosowany przy dużej ilości
danych wyjściowych
Standardowy strumień wejścia najczęściej reprezentuje klawiaturę, a
standardowy strumień wyjścia – ekran monitora. Przykładowo, dla
zdefiniowanych zmiennych:
char c; int i; float f; double d;
można wyświetlić ich wartości przez użycie instrukcji :
cout << c << i << f << d << ”\n”;
W podobny sposób można odczytać wartości ze strumienia wejściowego:
cin >> c >> i >> f >> d;
Dla wczytania z strumienia wejściowego jednego znaku i zapisaniu go w
zmiennej c należy wykorzystać funkcję składową get klasy istream:
#include <iostream.h>
int main()
{
char c;
while (cin.get(c)) cout.put(c);
return(0);
}
Funkcja składowa put klasy ostream wstawia jeden znak do strumienia
wyjściowego. Dla „wyłowienia” ze strumienia wejściowego ciągu znaków i
zapisaniu ich w buforze można wykorzystać funkcję getline. Funkcja ta może
domyślnie kończy kopiowanie znaków po napotkaniu w strumieniu danych
znaku końca pliku EOF. Znak ten zdefiniowany jest w pliku iostream.h i
reprezentowany przez naciśnięte klawisze Ctrl+Z. Funkcja write wstawia
określoną parametrem ilość znaków do strumienia wyjściowego. Funkcja
gcount zwraca liczbę znaków wyłowionych przez ostatnie wywołanie getline.
Zasadę stosowania tych funkcji wyjaśnia poniższy program:
#include <iostream.h>
const int MAX=80;
int main()
{
char bufor[MAX];
while (cin.getline(bufor,MAX))
{
int ile_znakow;
ile_znakow=cin.gcount();
cout.write(bufor,ile_znakow);
}
return(0);
}
II. Projektowanie i tworzenie programów w pakietach Borland C++ v.3.1
oraz Visual C++
1. Charakterystyka zintegrowanego środowiska programowania Borland C++
1.1. Elementy składowe pakietu Borland C++ v.3.1
Borland C++ v.3.1. jest pakietem umożliwiającym tworzenie oprogramowania
w języku C/C++ zawierającym wszystkie właściwości standardu języka C
wzbogacone o techniki programowania obiektowego. W skład pakietu wchodzą
następujące elementy:
•
zintegrowany system programowania, zawierający wszystkie narzędzia
niezbędne do tworzenia programów i usuwania usterek, wykorzystania
innych programów pakietu, w tym wersja systemu zintegrowanego
przeznaczona dla systemu MS WINDOWS,
•
zewnętrzny kompilator języka C/C++,
•
Turbo Asembler v 3.0 – asembler udostępniający programiście obiektowe
techniki programowania w języku wewnętrznym procesora,
•
Turbo Debugger v. 3.0 – program służący do umiejscawiania i usuwania
usterek w programach,
•
Turbo Profiler v.30 – program do badania czasów wykonywania
poszczególnych elementów programu,
•
wiele innych programów wspomagających tworzenie oprogramowania dla
sytemu DOS i Windows.
W kolejnych rozdziałach zostaną omówione te elementy pakietu, które są
niezbędne do tworzenia oprogramowania w języku C/C++: zintegrowany system
programowania BC.exe oraz kompilator zewnętrzny BCC.exe.
1.2. Uruchomienie i wyjście z systemu zintegrowanego
Po zainstalowaniu pakietu Borland C++ v.3.1 w celu uruchomienia systemu
zintegrowanego należy wykonać polecenie o następującym formacie ogólnym:
BC [plik_źródłowy\nazwa_projektu]
gdzie plik_źródłowy powinien okreslać nazwę pliku tekstowego, który istnieje
lub ma zostać utworzony, nazwa_projektu oznacza projekt, który ma zostać
wczytany bezpośrednio po uruchomieniu systemu.
W celu zakończenia pracy z systemem zintegrowanym wykonywane jest
polecenie FILE/EXIT (kombinacja klawiszy ALT-X). Powoduje ono zapisanie
aktualnie przetwarzanego projektu i informacji o stanie systemu w zbiorach
konfiguracyjnych.
Po uruchomieniu systemu możliwe jest wykonywanie następujących czynności:
•
edycji zbiorów źródłowych, w wielu oknach edycyjnych, z możliwością
operacji edytorskich polegających na wycinaniu, kopiowaniu, powielaniu i
usuwaniu fragmentów tekstu,
•
kompilacji plików źródłowych,
•
generowanie plików źródłowych ze skompilowanych modułów,
•
wykonywanie i usuwanie usterek w programach w C/C++,
•
zarządzanie złożonymi programami za pomocą tzw. projektów,
•
wywoływanie innych programów pakietu.
Podane w wywołaniu systemu zintegrowanego pojęcie projektu określa listę
elementów składowych tworzonego programu:
•
moduły źródłowe programu (zbiory o rozszerzenicah: *.c, *.cpp),
•
moduły w postaci *.obj (już skompilowane),
•
biblioteki *.lib.
Program zarządzający projektami w systemie zintegrowanym rozpoznaje typy
zbiorów na podstawie ich rozszerzeń i w zależności od tego będą one
traktowane w różny sposób:
•
moduły źródłowe programu (*.c, *.cpp) zostają skompilowane do zbiorów
*.obj, a uzyskane w ten sposób moduły, *.obj zostaną wykorzystane później
w czasie łączenia,
•
moduły *.obj i biblioteki zostaną wykorzystane dopiero w fazie łączenia.
1.3. Obsługa systemu zintegrowanego Borland C++ v. 3.1.
Po uruchomieniu systemu (program BC.exe) jego obsługa jest możliwa zarówno
za pomocą myszki jak i klawiatury. Ekran w systemie zintegrowanym jest
podzielony na trzy obszary (rys.1):
•
wiersz menu,
•
część roboczą ekranu,
•
wiersz statusu.
Rys.1 Podział ekranu w systemie zintegrowanym
Wiersz menu (1 wiersz od góry ekranu) umożliwia dostęp do wszystkich
poleceń systemu. Przejście do wiersza menu umożliwia klawisz F10, natomiast
szybszą metodą wyboru konkretnej opcji jest naciśnięcie kombinacji klawisza
Alt i klawisza reprezentującego podświetlony znak wybieranej grupy opcji. Na
przykład naciśnięcie Alt-F spowoduje wybranie grupy opcji FILE.
Rezultatem wyboru opcji jest wyświetlenie okna zawierającego polecenia lub
kolejne opcje. Na rysunku nr 2 przedstawiono widok ekranu po wybraniu opcji
OPTION.
Rys.2. Menu w środowisku zintegrowanym
Wybór polecenia w okienku menu jest możliwy za pomocą klawiszy kursorów
oraz klawisza Enter lub za pośrednictwem wskazania kursorem myszki i
podwójnym szybkim ‘kliknięciem’.
Część robocza ekranu stanowi główny teren działań użytkownika środowiska,
gdyż w niej wyświetlane są okna tekstowe będące głównym miejscem pracy.
Wygląd typowego okna przedstawiono na rysunku nr 3.
Rys.3. Okno w systemie zintegrowanym
Aktywne okno systemowe, w którym wykonywane są operacje wyróżnia
podwójna ramka oraz numer wyświetlany w lewej części ramki okna. Możliwe
jest otwarcie kilku lub nawet kilkunastu okienek operacyjnych. Zmiana okna
aktywnego jest dokonywana poprzez naciśniecie klawisza myszki przy
ustawieniu kursora myszki w obszarze okna nieaktywnego, przez użycie
polecenia Widow/Next lub naciśnięcie klawisza Alt-numer-okna.
Okna mogą być przesuwane, powiększane, zmniejszane i wyświetlane w
różnych układach. Wszystkie te operacje można wykonać albo przez wybranie
polecenia z grupy poleceń Window, albo za pomocą myszki:
•
zamknięcie okna – poprzez naciśnięcie klawisza myszki przy położeniu
kursora w obszarze piktogramu zamknięcia okna,
•
przesunięcie okna – poprzez naciśnięcie klawisza myszki, gdy jej kursor
znajduje się w obszarze krawędzi okna i trzymając go przesunięciu myszki,
•
zmiana rozmiarów okna – poprzez naciśnięciu klawisza myszki, gdy jej
kursor znajduje się w obszarze piktogramu zmiany rozmiarów okna i po
przesunięciu myszki,
•
powiększenie lub powrót do poprzednich rozmiarów (Zoom) – poprzez
naciśnięcie klawisza myszki przy położeniu jej kursora w obszarze
piktogramu powiększania okna.
W systemie zintegrowanym Borland korzysta się z kilku rodzajów okien:
•
okna edycyjne,
•
okna komunikatów,
•
okna projektów,
•
okna obserwowanych wyrażeń,
•
okna dialogowe.
Na szczególną uwagę zasługują okna dialogowe, gdyż ich zastosowanie i
funkcjonowanie różnią je nieco od pozostałych. Służą one do wymiany
informacji pomiędzy użytkownikiem a systemem. Rozmiary okna dialogowego
są stałe, natomiast dopuszczalne jest jego przesuwanie. W oknach dialogowych
wyróżniane są następujące elementy (rysunek nr 4):
•
wiersze wprowadzania danych – pozwalają na wprowadzanie informacji z
klawiatury,
•
listy danych wprowadzonych w wierszach wprowadzania – po naciśnięciu
klawisza strzałka w dół w czasie wprowadzania danych zostanie wyświetlona
lista wprowadzonych wcześniej informacji. Z listy tej można wybrać i
przenieść daną do wiersza wprowadzania,
•
przyciski – służą do wykonywania różnych operacji. Trzy przyciski mają
charakter uniwersalny: OK – potwierdza ustalenia poczynione w oknie
dialogowym, Cancel – oznacza rezygnację z wprowadzonych w oknie
modyfikacji, Help – wywołuje podręczną pomoc dla danego okna
dialogowego.
•
listy łańcuchów – umożliwiają wybór jednego z wyświetlanych w nich
elementów listy
Rys. 4. Elementy okna dialogowego
•
podokienka z opcjami – umożliwiające włączenie lub wyłączenie jednej lub
kilku opcji z danej grupy jednocześnie (rysunek nr 5).
Rys. 5. Podokienka z opcjami
Wiersz statusu służy do wyświetlania informacji pomocniczych dotyczących
aktualnie wybranego polecenia oraz aktualnie dostępnych klawiszach szybkiego
wyboru. Wyświetlenie polecenia mogą być wywoływane za pomocą klawiatury
lub myszki.
1.4. Edycja zbiorów źródłowych
Kod źródłowy programów opracowywany jest w oknach edycyjnych, które
stanowią główny składnik części roboczej ekranu.