background image

LEKCJA 1. Co o C i C++ każdy wiedzieć powinien.  

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się, dlaczego pora na C++.    
________________________________________________________________ 
 
  
Język C++ jest uniwersalnym, nowoczesnym językiem programowania. 
Stosowane przez USA i inne kraje wobec Polski wieloletnie 
embargo COCOM'u (przeszkody w dostępie do nowoczesnej   
technologii) sprawiły m. in., że popularność OS2, UNIXa i C/C++ 
jest w Polsce do dziś nieproporcjonalnie mała, a Basica, Pascala 
i DOSa nieproporcjonalnie duża. W USA C++ już od kilku lat 
stanowi podstawowe narzędzie programistów.   
  
Już słyszę oburzenie (A co mnie obchodzi historia   
"komputerologii" i koligacyjki!). Otóż obchodzi, bo wynikają z   
niej pewne "grzechy pierworodne" języka C/C++, a dla Ciebie,   
szanowny Czytelniku - pewne wnioski praktyczne.  
  
Grzech Pierwszy:   
* Kompilator języka C/C++ jest standardowym wyposażeniem systemu 
operacyjnego UNIX.   
  
Skutki praktyczne:   
  
Każdy PC jest w momencie zakupu (co często wchodzi w cenę zakupu 
komputera) wyposażany w system operacyjny DOS - np. DR DOS, PC   
DOS, PTS DOS lub MS DOS. Standardowo w zestaw systemu MS DOS   
wchodzi interpreter języka BASIC (w MS-DOS - QBasic.EXE). Możesz 
więc być pewien, że jeśli jest DOS, to musi być i BASIC. 
Podobnie rzecz ma się z C/C++. Jeśli jest na komputerze system   
UNIX (za wyjątkiem najuboższych wersji systemu XENIX), masz tam 
do dyspozycji kompilator C/C++, za to BASICA ani Pascala prawie 
na pewno tam nie ma. Podobnie coraz popularniejszy OS/2   
wyposażony jest w kompilator (całkiem niezły) C++ i dodatkowo   
jeszcze w pewne gotowe-firmowe biblioteki.  
  
Grzech drugi:   
* Język C/C++ powstał jeszcze zanim wymyślono PC, DOS, GUI   
(Graficzny Interfejs Użytkownika), Windows i inne tym podobne.   
  
Dwa najważniejsze skutki praktyczne:  
  
I. W założeniach twórców język C++ miał być szybki (i jest) i   
zajmować mało miejsca w pamięci (bo ówczesne komputery miały jej 
bardzo mało!). Zawiera więc różne, niezrozumiałe dla nas z   
dzisiejszego punktu widzenia skróty. Np. to co w Pascalu czy   
Basicu wygląda zrozumiale:   
  
i:=i+1;                            (Pascal)   
10 I=I+1     lub inaczej NEXT I    (Basic)   
  
to w języku C++ wygląda dziwacznie:   
  
i++;            albo jeszcze dziwniej         ++i;  
  

1

background image

Tym niemniej zwróć uwagę, że w Pascalu zajmuje to 7 znaków, w   
Basicu - 8 znaków (spacja to też znak!), a w C++ tylko 4.  
  
Inny przykład:   
  
X=X+5     (Basic, 5 znaków),   
X:=X+5    (Pascal, 6 znaków),   
X+=5      (C++, tylko 4 znaki).  
  
Z takiej właśnie filozofii wynika i sama nazwa - najkrótsza z   
możliwych. Jeśli bowiem i++ miało znaczyć mniej więcej tyle samo 
co NEXT I (następne I) to C++ znaczy mniej więcej tyle samo co   
"NASTĘPNA WERSJA C".   
  
II. Nie ma nic za darmo. W języku C/C++, podobnie jak w   
samochodzie wyścigowym formuły I, za szybkość i skuteczność   
płaci się komfortem. Konstrukcje stosowane w języku C/C++ są   
maksymalnie dostosowane do "wygody" komputera. Pozwala to na   
uzyskiwanie ˙niezwykle szybkich "maszynowo-zorientowanych" kodów 
wykonywalnych programu, ale od programisty wymaga   
przyzwyczajenia się do "komputerowo-zorientowanego sposobu   
myślenia".  
  
Grzech Trzeci (i chyba najcięższy):   
* Jest najlepszy. Ostrożniej - jest najchętniej stosowanym   
narzędziem profesjonalnych programistów.  
  
Ma oczywiście konkurentów. Visual Basic (do małych aplikacji   
okienkowych), Turbo Pascal (do nauki podstaw - elementów   
programowania sekwencyjnego, proceduralno-strukturalnego),   
QuickBasic (programowanie strukturalne w środowisku DOS),   
Fortran 90, ADA, SmallTalk, itp, itd.   
  
Sam wielki Peter Norton przyznaje, że początkowe wersje swojego 
słynnego pakietu Norton Utilities pisał w Pascalu, ale dopiero   
przesiadka na C/C++ pozwoliła mu doprowadzić NU do dzisiejszej   
doskonałości. Jakie są programy Petera Nortona - każdy widzi...  
  
Zapewne masz na swoim komputerze kilka różnych aplikacji (np.   
TAG, QR-Tekst, Word, itp.) - jeśli zajrzysz do nich do środka   
(View), możesz sam przekonać się, że większość z nich została   
napisana właśnie w C++ (Kompilatory C++ pozostawiają w kodzie   
wynikowym .EXE swoją wizytówkę zwykle czytelną przy pomocy   
przeglądarki; przekonasz się o tym także zaglądając przez [View] 
do własnych programów); stosowane narzędzia możesz rozpoznać   
także po obecności dynamicznych bibliotek - np. BWCC.DLL -   
biblioteka elementów sterujących - klawiszy, itp - Borland   
Custom Controls for Windows).  
  
Skutki praktyczne:   
  
Nauczywszy się języka C/C++ możesz nie bać się ani systemu   
UNIX/XENIX a ich środowiska okienkowego - X Windows, ani OS2,   
ani Windows 95 (dotychczasowe testy starych 16-bitowych   
aplikacji wykazały b. wysoki stopień kompatibilności), ani   
stacji roboczych, ani dużych komputerów klasy mainframe. Język   
C/C++ dosłużył się bowiem ogromnej ilości tzw. implementacji   
czyli swoich odmian, przeznaczonych dla różnych komputerów i dla 
różnych systemów operacyjnych. Windows NT i Windows 95 również   
zostały napisane w C++.   

2

background image

Czytając prasę (np. Computer World, PC-Kurier i in.) zwróć   
uwagę, że najwięcej ofert pracy jest właśnie dla programistów   
posługujących się C++ (i tak zapewne będzie jeszcze przez kilka 
lat, póki nie wymyślą czegoś lepszego - np. jakiegoś C+++).   
Z Grzechu Trzeciego (choć nie tylko) wynika także pośrednio   
Grzech Czwarty.   
 
Języka C++ Grzech Czwarty - ANSI C, C++, czy Turbo C++, Visual 
C++, czyli mała wieża BABEL.   
  
Nie jestem pewien, czy "wieża BABEL" jest określeniem   
trafniejszym niż "kamień filozoficzny", bądź "perpetuum mobile". 
To co w ciągu ostatnich lat stało się z językiem C++ ma coś   
wspólnego z każdym z tych utopijnych symboli. A w dużym   
uproszczeniu było to tak.   
  
Podobnie, jak mechanikom od zarania dziejów marzyło się   
perpetuum mobile, tak informatykom zawsze marzyło się stworzenie 
jednego SUPER-UNIWERSALNEGO języka programowania. Takiego, który 
byłby zupełnie niezależny od sprzętu tzn., aby program napisany 
w takim języku mógł być przeniesiony BEZ ŻADNYCH ZMIAN na   
dowolny komputer I DZIAŁAŁ. Do takiej roli pretendowały kolejno 
FORTRAN, Algol a potem przyszła pora na C/C++. Gdyby informatycy 
nie okazali się zbyt zachłanni, może coś by z tego wyszło. Ale, 
jak to w życiu, programiści (podobnie jak żona rybaka z bajki "O 
rybaku i złotej rybce") chcieli wszystkiego naraz: 
  
* żeby program dał się przenieść na komputer innego typu i   
działał,   
* żeby działał szybko i optymalnie wykorzystywał sprzęt,   
* żeby umiał wszystko, co w informatyce tylko wymyślono (tj. i   
grafika, i obiekty, i obsługa peryferii i...).   
  
I stało się. W pomyślanym jako uniwersalny języku zaczęły   
powstawać odmiany, dialekty, mutacje, wersje itp. itd.   
  
Jeśli C++ nie jest Twoim pierwszym językiem, z pewnością   
zauważyłeś Czytelniku, że pomiędzy GW Basic a Quick Basic są   
pewne drobne różnice. Podobnie Turbo Pascal 7.0 trochę różni się 
od Turbo Pascala 5.0. Mimo to przykład poniżej pewnie Cię trochę 
zaskoczy. Dla zilustrowania skali problemu przedstawiam poniżej 
dwie wersje TEGO SAMEGO PROGRAMU napisanego w dwu różnych   
wersjach TEGO SAMEGO JĘZYKA C++. . Obydwa programy robią   
dokładnie to samo. Mają za zadanie wypisać na ekranie napis   
"Hello World" (czyli "Cześć świecie!").   
  
Program (1)  
  
main()  
{  
  printf("Hello World\n");   
}   
  
  
Program (2)   
  
#include <windows.h>   
#include <iostream.h>   
  
LPSTR p = "Hello World\n";  

3

background image

  
main(void)   
{   
  cout << "Hello World" << endl;  
  MessageBox(0, p, "Aplikacja dla Windows", MB_OK);   
  return (0);   
}  
  
Cóż za uderzające podobieństwo, prawda? Ale żarty na bok. Jeśli 
zaistnieje jakiś problem, zawsze mamy co najmniej trzy wyjścia. 
Możemy:   
  
1. Udawać, że go nie ma.   
Tak postępuje wielu autorów podręczników na temat C/C++.   
2. Krzyczeć, że nam się to nie podoba.   
Mamy pełne prawo obrazić się i wrócić do Basica lub Pascala.   
3. Spróbować poruszać się w tym gąszczu.   
  
Wyjście trzecie ma jedną wadę - jest najtrudniejsze, ale i   
efekty takiego wyboru są najbardziej obiecujące.  
  
Jeśli chcesz zaryzykować i wybrać wyjście trzecie, spróbujmy   
zrobić pierwszy krok w tej "dżungli". Wyjaśnijmy kilka nazw,   
pojęć i zasad gry obowiązujących w tym obszarze.   
  
Języki programowania posługują się pewnymi specyficznymi grupami 
słów i symboli. Są to m. in.:   
  

słowa kluczowe   

(tu pomiędzy wersjami C++ rozbieżności są niewielkie),   
  

operatory (po prostu znaki operacji - np. +),   

(tu zgodność jest niemal 100 %-owa)  
  

dyrektywy   

(polecenia dla kompilatora JAK tworzyć program wynikowy;  
tu już jest gorzej, szczególnie dyrektywa #pragma w każdej   
wersji kompilatora C++ jest inna)   
  

nazwy funkcji   

(z tym gorzej, bo każdy producent ma własne funkcje i własne   
upodobania)   
  

nazwy stałych   

(gdyby chodziło tylko o PI i e - wszystko byłoby proste)   
  

nazwy zasobów (FILE, PRN, CONSOLE, SCREEN itp. itd)   

(tu jest lepiej, ale też rozbieżności są zauważalne)  
  
Autor programu może jeszcze nadawać zmiennym (liczbom, zmiennym 

4

background image

napisom, obiektom, itp.) własne nazwy, więc czasem nawet   
wytrawny programista ma kłopoty ze zrozumieniem tekstu   
żródłowego programu...  
  
W języku C a następnie C++ przyjęto pewne maniery nadawania nazw 
 
  
- identyfikatorów ułatwiające rozpoznawanie tych grup słów:   
  
* nazwa() - funkcja   
* słowa kluczowe i nazwy zmiennych - małymi literami   
* STAŁE - nazwy stałych najczęściej dużymi literami   
* long/LONG - typy danych podstawowe/predefiniowane dla Windows  
_NAZWA - nazwy stałych predefiniowanych przez producenta   
__nazwa lub __nazwa__ - identyfikatory charakterystyczne dla   
danej wersji kompilatora   
  
itp., których to zwyczajów i ja postaram się przestrzegać w   
tekście książki.  
  
Amerykański Instytut Standardów ANSI od lat prowadzi walkę z   
wiatrakami. Stoi na straży jednolitego standardu języka, który   
nazywa się standardem ANSI C i ANSI C++. Wielcy producenci od   
czasu do czasu organizują konferencje i spotkania gdzieś w   
ciepłych krajach i uzgadniają niektóre standardy - czyli wspólne 
dla nich i zalecane dla innych normy, ale niektórzy bywają   
zazdrośni o własne tajemnice i nie publikują wszystkich   
informacji o swoich produktach. Dlatego wszelkie "słuszne i   
uniwersalne" standardy typu ODBC, Latin 2, Mazovia, LIM, OLE,   
DDE, BGI, itp., itd. mają niestety do dziś ograniczony zakres   
stosowalności a wszelkie zapewnienia producentów o całkowitej   
zgodności ich produktu z... (tu wpisać odpowiednie) należy   
niestety nadal traktować z pewną rezerwą.  
  
W niniejszej książce zajmiemy się kompilatorem Borland C++ w   
jego wersjach 3.0 do 4.5, jest to bowiem najpopularniejszy w   
Polsce kompilator języka C/C++ przeznaczony dla komputerów IBM   
PC. Nie bez znaczenia dla tej decyzji był także fakt, że Borland 
C++ i Turbo C++ bez konfliktów współpracuje z:   
  
* Turbo Pascal i Borland Pascal;   
* Assemblerami: TASM, BASM i MASM;   
* Turbo Debuggerem i Turbo Profilerem;   
* bibliotekami Turbo Vision, ObjectVision, Object Windows   
Library, Database Tools, itp.   
* pakietami innych producentów - np. Win/Sys Library, Object   
Professional, CA-Visual Objects, Clipper, itp.   
  
i in. produktami "ze stajni" Borlanda popularnymi wśród   
programistów. Programy TASM/BASM, Debugger, Profiler a także   
niektóre biblioteki (np. Object Windows Library, Turbo Vision   
Library, itp. wchodzą w skład pakietów instalacyjnych BORLANDA, 
ale UWAGA - niestety nie wszystkich). Borland C++ 4+ pozwala,   
dzięki obecności specjalnych klas VBX w bibliotece klas i   
obiektów Object Windows Library na wykorzystanie programów i   
zasobów tworzonych w środowisku Visual Basic'a. Podobnie   
kompilatory C++ firmy Microsoft (szczególnie Visual C++)   
bezkonfliktowo współpracują z zasobami innych aplikacji - np.   
Access, Excel, itp..  
  

5

background image

Warto tu zwrócić uwagę na jeszcze jeden czynnik, który może stać 
się Twoim, Czytelniku atutem. Jeśli znasz już kompilatory Turbo 
Pascal, bądź Borland Pascal, zwróć uwagę, że wiele funkcji   
zaimplementowanych w Turbo Pascal 6.0. czy 7.0 ma swoje   
odpowiedniki w BORLAND C++ i Turbo C++. Odpowiedniki te zwykle   
działają dokładnie tak samo, a różnią się najczęściej   
nieznacznie pisownią nazwy funkcji. Wynika to z błogosławieństwa 
"lenistwa" (ponoć homo sapiens najwięcej wynalazków popełniał   
właśnie ze strachu, bądź z lenistwa...). Firmie Borland "nie   
chciało się" wymyślać od nowa tego, co już sprawdziło się   
wcześniej i do czego przyzwyczaili się klienci! I odwrotnie.   
Poznawszy Borland/Turbo C++ z łatwością zauważysz te same   
funkcje w Borland/Turbo Pascalu.  
  
[!!!]UWAGA!   
________________________________________________________________ 
 
O Kompilatorach BORLAND C++ 4 i 4.5 napiszę nieco póżniej,   
ponieważ są bardziej skomplikowane i wymagają trochę większej   
znajomości zasad tworzenia i uruchamiania programów (projekty). 
 
To prawda, że zawierają narzędzia klasy CASE do automatycznego   
generowania aplikacji i jeszcze kilka innych ułatwień, ale miej 
trochę cierpliwości...  
________________________________________________________________ 

[???] C.A.S.E.   
________________________________________________________________ 
 

CASE - Computer Aided Software Engineering 

inżynieria programowa wspomagana komputerowo. Najnowsze kompilatory C++   
wyposażone są w narzędzia nowej generacji. W różnych wersjach   
nazywają się one AppExpert, ClassExpert, AppWizard, VBX   
Generator, itp. itd, które pozwalają w dużym stopniu   
zautomatyzować proces tworzenia aplikacji. Nie można jednak   
zaczynać kursu pilotażu od programowania autopilota - a kursu   
programowania od automatycznych generatorów aplikacji dla   
Windows...   
________________________________________________________________ 
 
  
Zaczynamy zatem od rzeczy najprostszych, mając jedynie tę   
krzepiącą świadomość, że gdy już przystąpimy do pisania   
aplikacji konkurencyjnej wobec Worda, QR-Tekst'a, czy Power   
Point'a - może nas wspomagać system wspomaganina CASE dołączony 
do najnowszych wersji BORLAND C++ 4 i 4.5. Jeśli mamy już gotowe 
aplikacje w Visual Basic'u - Borland C++ 4+ pozwoli nam 
skorzystać z elementów tych programów (ale pracować te aplikacje 
po przetransponowaniu do C++ będą od kilku do kilkuset razy   
szybciej).  
_______________________________________________________________ 

6

background image

LEKCJA 2. Jak korzystać z kompilatora BORLAND C++? 

 
________________________________________________________________ 
W trakcie tej lekcji poznasz sposoby rozwiązania typowych  
problemów występujących przy uruchomieniu kompilatora Borland  
C++.  
________________________________________________________________ 
 
UWAGA: 
Z A N I M rozpoczniesz pracę z dyskietką dołączoną do niniejszej 
książki radzimy Ci SPORZĄDZIĆ ZAPASOWĄ KOPIĘ DYSKIETKI przy  
pomocy rozkazu DISKCOPY, np. 
 
DISKCOPY A: A:    lub    DISKCOPY B: B:  
 
Unikniesz dzięki temu być może wielu kłopotów, których może Ci  
narobić np. przypadkowy wirus lub kropelka kawy.  
 
INSTALACJA DYSKIETKI.  
 
Na dyskietce dołączonej do niniejszej książki, którą najlepiej  
zainstalować na dysku stałym (z dyskiem pracuje się znacznie  
szybciej, a prócz tego jest tam znacznie więcej miejsca), w jej  
katalogu głównym znajduje się programik instalacyjny o nazwie:  

INSTALUJ.BAT  

napisany jako krótki plik wsadowy w języku BPL (Batch  
Programming Language - język programowania wsadowego). Aby  
zainstalować programy z dyskietki na własnym dysku powinieneś:  
 
* sprawdzić, czy na dysku (C:, D:, H: lub innym) jest co  
najmniej 2 MB wolnego miejsca,  
* włożyć dyskietkę do napędu i wydać rozkaz:  
 
<-- patrz tekst ksiazki 
 
* po naciśnięciu [Entera] rozpocznie się nstalacja. O  
zakończeniu instalacji zostaniesz poinformowany napisem na  
ekranie.  
 
UWAGI:  
* Jeśli korzystasz z napędu dyskietek B:, lub chcesz  
zainstalować programy z dyskietki na innym dysku niż C: -  
wystarczy napisać rozkaz - np. B:\INSTALUJ AMC48 D: i nacisnąć  
[Enter].  
* Program instalacyjny zadziała poprawnie tylko wtedy, gdy masz  
system operacyjny DOS 6+ (6.0 lub nowszy) na dysku C: w katalogu 
 
C:\DOS. 
* Możesz zainstalować programy z dyskietki z poziomu środowiska  
Windows. W oknie Menedżera Programów:  
- rozwiń menu Plik  
- wybierz rozkaz Uruchom...  
- do okienka wpisz <-- patrz tekst książki 
  
Program instalacyjny utworzy na wskazanym dysku katalog 

7

background image

\C-BELFER  
i tam skopiuje całą zawartość dyskietki oraz dokona dekompresji  
(rozpakowania) plików. Jeśli chcesz skopiwać zawartość dyskietki 
do własnego katalogu roboczego, wystarczy "wskazać" programowi  
instalacyjnemu właściwy adres:  
 
<-- patrz tekst książki 
 
Zostanie utworzony katalog: F:\USERS\ADAM\TEKSTY\C-BELFER 
 
UWAGA:  
Prócz przykładów opisanych w książce dyskietka zawiera dodatkowo 
kilka przykładowych aplikacji, na które zabrakło miejsca, między 
innymi: 
 
WYBORY95 - prosta gra zręcznościowa (dla Windows) 
FOR*.CPP - przykłady zastosowania pętli  
BGI*.CPP - przykłady grafiki DOS/BGI  
oraz programik ułatwiający kurs - MEDYT.EXE wyposażony w 
dodatkowe pliki tekstowe.  
 

I. URUCHOMIENIE KOMPILATORA.  

 
Aby uruchomić kompilator, powinieneś w linii rozkazu po  
DOS'owskim znaku zachęty (zwykle C> lub C:\>) wydać polecenie:  
 
BC  
 
i nacisnąć [Enter].  
(UWAGA: w różnych wersjach kompilatorów może to być np.:  
BC, TC, a dla Windows np. BCW - sprawdź swoją wersję)  
 
Jeśli Twój komputer odpowiedział na to:  
 
Bad command or file name  
 
* na Twoim komputerze nie ma kompilatora BORLAND C++:  
ROZWIĄZANIE: Zainstaluj C++.  
 
* w pliku AUTOEXEC.BAT nie ma ścieżki dostępu do katalogu, w  
którym zainstalowany jest kompilator C++.  
 
ROZWIĄZANIE:  
 
1. Zmienić bieżący katalog (i ewentualnie dysk) na odpowiedni,  
np.:  
D:[Enter]  
CD D:\BORLANDC\BIN[Enter].       //UWAGA: Podkatalog \BIN  
Albo  
 
2. Ustawić ścieżkę dostępu przy pomocy rozkazu np:  
PATH C:\BORLANDC\BIN  
(lub D:\TURBOC\BIN stosownie do rozmieszczenia plików na Twoim  
komputerze; najlepiej zasięgnij rady lokalnego eksperta). 
 

NIE CHCE USTAWIĆ ŚCIEŻKI

8

background image

________________________________________________________________ 
Tak czasem się zdarza - zwykle wtedy, gdy pracujesz w DOS-ie z  
programem Norton Commander. Musisz pozbyć się "na chwilę"  
programu NC. Naciśnij [F10] - Quit i potwierdź przez [Y] lub  
[Enter]. Po ustawieniu ścieżek możesz powtórnie uruchomić NC.  
________________________________________________________________ 
 
Albo 
 
3. Dodać do pliku AUTOEXEC.BAT dodatkową ścieżkę. Jest to  
wyjście najlepsze. Na końcu linii ustawiającej ścieżki - np.:  
 
PATH C:\; C:\DOS; C:\NC; C:\WINDOWS 
 
dodaj ścieżkę do kompilatora C++, np.:  
 
PATH C:\; C:\DOS; C:\NC; D:\BORLANDC\BIN; 
 
Załatwi to problem "raz na zawsze". Po uruchomieniu komputera  
ścieżka będzie odtąd zawsze ustawiana automatycznie.  
Ponieważ kompilator C++ wymaga w trakcie pracy otwierania i  
łączenia wielu plików, różne wersje (program instalacyjny  
INSTALL.EXE podaje tę informację w okienku pod koniec  
instalacji) wymagają dodania do pliku konfiguracyjnego  
CONFIG.SYS wiersza:  
 
FILES = 20  
 
(dla różnych wersji wartość ta wacha się w granicach od 20 do  
50). Najbezpieczniej, jeśli nie masz pewności dodać 50. Jeśli  
wybrałeś wariant trzeci i ewentualnie zmodyfikowałeś swój  
CONFIG.SYS, wykonaj przeładowanie systemu [Ctrl]-[Alt]-[Del].  
Teraz możesz wydać rozkaz  
 
BC[Enter] 
 
Mam nadzieję, że tym razem się udało i oto jesteśmy w IDE  
Borland C++. Jeśli nie jesteś jedynym użytkownikiem, na ekranie  
rozwinie się cała kaskada okienek roboczych. Skonsultuj z  
właścicielem, które z nich można pozamykać a które pliki można  
skasować lub przenieść. Pamiętaj "primo non nocere" - przede  
wszystkim nie szkodzić! 
 
€[S!]  IDE = Integrated Development Environment,  
 
IDE, czyli Zintegrowane Środowisko Uruchomieniowe. Bardziej  
prozaicznie - połączony EDYTOR i KOMPILATOR. Zapewne znasz już  
coś podobnego z Pascala lub Quick Basica. Od dziś będzie to  
Twoje środowisko pracy, w którym będziesz pisać, uruchamiać i  
modyfikować swoje programy.  
 

DISK FULL

________________________________________________________________ 
Co robić, jeśli przy próbie uruchomienia kompilator C++  
odpowiedział Ci:  
 
Disk full! Not enough swap space.  
 

9

background image

Program BC.EXE (TC.EXE) jest bardzo długi. Jeśli wydasz rozkaz  
(wariant 1: Turbo C++ 1.0, niżej BORLAND C++ 3.1): 
 
DIR TC.EXE 
uzyskasz odpowiedź, jak poniżej:  
 
C:>DIR TC.EXE 
Directory of D:\TC\BIN  
 
TC       EXE   876480 05-04-90   1:00a  
        1 file(s)     876480 bytes  
                    17658880 bytes free  
  
C:>DIR BC.EXE 
Directory of C:\BORLANDC\BIN  
  
BC       EXE   1410992 06-10-92   3:10a  
        1 file(s)    1410992 bytes  
                    18926976 bytes free  
  
 
Ponieważ plik kompilatora nie mieści się w 640 K pamięci musi  
dokonywać tzw. SWAPOWANIA i tworzy na dysku dodatkowy plik  
tymczasowy (ang. swap file). Na dysku roboczym powinno  
pozostawać najmniej 500 KB wolnego miejsca. Jeśli możesz,  
pozostaw na tym dysku wolne nie mniej niż 1 MB. Ułatwi to i  
przyspieszy pracę. 
________________________________________________________________ 
 

Tworzony tymczasowo plik roboczy wygląda tak:  

  
 Volume in drive D has no label  
 Directory of D:\SIERRA  
  
TC000A   SWP    262144 12-13-94   5:42p           (13-XII to dziś!) 
        1 file(s)     262144 bytes  
                    11696320 bytes free  
 
€[!!!] UWAGA: 
 
Kompilator C++ będzie próbował tworzyć plik tymczasowy zawsze w  
bieżącym katalogu, tzn. tym, z którego wydałeś rozkaz  
TC lub BC. 
 

II. WNIOSKI PRAKTYCZNE.  

 
* Lepiej nie uruchamiać C++ "siedząc" na dyskietce, ponieważ  
może mu tam zabraknąć miejsca na plik tymczasowy.  
* Dla użytkowników Novella: Uruchamiajcie kompilator C++ tylko  
we własnych katalogach - do innych możecie nie mieć praw zapisu. 
 
Plik .SWP jest tworzony tylko podczas sesji z kompilatorem C++ i 
 
usuwany natychmiast po jej zakończeniu. Możesz go zobaczyć tylko 
 
wychodząc "na chwilę" do systemu DOS przy pomocy rozkazu DOS  
Shell (menu File). 

10

background image

 

SWAP - Zamiana.  

________________________________________________________________ 
Jeśli wszystkie dane, potrzebne do pracy programu nie mieszczą  
się jednocześnie w pamięci operacyjnej komputera, to program -  
"właściciel", (lub system operacyjny - DOS, OS2, Windows) może  
dokonać tzw. SWAPOWANIA. Polega to na usunięciu z pamięci  
operacyjnej i zapisaniu na dysk zbędnej w tym momencie części  
danych, a na ich miejsce wpisaniu odczytanej z dysku innej  
części danych, zwykle takich, które są programowi pilnie  
potrzebne do pracy właśnie teraz. 
________________________________________________________________ 
 

€[Z] - Propozycje zadań do samodzielnego wykonania.
----------------------------------------------------------------  
 
1.1 Sprawdź ile bajtów ma plik .EXE w tej wersji kompilatora  
C++, której używasz.  
1.2. Posługując się rozkazem DOS Shell z menu File sprawdź gdzie 
znajduje się i jakiej jest wielkości plik tymczasowy .SWP. Ile  
masz wolnego miejsca na dysku ? 
________________________________________________________________ 
EOF

LEKCJA 3. Główne menu i inne elementy IDE.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się jak poruszać się w  
zintegrowanym środowisku (IDE) Turbo C++. 
________________________________________________________________ 
 
Najważniejszą rzeczą w środowisku IDE jest GŁÓWNE MENU (ang. 
MENU BAR), czyli pasek, który widzisz w górnej części ekranu.  
Działa to podobnie, jak główne menu w programie Norton Commander 
(dostępne tam przez klawisz [F9]). 
 

KRÓTKI PRZEGLĄD GŁÓWNEGO MENU.  

 
Przyciśnij klawisz [F10].  
Główne menu stało się aktywne. Teraz przy pomocy klawiszy  
kursora (ze strzałkami [<-], [->]) możesz poruszać się po menu i 
 
wybrać tę grupę poleceń, która jest Ci potrzebna. A oto nazwy  
poszczególnych grup:  
 
[S!]€GRUPY POLECEŃ - NAZWY POSZCZEGÓLNYCH "ROZWIJANYCH" MENU. 
 
=

Bez nazwy (menu systemowe).  

File

Operacje na plikach.  

Edit

Edycja plików z tekstami źródłowymi programów.  

11

background image

Search Przeszukiwanie.  
Run

Uruchomienie programu.  

CompileKompilacja programu. 
Debug "Odpluskwianie", czyli wyszukiwanie błędów w  

programie.  

Project Tworzenie dużych, wielomodułowych programów.  
Options Opcje, warianty IDE i kompilatora.  
Window Okna (te na ekranie).  
Help

Pomoc, niestety po angielsku.  

 
UWAGA:  
__________________________________________________________
W niektórych wersjach kompilatora na pasku głównego menu pojawi  
się jeszcze Browse - przeglądanie (funkcji, struktury klas i  
obiektów). Zwróć uwagę, że w okienkowych wersjach niektóre  
rozkazy "zmieniają" menu i trafiają do  
Browse, Debug, Project.  
W BC++ 4 menu Run brak (!). Tworzenie aplikacji sprowadza się  
tam do następujących kroków:  

Project | Open project     lub   

| AppExpert  

Debug | Run 
 

ROZWIJAMY MENU.  

 
Z takiego kręcenia się w kółko po pasku (a propos, czy  
zauważyłeś, że pasek podświetlenia może być "przewijany w  
kółko"?) jeszcze niewiele wynika. Robimy więc następny krok.  
 
Wskaż w menu głównym nazwę "File" i naciśnij [Enter].  
Rozwinęło się menu File zawierające listę rozkazów dotyczących  
operacji na plikach. Po tym menu też możesz się poruszać przy  
pomocy klawiszy kursora ze strzałkami górę lub w dół. Masz do  
wyboru dwie grupy rozkazów rozdzielone poziomą linią:  
 
€[S!]  
______________________________________________________________
Open

- Otwórz istniejący już plik z programem (np. w celu  
dopisania czegoś nowego).  

New

- Utwórz nowy plik (zaczynamy tworzyć nowy program).  

Save

- Zapisz bieżący program na dysk. Pamiętaj: Pliki z  
dysku nie znikają po wyłączeniu komputera. Zawsze  
lepiej mieć o jedną kopię za dużo niż o jedną za mało. 

 
oraz  
Print

- Wydrukuj program.  

Get Info€€

- Wyświetl informacje o stanie IDE.  

Dos Shell

- Wyjście "na chwilę" do systemu DOS z możliwością  

powrotu do IDE przez rozkaz EXIT.  

Quit

- Wyjście z IDE Turbo C++ i powrót do DOSa. Inaczej -  
KONIEC PRACY.  

_______________________________________________________________
 
Skoro już wiemy jak rozpocząć pracę nad nowym programem,   
zacznijmy przygotowanie do uruchomienia naszego pierwszego  
programu.  
 
Wybierz z menu File rozkaz Open... (otwórz plik). Ponieważ  

12

background image

rozkaz taki jest niejednoznaczny, wymaga przed wykonaniem  
podania dodatkowych informacji. Gdyby Twój komputer mówił,  
zapytałby w tym momencie "który plik mam otworzyć?". Pytanie  
zadać musi, będzie więc prowadził dialog z Tobą przy pomocy  
OKIENEK DIALOGOWYCH. Jeśli wybrałeś z menu rozkaz OPEN i  
nacisnąłeś [Enter], to masz właśnie na ekranie takie okienko  
dialogowe. Okienko składa się z kilku charakterystycznych  
elementów:  
 
OKIENKO TEKSTOWE - (ang. Text Box lub Input Box) w którym możesz 
 

pisać (klawisz BackSpace [<-] pozwoli Ci  
skasować wprowadzony tekst, jeśli się  
rozmyślisz). Okienko to zawiera tekst "*.C". 

 
OKIENKO Z LISTĄ - (ang. List Box) zawiera listę plików, z której 
 

możesz wybrać plik z programem.  

KLAWISZE OPCJI/POLECEŃ - (ang. Command Button) kiedy już  

dokonasz wyboru, to możesz wskazując  
taki klawisz np. potwierdzić [OK],  
zrezygnować [Cancel], otworzyć plik  
[Open] itp..  

 
Pomiędzy elementami okienka dialogowego możesz poruszać się przy 
pomocy klawiszy kursora i klawisza [Tab] lub kombinacji klawiszy 
 
[Shift]-[Tab] (spróbuj!). 

Możesz także posługiwać się myszką.  
Więcej o okienkach i menu dowiesz się z następnych lekcji, a na  
razie wróćmy do naszego podstawowego zadania - tworzenia  
pierwszego programu. 
 
Zanim zaczniemy tworzyć program włóż do kieszeni napędu A: (lub  
B:) dyskietkę dołączoną do niniejszej książki. Może ona stać się 
Twoją dyskietką roboczą i pomocniczą zarazem na okres tego  
kursu.  
 
Jeżeli zainstalowałeś zawartość dyskietki na dysku - przejdź do  
stosownego katalogu - C:\C-BELFER (D:\C-BELFER) i odszukaj tam  
programy przykładowe. Jeśli nie - możesz nadal korzystać z  
dyskietki (jest na niej trochę miejsca).  
 
Wpisz do okienka tekstowego nazwę A:\PIERWSZY (lub odpowiednio  
np. C:\C-BELFER\PIERWSZY). Rozszerzeniem możesz się nie  
przejmować - zostanie nadane automatycznie. Plik roboczy z Twoim 
programem zostanie utworzony na dyskietce w napędzie A:. 
 
Wskaż klawisz [Open] w okienku dialogowym i naciśnij [Enter] na  
klawiaturze.  
 
UWAGA!  
_________________________________________________________________
Dopóki manipulujesz okienkiem tekstowym i okienkiem z listą  
klawisz polecenia [Open] jest wyróżniony (podświetlony) i  
traktowany jako tzw. OPCJA DOMYŚLNA (ang. default). W tym  
stadium aby wybrać [Open] WYSTARCZY NACISNĄĆ [Enter]. 
__________________________________________________________________

13

background image

 
Wróciliśmy do IDE. zmieniło się tyle, że w nagłówku okna edytora 
zamiast napisu 

"NONAME00.CPP"            (ang. no mame - bez nazwy) 

jest teraz nazwa Twojego programu - PIERWSZY.CPP. Kursor miga w lewym 
górnym rogu okna edytora. Możemy zaczynać.  
 
Pierwsze podejście do programu zrobimy trochę "intuicyjnie".  
Zamiast wyjaśniać wszystkie szczegóły posłużymy się analogią do  
konstrukcji w Pascalu i Basicu (zakładam, że napisałeś już  
choćby jeden program w którymś z tych języków). Szczegóły te  
wyjaśnię dokładniej począwszy od następnej lekcji. 
 
 

WPISUJEMY PROGRAM "PIERWSZY.CPP".  

 
Wpisz następujący tekst programu:  
 
/* Program przykładowy - [P-1] */ 
 
#include <stdio.h> 
main() 

  printf("Autor: ...........");         /*tu wpisz imie Twoje!*/ 
  printf(" TO JA, TWOJ PROGRAM - PIERWSZY.CPP"); 
  printf("...achoj !!!");  

 
I już. Jak widzisz nie jest to aż takie straszne. Gdyby nie to,  
że zamiast znajomego PRINT"TO JA...", albo writeln(".."); jest  
printf("...");, byłoby prawie całkiem zrozumiałe. Podobny  
program w Pascalu mógłby wyglądać np. tak:  
 
# include <stdio.h>

            uses Crt; 

main()  /* początek */           

program AHOJ; {początek} 

{

   Begin 

printf("Autor");

write('Autor'); 

printf("TO JA"); 

write('TO JA'); 

printf("ahoj"); 

write('ahoj'); 

}

end. 

 

a w BASICU:  
 
10 PRINT "Autor" : REM Początek  
20 PRINT "TO JA"  
30 PRINT "ahoj"  
40 END  
 

€[!!!]UWAGA 
______________________________________________________________  
Zwróć uwagę, że działanie funkcji:  
PRINT (Basic),  
printf() (C++),  
Write i Writeln (Pascal)  
nie jest identyczne, a TYLKO PODOBNE. 

14

background image

________________________________________________________________ 
 
Sprawdzimy, czy program działa. Tam, gdzie są kropki wpisz Twoje 
imię - np. Ewa, Marian, Marcin. Pamiętaj o postawieniu na końcu  
znaków cudzysłowu ("), zamknięciu nawiasu i średniku (;) na  
końcu linii (wiersza). 
 
Naciśnij kombinację klawiszy [Alt]-[R]. Jest to inny, niż  
opisano poprzednio sposób dostępu do menu. Kombinacja klawiszy  
[Alt]-[Litera] powoduje uaktywnienie tego menu, którego nazwa  
zaczyna się na podaną literę. Przy takiej konwencji litera nie  
musi być zawsze pierwszą literą nazwy opcji. Może to być także  
litera wyróżniona w nazwie przez podkreślenie lub wyświetlenie  
np. w innym kolorze. I tak:  
 
[Alt]+[F]

menu File                   (Plik) 

[Alt]+[C]

menu Compile        

    (Kompilacja 

[Alt]+[R]

menu Run                    (Uruchamianie) 

[Alt]+[W]

menu Window                 (Okna) 

itd., itd..  
 
Kombinacja [Alt]+[R] wybiera więc menu Run (uruchomienie  
programu). Menu Run daje Ci do wyboru następujące polecenia: 
 
[S!] 
________________________________________________________________ 
Run

- Uruchomienie programu (Utwórz plik .EXE i Wykonaj). 

Program Reset - "Wyzerowanie" zmiennych programu. 
Go to Cursor - Wykonanie programu do miejsca wskazanego kursorem 
 

w tekście. 

Trace Into - Uruchom śledzenie programu. 
Step Over - Śledzenie programu z możliwością pominięcia funkcji. 
 
            (dosł. tzw. "Przekraczanie" funkcji). 
Arguments - Uruchom program z zadanymi argumentami. 
________________________________________________________________ 
 
Wybierz "Run". Jeśli nie zrobiłeś żadnego błędu, program  
powinien się skompilować z komentarzem "Success" i wykonać  
(kompilacja zakończona sukcesem; napis mignie tak szybko, że  
możesz tego nie zauważyć). Jeśli chcesz spokojnie obejrzeć  
wyniki działania swojego programu powinieneś wykonać  
następujące czynności: 
 
1. Rozwiń menu Window naciskając klawisze [Alt]-[W].  
2. Wybierz z menu rozkaz User screen (ekran użytkownika).  
Możesz wykonać to samo bez rozwijania menu naciskając kombinację 
 
klawiszy [Alt]-[F5].  
3. Po przejrzeniu wydruku naciśnij [Enter]. Wrócisz do okna  
edytora. 
 
Jeśli zrobiłeś błędy - kompilacja się nie uda i program nie  
zostanie wykonany, w okienku natomiast pojawi się napis "Errors" 
 
(czyli "Błędy"). Jeśli tak się stało naciśnij [Enter]  
dwukrotnie. Popraw ewentualne niezgodności i spróbuj jeszcze  
raz.  
 
Błędów zwykle bywa nie więcej niż dwa. Najczęściej jest to brak  

15

background image

lub przekłamanie którejś litery (w słowie main lub printf) i  
brak średnika na końcu linii. W okienku komunikatów (Message)  
mogą pojawić się napisy - np.:  
 
Error: Statement missing ;  
(Błąd: Zgubiony znak ;)  
 

Error Messages - Komunikaty o błędach. 

________________________________________________________________ 
Najczęściej w komunikatach o błędach będą na początku pojawiać  
się następujące słowa:  
 
Error - błąd  
Warning - ostrzeżenie  
Syntax - składnia (składniowy)  
Expression - wyrażenie  
never used - nie użyte (nie zastosowane)  
assign - przypisywać, nadawać wartość/znaczenie  
value - wartość  
statement - operator, operacja, wyrażenie 
________________________________________________________________ 
 

Co z tym średnikiem?  

 
________________________________________________________________ 
Zwróć uwagę, że po pdświetleniu komunikatu o błędzie (pasek  
wyróżnienia podświetlenia możesz przesuwać po liście przy pomocy 
klawiszy ze strzałkami w górę i w dół) i po naciśnięciu [Entera] 
kompilator pokaże ten wiersz programu, w którym jego zdaniem  
jest coś nie w porządku. Brak średnika zauważa zwykle dopiero po 
przejściu do następnego wiersza (i tenże wiersz pokaże), co bywa 
na początku trochę mylące. 
________________________________________________________________ 
  
 

CZEGO ON JESZCZE CHCE ?  

________________________________________________________________ 
Nawet po usunięciu wszystkich błędów C++ nie "uspokoi się"  
całkiem i będzie wyświetlał ciągle komunikat ostrzegawczy:  
 
* w OKIENKU KOMPILACJI:           (bardzo typowa sytuacja)

Errors:         0         (Błędy: 0)  
Warnings:

  (Ostrzeżenia:

1) 

* W OKIENKU KOMUNIKATÓW - (Messages - tym w dolnej części  
ekranu):  

*WARNING A:\PIERWSZY.C 4: Function should return a value in  
function main  

(Uwaga: Funkcja main powinna zwrócić wartość.) 
 
Na razie zadowolimy się spostrzeżeniem, że:  
* Błędy UNIEMOŻLIWIAJĄ KOMPILACJĘ i powodują komunikat ERRORS.  

16

background image

* Ostrzeżenia NIE WSTRZYMUJĄ KOMPILACJI i powodują komunikat  
WARNINGS.  
 
Jaki jest sens powyższego ostrzeżenia i jak go uniknąć dowiesz  
się z następnych lekcji. 
________________________________________________________________ 
 
Pozostaje nam w ramach tej lekcji:  
* Zapisać Twój pierwszy program na dysku i  
* Wyjść z IDE C++.  
 

JAK STĄD WYJŚĆ ?  

 
Aby zapisać plik PIERWSZY.CPP z Twoim programem (końcową  
ostateczną wersją) na dysk należy wykonać następujące czynności: 
 
1. Naciśnij klawisz [F10].  
W głównym menu pojawi się pasek wyróżnienia sygnalizując, że  
menu stało się aktywne.  
 
2. Naciśnij klawisz [F].  
Pasek wyróżnienia przesunie się podświetlając menu File  
(operacje na plikach). Rozwinie się menu File. 
 
3. Naciśnij klawisz [S] - wybierz polecenie Save (jeśli chcesz  
zapisać program w bieżącym katalogu i pod bieżącą nazwą) lub  
rozkaz Save As... (zapisz jako...), podaj nowy dysk/katalog i  
nową nazwę pliku.  
 
Tekst Twojego programu został zapisany na dysku/dyskietce. Teraz 
 
możemy wyjść z C++.  
 
Aby to zrobić, wykonaj następujące czynności:  
 
1. Naciśnij klawisz [F10]. Uaktywni się główne menu.  
2. Rozwiń menu File naciskając klawisz [F].  
3. Wybierz z menu polecenie "Exit/Quit" i naciśnij [Enter].  
 
€[!!!] SAVE szybciej.  
________________________________________________________________ 
Zwróc uwagę, że zamiast rozwijać kolejne menu, możesz korzystać  
z kombinacji klawiszy, które pozwalają Ci wydać rozkaz bez  
rozwijania menu. Takie kombinacje klawiszy (ang. hot keys lub  
shortcut keys) znajdziesz w menu obok rozkazu, np.:  
 
[Alt]-[X]      - Quit/Exit  
[F2]           - Save  
[F3]           - Open  
[Alt]-[F5]     - User screen (Podglądanie działania programu) itp.  
________________________________________________________________ 
 
€[Z] 
________________________________________________________________ 
1. Spróbuj napisać i uruchomić kilka własnych programów  
wypisujących różne napisy. W swoich programach zastosuj funkcję  
printf() według następującego wzoru:  
 
printf("....tu wpisz napis do wydrukowania...");  

17

background image

 
zastosuj znaki przejścia do nowego wiersza według wzoru:  
 
printf("...napis...\n");  
 
porównaj działanie. 
 
Swoim programom staraj się nadawać łatwe do rozpoznania nazwy  
typu PIERWSZY, DRUGI, ADAM1, PRZYKLAD itp. 
 

NIE CHCE DZIAŁAĆ ?  

________________________________________________________________ 
Pamiętaj, że dla języka C i C++ (w przeciwieństwie np. do  
Basica) PRINTF i printf to nie to samo! Słowa kluczowe i nazwy  
standardowych funkcji  
MUSZĄ BYĆ PISANE MAŁYMI LITERAMI !!!  
________________________________________________________________ 
 
€[???] GDZIE MOJE PROGRAMY ?  
________________________________________________________________ 
Bądź spokojny. Zapisz wersje źródłowe programów na dyskietkę  
(dysk). Swoje programy skompilowane do wykonywalnej wersji *.EXE 
znajdziesz najprawdopodobniej w katalogu głównym tego dysku, na  
którym zainstalowany został C++ lub w katalogu  
\BORLANDC\BIN\.... Jeśli ich tam nie ma, zachowaj zimną krew i  
przeczytaj uważnie kilka następnych stron. 
________________________________________________________________ 

€ PAMIĘTAJ:  
________________________________________________________________
Jeśli masz oryginalny tekst programu, nazywany WERSJĄ ŹRÓDŁOWĄ  
PROGRAMU, zawsze możesz uzyskać ten program w wersji "roboczej", 
tzn. skompilować go na plik wykonywalny typu *.EXE (ang.  
EXEcutable - wykonywalny).  
________________________________________________________________ 
 
 
€[S!]  printf() - PRINTing Function - Funkcja DRUKująca  
________________________________________________________________ 
na ekranie (dokładniej - na standardowym urządzeniu wyjścia).  
Odpowiednik PRINT w Basicu lub write w Pascalu. Dla ułatwienia  
rozpoznawania nazw funkcji w tekście większość autorów pisząca o 
języku C++ umieszcza zawsze po nazwie funkcji parę nawiasów (tak 
też musi ją stosować programista w programach w C++). Ja także  
będę stosować dalej tę zasadę. 
________________________________________________________________ 
 
 

A JEŚLI NIE MA C++ ???  

________________________________________________________________ 
W przeciwieństwie do INTERPRETERÓW (np. QBasic), które muszą być 
obecne, by program zadziałał, KOMPILATORY tworzą wersje  
wykonywalne programów, które mogą pracować niezależnie. W  
katalogu głównym tego dysku, na którym jest zainstalowany Twój  
BORLAND/Turbo C++ znajdziesz swoje programy PIERWSZY.EXE,  
DRUGI.EXE itp. Aby te programy uruchomić nie musisz uruchamiać  

18

background image

kompilatora C++. Wystarczy:  

1. Przejść na odpowiedni dysk przy pomocy polecenia:  
D:   (E: lub F:)  

2. Przejść do odpowiedniego katalogu - np. głównego:  
CD \  

3. Wydać polecenie:  
PIERWSZY[Enter]  
________________________________________________________________ 
 
€[!!!]UWAGA:  
________________________________________________________________ 
Jeśli nie jesteś jedynym użytkownikiem kompilatora C++ i na tym  
samym komputerze pracuje jeszcze ktoś inny, sprawdź, czy inny  
użytkownik nie ustawił inaczej katalogu wyjściowego (Options |  
Directories | Output Directory). Katalog wyjściowy (ang. output  
directory) to ten katalog, w którym C++ zapisuje pliki *.EXE po  
wykonaniu kompilacji. Jeśli jesteś skazany na własne siły -  
patrz - następne lekcje. 
________________________________________________________________ 
 

SPECJALNE KLAWISZE, które warto poznać.  

 
Oto skrócona tabela z najważniejszymi kombinacjami klawiszy  
służącymi do "nawigacji" (czyli prościej - poruszania się) w  
środowisku IDE kompilatorów BORLAND C++ i Turbo C++.  
 
Przydatne w Borland C++ i Turbo C++ kombinacje klawiszy.  
________________________________________________________________ 
Wybór rozkazów z menu:  
Alt+F    Rozwinięcie menu File (operacje na plikach)  
Alt+E    Rozwinięcie menu Edit (edycja tekstu programu)  
Alt+S    Rozwinięcie menu Search (przeszukiwanie)  
Alt+R    Rozwinięcie menu Run (uruchamianie programu)  
Alt+C    Rozwinięcie menu Compile (kompilacja)  
Alt+D    Rozwinięcie menu Debug (diagnostyka i błędy)  
Alt+P    Rozwinięcie menu Project (program wielomodułowy)  
Alt+O    Rozwinięcie menu Option (opcje, konfiguracja)  
Alt+W    Rozwinięcie menu Window (zarządzanie oknami)  
Alt+H    Rozwinięcie menu Help (pomoc)  
Alt+B    Rozwinięcie menu przeglądarki - Browse (Win) 
Alt+X    Wyjście z kompilatora DOS'owskiego - Exit  
Alt+F4   Wyjście z kompilatora dla Windows 
________________________________________________________________ 
 
 
Rozkazy w trybie edycji tekstu programu:  
________________________________________________________________ 
Shift+Delete    Wytnij wybrany blok tekstu (Cut) i umieść w  
                 przechowalni (Clipboard)  
Shift+Insert     Wstaw blok tekstu z przechowalni (Paste)  
Ctrl+Insert      Skopiuj zaznaczony blok tekstu do przechowalni  
                 (Copy)  
Ctrl+Y           Skasuj wiersz tekstu (Delete a line)  
Ctrl+Delete      Skasuj zaznaczony blok tekstu  
Shift+[-->]      Zaznaczanie bloku tekstu w prawo  

19

background image

Shift+[<--]      Zaznaczanie bloku tekstu w lewo  
Shift+[Down Arrow]  Zaznaczanie bloku tekstu w dół (strzałka w  
                 dół)  
Shift+[Up Arrow]  Zaznaczanie bloku tekstu w górę (strzałka w  
                 górę)  
Alt+Backspace    Anuluj ostatnią operację (Undo)  
Ctrl+L           Powtórz przeszukiwanie (Repeat search)  
________________________________________________________________ 
 
 
Rozkazy ogólnego przeznaczenia: 
________________________________________________________________ 
F1              Wyświetl pomoc - Help screen  
F2              Zapisz bieżący stan tekstu na dysk (Save)  
F3              Otwórz nowy plik (Open)  
F4              Uruchom i wykonaj program do pozycji wskazanej  
                kursorem  
F5              Powiększ (maximize) bieżące aktywne okno  
F6              Przejdź do następnego okna (next window)  
F7              Wykonaj program krok-po-kroku  
F8              Wykonaj program krok-po-kroku z pominięciem  
                śledzenia funkcji  
F9              Skompiluj i skonsoliduj program (Compile/Make)  
F10             Uaktywnij pasek głównego menu  
Shift+F1        Wyświetl spis treści Help - tzw. Help index  
Shift+F2        Wybierz rozkaz Arguments... z menu Run  
                (uruchamianie programu z parametrami w  
                DOS'owskim wierszu rozkazu)  
Ctrl+F1         Podpowiedzi kontekstowe (help topic search)  
Ctrl+F2         Wyzeruj bieżący program  
Ctrl+F5         Zmień pozycję aktywnego okna  
Ctrl+F7         Wyświetl okienko dialogowe "Add Watch"  
Ctrl+F8         Zaznacz punkt krytyczny (Toggle breakpoint)  
Ctrl+F9         Uruchom program (Run) 
Ctrl+PgUp       Skocz na początek pliku  
Ctrl+PgDn       Skocz na koniec pliku  
Alt+F1          Pokaż poprzedni ekran Help  
Alt+F2          Zmniejsz okno  
Alt+F3          Zamknij aktywne okno  
Alt+F4          Dokonaj inspekcji (inspect)  
Alt+F5          Pokaż DOS'owski ekran roboczy (User screen)  
Alt+F7          Przejdź do poprzedniego błędu (previous error)  
Alt+F8          Przejdź do następnego błędu (next error)  
________________________________________________________________ 

________________________________________________________________
EOF

20

background image

LEKCJA 4. Jeszcze o IDE C++ . 

 
_______________________________________________________________  
W trakcie tej lekcji:  
1. Dowiesz się więcej o menu i okienkach w środowisku IDE.  
2. Poznasz trochę technik "myszologicznych". 
3. Napiszesz i uruchomisz swój drugi program.  
________________________________________________________________ 
 
 
W dolnej części ekranu jest podobny pasek do paska menu,  
niemniej ważny, choć o innym przeznaczeniu. Pasek ten jest to  
tzw. WIERSZ STATUSOWY (ang. Status Line). Jak wynika z nazwy w  
tym wierszu wyświetlane są informacje dotyczące bieżącego stanu  
(i bieżących możliwości) środowiska IDE. Zaryzykuję tezę, że  
często jeden prosty, własny eksperyment może być więcej wart niż 
 
wiele stron opisów. Poeksperymentujmy zatem chwilę z wierszem  
statusowym.  
 

NIE CHCE SIĘ URUCHOMIĆ ???  

________________________________________________________________ 
Jeśli przy starcie kompilatora C++ nastąpi komunikat:  
 
               System Message 
          Disk is not ready in drive A 
            [Retry]      [Cancel]  
 
(Komunikat systemu C++: Dyskietka w napędzie A nie gotowa do  
odczytu; Jeszcze raz? Zrezygnować?)  
 
to znaczy, że C++ nie może odtworzyć ostatniego ekranu  
roboczego, ponieważ nie udostępniłeś mu dyskietki z programami,  
nad którymi ostatnio pracowałeś. 
________________________________________________________________ 
 
 
W wierszu statusowym wyjaśnione jest działanie klawiszy  
funkcyjnych F1, F2, itd. Mogą tam również pojawiać się  
krótkie napisy-wyjaśnienia dotyczące np. rozkazu wyróżnionego  
właśnie w menu. Powinien tam być napis:  
 
F1 Help  F2 Save  F3 Load  AltF9 Compile  F9 Make  F10 Menu  
 
znaczy to:  
 
[F1] - Pomoc  
[F2] - Zapamiętanie bieżącego pliku na dysku pod bieżącą nazwą  
       (nawet jeśli tą nazwą jest NONAME01.CPP, tzn. została nadana  
       automatycznie i znaczy - o ironio - "BEZNAZWY01.CPP") i w  
       bieżącym katalogu.  
[F3] - Załadowanie do okienka edycyjnego nowego pliku tekstowego 
       (np. nowego programu).  
[Alt]-[F9] - Kompilacja w trybie "Compile".  

21

background image

[F9] - Kompilacja w trybie "Make" (jednoczesnej kompilacji i  
       konsolidacji). 
[F10] - Uaktywnienie głównego menu.  
 
 

JAK ZROBIĆ PORZĄDEK?  

 
W trakcie uruchamiania kompilator korzysta z plików  
zewnętrznych. C++ stara się być USER FRIENDLY (przyjazny wobec  
użytkownika) i odtworzyć taki stan ekranu, w jakim ostatnio  
przerwałeś pracę, co nie zawsze jednak jest korzystne. W wierszu 
 
statusowym pojawiają się napisy informujące o tym (np: 

Loading  Desktop File . . . 

- ładuję plik zawierający konfigurację  ostatniego ekranu 
roboczego...). Jeśli chcesz by na początku  
sesji z C++ ekran był "dziewiczo" czysty, powinieneś:  
 
* zmienić nazwę pliku [D:]\BORLANDC\BIN\TCDEF.DSK  
 
na dowolną inną, np. STARY.DSK lub STARY1.DSK, stosując  
polecenie systemu DOS RENAME. [D:] oznacza odpowiedni dla  
Twojego komputera dysk. C++ wystartuje wtedy z czystym ekranem i 
 
utworzy nowy plik TCDEF.DSK.  
 
* Plików TCDEF nie należy usuwać. Kiedy nabierzesz trochę wprawy 
 
pliki te znacznie przyspieszą i ułatwią Ci pracę z C++.  
 
Aby zamknąć zbędne okna możesz zastosować również rozkaz Close  
(ang. Close - zamknij) z menu Window (okna). Zwróć uwagę, że  
polecenie Close odnosi się do bieżącego okna wyróżnionego przy  
pomocy podwójnej ramki. Aby zamknąć bieżące okno, powinieneś:  
 
1. Nacisnąć klawisze [Alt]-[W]  
   Rozwinie się menu Windows.  
2. Wybrać z menu rozkaz Close - [C].  
 
Może pojawić się okienko z ostrzeżeniem:  
 
WARNING: A:\PIERWSZY.CPP not saved. Save?  
(UWAGA: plik A:\PIERWSZY.CPP nie zapisany na dysku. Zapisać ?). 
 
[???] ZNIKNĄŁ PROGRAM ??? 
________________________________________________________________ 
C++ chce Cię uchronić przed utratą programu, ale uważaj! Jeśli  
odpowiesz Yes - Tak ([Y] lub [Enter]), to nowa wersja programu  
zostanie nadpisana na starą! 
________________________________________________________________ 
 
[!!!] ZAMYKANIE OKIEN.  
________________________________________________________________ 
Możesz szybciej zamknąć okno naciskając kombinację klawiszy  
[Alt]-[F3]. 
________________________________________________________________ 
 

22

background image

[!!!]UWAGA  
________________________________________________________________ 
Bądź ostrożny podejmując decyzję o zapisie wersji programu na  
dysk. Okienko z ostrzeżeniem pojawi się za każdym razem przed  
zamknięciem okna edycyjnego z tekstem programu. Jeśli przy  
zamykaniu okna nie pojawi się ostrzeżenie, to znaczy, że program 
w tej wersji, którą widzisz na ekranie został już zapisany na  
dysk. 
________________________________________________________________ 
 
 

A JEŚLI NIE CHCĘ ZAMYKAĆ OKIEN? 

 
W porządku, nie musisz. W menu Window ([Alt]-[W]) masz do  
dyspozycji rozkaz Next (następne okno). Możesz go wybrać albo  
naciskając klawisz [N], albo przy pomocy klawiszy kursora. Każde 
z okien na Twoim roboczym ekranie ma nazwę - nagłówek - np.  
NONAME00.CPP, PIERWSZY.CPP, ale nie tylko. Pierwsze dziesięć  
okien ma również swoje numery - podane blisko prawego - górnego  
rogu okna w nawiasach kwadratowych - np. [1], [2] itd.  
Posługując się tym rozkazem możesz przechodzić od okna do okna  
nie zamykając żadnego z okien. Spróbuj! 
 
Jest jeszcze inny sposób przejścia od okna do okna. Jeśli chcesz 
przejść do okna o numerze np. [1], [2], [5] itp. powinieneś  
nacisnąć kombinację klawiszy [Alt]-[1], [Alt]-[5] itp..  
Niestety, tylko pierwsze 9 okien ma swoje numerki.  
 
Możesz korzystać z listy okien (Window | List) lub klawisza  
funkcyjnego [F6]. 
 
[S] ACTIVE WINDOW - AKTYWNE OKNO.  
________________________________________________________________ 
Na ekranie może się znajdować jednocześnie wiele okien, ale w  
danym momencie tylko jedno z nich może być AKTYWNE. Aktywne  
okno, to to, w którym miga kursor i w którym aktualnie  
pracujesz. Aktywne okno jest dodatkowo wyróżnione podwójną  
ramką.  
________________________________________________________________ 
 
 

 Robi "na szaro"?  

________________________________________________________________ 
 
Zwróć uwagę, że dopóki bieżącym aktywnym oknem jest okienko  
komunikatów (Message - to w dolnej części ekranu), nie możesz  
np. powtórzyć kompilacji programu. Rozkazy Compile | Compile i  
Run | Run będą "zrobione na szaro" (ang. grayed out) - czyli  
nieaktywne. Najpierw trzeba przejść do okna edycji tekstu  
programu (np. poprzez kliknięcie myszką).  
________________________________________________________________ 
 
 
Rozwiń menu Options (opcje). 
Możesz to zrobić na wiele sposobów. Najszybciej chyba naciskając: 

[Alt]+[O] 

23

background image

Rozwinęło się menu,  udostępniając następującą listę poleceń:  
 
FULL MENUs - Pełne Menu ("s" oznacza, że chodzi o "te" menu w  
             liczbie mnogiej, a nie o pojedyncze menu).  
COMPILER -   Kompilator.  
MAKE... -    dosł. "ZRÓB", dotyczy tworzenia "projektów" (zwróć  
             uwagę na wielokropek [...]).  
DIRECTORIES... - KATALOGI (znów wielokropek !).  
ENVIRONMENT... - OTOCZENIE lub inaczej ŚRODOWISKO.  
SAVE -           ZAPAMIĘTAJ     (UWAGA: To jest zupełnie inne SAVE niż
                 w menu  File. Nie wolno mylić tych poleceń. 
                 Pomyłka grozi utratą tekstu programu!).  
 
Popatrz na linię statusową. Jeśli będziesz poruszać się po menu  
Option, podświetlając kolejne rozkazy, w wierszu statusowym  
będzie wyświetlany krótki opis działania wskazanego rozkazu. I  
tak, powinieneś zobaczyć kolejno następujące napisy:  
 
Full Menus [Off/On]- Use or don't use full set of menu commands. 

(Stosuj lub nie stosuj pełnego zestawu rozkazów w menu -  
domyślnie przyjmowane jest Off/Nie).  
 
Compiler - Set compiler defaults for code generation, error  
messages and names.  

(Ustaw domyślne parametry pracy kompilatora dotyczące  
generowania kodu programu, komunikatów o błędach i nazw).  
 
Make... - Set condition for project-makes.  
(Ustawianie warunków do tworzenia projektu).  
 
Directories... - Set path for compile, link and executable  
files.  
(Wybierz katalogi i ustaw ścieżki dostępu dla kompilacji,  
konsolidacji i WSKAŻ MIEJSCE - GDZIE ZAPISAĆ PLIK TYPU *.EXE po  
kompilacji).  
 
Environment... - Make environment wide settings (eg, mouse  
settings).  
(Ustawienie parametrów rozszerzonego otoczenia, np. parametrów  
pracy myszki).  
 
Save - Save all the settings you've made in the Options menu.  

(Powoduje zapamiętanie na dysku wszystkich zmian parametrów  
roboczych IDE, które ustawiłeś, korzystając z rozkazów  
dostępnych za pośrednictwem menu Options.).  

Ten rozkaz pozwala Ci ustawić konfigurację IDE "raz na zawsze".  
 
Przygotujmy się do powtórzenia kompilacji programu PIERWSZY.CPP. 
 
Jeśli masz na ekranie rozwinięte menu Options, wybierz z menu  
polecenie Directories... .  
 

KOMPILACJA ZE WSKAZANIEM ADERSU. 

 

24

background image

1. Wskaż w menu polecenie Directories i naciśnij [Enter]. 
Po poleceniu umieszczony jest wielokropek. Znaczy to, że rozkaz  
nie zostanie wykonany, zanim komputer nie uzyska od Ciebie  
pewnych dodatkowych informacji. Wiesz już, że praktycznie  
oznacza to dla Ciebie konieczność "wypełnienia" okienka  
dialogowego. Po wybraniu polecenia Directories ukazało się  
okienko dialogowe już "wstępnie wypełnione". Takie "wstępne  
wypełnienie" okienka daje Ci pewne dodatkowe informacje. Wynika  
z niego mianowicie JAKIE PARAMETRY SĄ PRZYJMOWANE DOMYŚLNIE  
(default).  
 
W okienku dialogowym masz trzy okienka tekstowe:  
 
* Include Directories (Katalog zawierający pliki nagłówkowe, np. 
 
STDIO.H, CONIO.H, GRAPHICS.H itp. dołączane do programów).  
 
* Library Directories (Katalog zawierający gotowe biblioteki,  
zawarte w plikach typu *.LIB,).  
 
* Output Directory (Katalog wyjściowy, w którym po kompilacji  
będą umieszczane Twoje programy w wersji *.EXE).  
 
Pierwsze dwa zostawimy w spokoju. 
 
2. Naciśnij dwukrotnie klawisz [Tab]. Kursor wskazuje teraz  
okienko tekstowe Output Directory. 
 
3. Wpisz do okienka tekstowego Output Directory: 
A:\ lub C:\C-BELFER 
 
znaczy to, że od teraz po wykonaniu kompilacji i utworzeniu  
pliku wykonywalnego typu *.EXE, plik taki zostanie zapisany we  
wskazanym katalogu i na wskazanym dysku/dyskietce. 
  
UWAGA: 
________________________________________________________________ 
* Jeśli zainstalowałeś zawartość dyskietki na dysku i wolisz  
posługiwać się własnym katalogiem roboczym - wpisz tam  
odpowiednią ścieżkę dostępu - np. C:\C-BELFER. Jeśli Twój  
katalog zagnieżdżony jest głębiej (np. w przypadku użytkowników  
sieci Novell) - podaj pełną ścieżkę dostępu - np.:  
F:\USERS\ADAM\C-BELFER  
* Wszędzie, gdzie w treści książki odwołuję się do dyskietki A:  
możesz konsekwentnie po zainstalowaniu stosować odpowiedni  
katalog na dysku stałym, bądź na dysku sieciowym. 
________________________________________________________________ 
 
4. Naciśnij [Enter]. 
 
Spróbuj teraz, znaną z poprzedniej lekcji metodą, wczytać do  
okienka edytora Twój pierwszy program. Musisz wykonać  
następujące czynności:  
 
1. Włóż do napędu A: dyskietkę z programem PIERWSZY.CPP (jeśli  
jeszcze jej tam nie ma). 
2. Rozwiń menu File, naciskając kombinację klawiszy [Alt]-[F].  
3. Wybierz z menu rozkaz Open, naciskając klawisz [O].  
Pojawi się znane Ci okienko dialogowe. Zwróć uwagę na wiersz  
statusowy. Napis:  

25

background image

 
Enter directory path and file mask  
znaczy:  
Wpisz ścieżkę dostępu do katalogu i "wzorzec" nazwy pliku.  
 
Użyte słowo "wzorzec" oznacza, że wolno Ci wpisać do okienka  
tekstowego także nazwy wieloznaczne, zawierające znaki "*" i  
"?", np.:  
 
*.C  
A:\???.C  
D:\BORLANDC\SOURCE\P*.*  
 
itp. (Spróbuj!, zawsze możesz się wycofać lub zmienić zdanie,  
posługując się klawiszami [BackSpace], [Shift], [Tab] i [Esc].). 
 
Klawisz [Tab] umożliwia Ci skok od okienka do okienka "do  
przodu", a [Shift]-[Tab] - "do tyłu". Zgodnie z nazwą (ang.  
ESCape - uciekać), klawisz [Esc] pozwala Ci wycofać się z  
niewygodnych sytuacji - np. zamknąć okienko dialogowe lub zwinąć 
rozwinięte menu bez żadnej akcji. 
 
Jeśli wpiszesz wzorzec nazwy, to w okienku z listą zobaczysz  
wszystkie pliki wybrane z podanego dysku i z podanego katalogu  
według zadanego wzorca. Aby wybrać plik z listy należy klawiszem 
 
[Tab] przejść do okienka z listą, klawiszami kursora wskazać  
potrzebny plik i nacisnąć [Enter].  
 
4. Wpisz do okienka tekstowego  
A:\PIERWSZY.CPP  
5. Naciśnij [Enter].  
 

SZYBKI START.  

________________________________________________________________ 
Jeśli chcesz by C++ automatycznie wczytał Twój program do  
okienka edytora, to możesz zadać nazwę pliku z tekstem programu  
jako parametr w wierszu polecenia, uruchamiając C++ np. tak:  
 
BC A:\PIERWSZY.CPP  
 
Jeśli korzystasz z programu Norton Commander, to możesz dodać do 
 
pliku NC.EXT następujący wiersz:  
 
C: TC !.!  
cpp: bc !.!  
 
wówczas wystarczy tylko wskazać odpowiedni plik typu *.C lub  
.CPP z tekstem programu i nacisnąć [Enter].  
________________________________________________________________ 
 
Kompilatory Borlanda mogą w różnych wersjach nazywać się różnie: 
 
TC.EXE, BC.EXE, BCW.EXE (dla Windows), itp.. Sprawdź swoją  
wersję kompilatora i wpisz właściwe nazwy dodając ewentualnie  
ścieżki dostępu - np.:  
 
C:      D:\BORLANDC\BIN\BC !.!  

26

background image

CPP:    WIN C:\BORLANDC\BIN\BCW !.! 
 
[!!!]UWAGA  
________________________________________________________________ 
Rozkazy uruchamiające kompilator mogą być złożone nawet z 4  
parametrów - np.:  
 
WIN /3 C:\BORLANDC\BIN\BCW C:\C-BELFER\PROGRAMY\P027.CPP  
 
spowoduje:  
* uruchomienie Windows w trybie rozszerzonym 386  
* uruchomienie kompilatora w wersji dla Windows - BCW.EXE  
* załadowanie pliku z programem - P27.CPP z wskazanego katalogu 
________________________________________________________________ 
 

[P002.CPP] 
 

Dokonaj w swoim programie następujących zmian:  

________________________________________________________________ 
#include (stdio.h>  
#include <conio>  
 
main()  
{  
  printf("\n"); 
  printf("Autor: np. Antoni Kowalski\n");  
  printf("program: PIERWSZY.CPP \n - wersja II \n");  
  getch();  

________________________________________________________________

******Uwaga: Jeśli pracujesz w Windows - Z TEGO MIEJSCA********

przy pomocy rozkazów Edit | Copy
możesz przenieść program do okna kompilatora
poprzez schowek Windows (Clipboard).
W oknie kompilatora należy:
1. Otworzyć nowe okno edytora tekstowego:
File | New
2. Wstawić plik ze schowka:
Edit | Paste
--- To okno (AM-Edit) i całego BELFRA możesz w tym czasie zredukować
--- Do ikonki.------------------------------------------------------
********************************************************************
 
Dzięki dodaniu do tekstu programu funkcji getch(), program nie  
powinien już tak szybko mignąć na ekranie i zniknąć. Zatrzyma  
się teraz i zaczeka na przyciśnięcie klawisza. Funkcja getch(),  
działa podobnie do:  
 
10 IF INKEY$="" GOTO 10  
 
w Basicu lub Readln w Pascalu.  
 
Nazwa pochodzi od GET CHaracter (POBIERZ ZNak, z klawiatury). 
 
Skompiluj program PIERWSZY.CPP. Aby to zrobić, powinieneś:  
 

27

background image

1. Rozwinąć menu Compile - [Alt]-[C].  
2. Wybrać z menu rozkaz Compile - [C].  
 
Ostrzeżenie WARNING na razie ignorujemy.  
 
Wykonaj kompilację programu powtórnie przy pomocy rozkazu Run z  
menu Run. Naciśnij kolejno klawisze:  
 
[Alt]-[R], [R]  
lub  
[Alt]-[R], [Enter]  
 
Ten sam efekt uzyskasz naciskając kombinację klawiszy  
[Ctrl]-[F9]. 
 
Uruchom program powtórnie naciskając kombinację klawiszy  
[Alt]-[R], [R]. Zwróć uwagę, że teraz kompilacja nastąpi  
znacznie szybciej. Tak naprawdę C++ stwierdzi tylko, że od  
ostatniej kompilacji nie dokonano żadnych zmian w programie i  
odstąpi od zbędnej kompilacji. Takie właśnie znaczenie ma  
komunikat "Checking dependences" (sprawdzam zależności, który  
mignie w okienku kompilacji. Po korekcie programu napisy  
wyglądają znacznie przyzwoiciej, prawda? Po obejrzeniu napisów  
naciśnij [Enter]. 
 
Możemy teraz wyjść z programu C++. Rozwiń menu File naciskając  
klawisze [Alt]-[F] i wybierz z menu rozkaz Quit. Pojawi się  
okienko z ostrzeżeniem:  
 
WARNING: A:\PIERWSZY.CPP not saved. Save?  
(UWAGA: plik A:\PIERWSZY.CPP nie zapisany na dysku. Zapisać ?). 
 
W ten sposób C++ ZNOWU chce Cię uchronić przed utratą programu,  
ale uważaj! Jeśli odpowiesz Tak ([Y] lub [Enter]), to nowa  
wersja programu zostanie nadpisana na starą! Jeśli odpowiesz Nie 
 
[N]

na dysku pozostanie stara wersja programu a nowa  
zniknie. 
 
Po wyjściu z C++ znajdziesz się w jego katalogu roboczym, lub w  
tym katalogu bieżącym, z którego wydałeś rozkaz uruchomienia  
kompilatora C++. Aby uruchomić swój program musisz zatem wydać  
następujący rozkaz:  
 
A:\PIERWSZY.EXE  
lub krócej  
A:\PIERWSZY  
 
a jeśli chcesz się przekonać, czy Twój program jest tam, gdzie  
powinien być, możesz go zobaczyć. Napisz rozkaz 
 
DIR A:\ 
lub  
DIR A:\*.EXE 
 
Aby upewnić się całkowicie, że to właśnie ten program, zwróć  
uwagę na datę i czas utworzenia pliku. Jeśli masz prawidłowo  
ustawiony zegar w swoim komputerze, data powinna być dzisiejsza  

28

background image

a czas - kilka minut temu. Jeśli coś jest nie tak, powinieneś  
przy pomocy rozkazów systemu DOS: DATE i TIME zrobić porządek w  
swoim systemie. O takich drobiazgach warto pamiętać. Pozwoli Ci  
to w przyszłości odróżnić nowsze i starsze wersje programów,  
uniknąć pomyłek i zaoszczędzić wiele pracy.  
 
[Z] 1. - Propozycja zadania - ćwiczenia do samodzielnego wykonania.
------------------------------------------------------------------- 

Spróbuj odszukać plik żródłowy .CPP i plik wynikowy .EXE  
wychodząc "na chwilę" z IDE przy pomocy rozkazu File | DOS  
Shell. 
-------------------------------------------------------------------
 
A teraz zajrzyjmy do środka do pliku PIERWSZY.EXE. Jeśli  
korzystasz z programu Norton Commander, to masz do dyspozycji  
opcje [F3] - View (przeglądanie) i [F4] - Edit (edycja). Jeśli  
nie korzystasz z NC, musisz wydać następujący rozkaz:  
 
TYPE A:\PIERWSZY.EXE | C:\DOS\MORE  
lub  
C:\DOS\EDIT A:\PIERWSZY.EXE 
 
Jak widzisz na ekranie, napisy zawarte w programie pozostały  
czytelne, ale to co widać dookoła nie wygląda najlepiej. Na  
podstawie tego co widzisz, można (na razie ostrożnie) wysnuć  
wniosek, że ani Viewer (przeglądarka), ani Edytor, które  
doskonale spisują się przy obróbce plików tekstowych, nie nadają 
się do analizy i obróbki programów w wersji *.EXE. Narzędziami,  
które będziemy musieli stosować, mogą być programy typu  
DEBUGGER, PROFILER, LINKER (konsolidator), kompilator i in.. 
 
Mam nadzieję, że czujesz się w środowisku IDE już trochę  
swobodniej, a więc bierzemy się za drugi program.  
__________________________________________________________ 
EOF

LEKCJA 5 - DZIAŁANIA PRZY POMOCY MYSZKI I BŁĘDY W 
PROGRAMIE. 

________________________________________________________________ 
Z tej lekcji dowiesz się,  
* Jak posługiwać się myszką w środowisku IDE (DOS)  
* O czy należy pamiętać, przy tworzeniu i uruchamianiu  
programów.  
* Jak poprawiać błędy w programie. 
________________________________________________________________ 
 
 
Zanim będzie można kontynuować eksperymenty, trzeba coś zrobić,  
by robocze okno edytora było puste. Aby otworzyć takie nowe  
puste okno edytora należy:  
 
* Rozwinąć menu File;   
* Wybrać z menu rozkaz New (nowy).   

29

background image

 
Na ekranie monitora otworzy się nowe puste okno zatytułowane   
"NONAME00.CPP", "NONAME01.CPP", itp (lub "bez nazwy" i o  
kolejnym numerze). Różne edytoro-podobne aplikacje mają zwyczaj  
otwierania okna dla nowego pliku tekstowego i nadawanie mu na  
początku jednej z dwóch nazw: 
 

SŁOWNICZEK: UFO w trybie Edycji  

________________________________________________________________ 
Untitled - niezatytułowany  
Noname - bez nazwy  
(Tak na marginesie UFO to skrót od Unidentified Flying Object -  
Niezidentyfikowany Obiekt Latający, gdy przejdziemy do  
programowania obiektowego, znajomość tego terminu też Ci się  
przyda).  
________________________________________________________________ 
 
Nadanie plikowi dyskowemu z tekstem źródłowym programu jego  
właściwej nazwy i zapisanie go na dysku stałym komputera w  
określonym miejscu następuje w tym momencie, kiedy po napisaniu  
programu zapisujesz go na dysk rozkazem: 
 
File | Save     lub      File | Save As... 
 
Zapis File | Save oznacza "Rozkaz Save z menu File". Gdy po  
opracowaniu programu rozwiniesz menu File i wybierzesz rozkaz  
Save as... (zapisz jako...), pojawi się okienko dialogowe "Save  
File as" (zapis pliku jako...).  
 
Do okienka edycyjnego "Name" (nazwa) możesz wpisać nazwę, którą  
chcesz nadać swojemu nowemu programowi. Zwróć uwagę, że możesz  
podać nazwę pliku i jednocześnie wskazać miejsce - np.: 
 
Name: 
F:\USERS\ADAM\PROBY\PROGRAM.CPP 
 
Po wpisaniu nazwy naciśnij klawisz [Enter] lub wybierz klawisz  
[OK] w okienku dialogowym myszką. Tytuł okna edytora zmieni się  
na wybraną nazwę.  
  
Możesz również (jeśli odpowiedni katalog już istnieje), wskazać  
właściwy katalog w okienku z listą "Files" i dwukrotnie  
"kliknąć" lewym klawiszem myszki.  
 
Możesz wskazać myszką okienko edycyjne i nacisnąć lewy klawisz  
myszki, bądź naciskać klawisz [Tab] aż do momentu, gdy kursor  
zostanie przeniesiony do okienka edycyjnego. Okienko edycyjne to 
to okienko, do którego wpisujesz nazwę pliku. W okienku  
edycyjnym (Save File As) naciskaj klawisz [BackSpace] aż do  
chwili skasowania zbędnej nazwy pliku i pozostawienia tam tylko  
ścieżki dostępu - np. A:\PROBY\. Wpisz nazwę programu - np.  
PROG1.CPP. Po wpisaniu nazwy możesz nacisnąć [Enter] lub wskazać 
myszką klawisz [OK] w okienku i nacisnąć lewy klawisz myszki.  
Jeśli tak zrobisz w przypadku pustego okienka NONAME00.CPP -  
kompilator utworzy na dysku we wskazanym katalogu plik o zadanej 
nazwie - np. A:\PROBY\PROGR1.CPP (na razie pusty). Zmieni się  
także nagłówek (nazwa) okienka edycyjnego na ekranie roboczym.  
  
[!!!]UWAGA.  

30

background image

________________________________________________________________ 
Wszystkie pliki zawierające teksty programów w języku C++   
powinny ˙mieć charakterystyczne rozszerzenie *.CPP (CPP to skrót 
od C Plus Plus), lub .C. Po tym rozszerzeniu rozpoznaje te  
programy kompilator. Nadanie rozszerzenia .C lub .CPP może  
dodatkowo wpływać na sposób kompilacji programu. Zanim wyjaśnimy 
te szczegóły, będziemy zawsze stosować rozszerzenie .CPP.  
Wszelkie inne rozszerzenia (.BAK, .TXT, .DEF, itp.) nie  
przeszkadzają w edycji i kompilacji programu, ale mogą w  
niejawny sposób wpłynąć na sposób kompilacji. 
________________________________________________________________ 
 
Jeśli masz puste robocze okno edytora - możesz wpisać tam  
swój własny nowy program. Wpisz:  
  
void main(void)  
  
Każdy program w C++ składa się z instrukcji. Wiele takich  
instrukcji to wywołania funkcji. W C++ rozkaz wywołania i  
wykonania funkcji polega na wpisaniu nazwy funkcji (bez żadnego  
dodatkowego słowa typu run, execute, load, itp.). Tych funkcji  
może być w programie jedna, bądź więcej. Tworzenie programu w  
C++ z zastosowaniem funkcji (takich jakgdyby mini-programików)  
przypomina składanie większej całości z klocków.  
 
Należy podkreślić, że:  
 
każdy program w C++ musi zawierać funkcję main() (ang. main -  
główna).  
 
Wykonanie każdego programu rozpoczyna się właśnie od początku  
funkcji main(). Innymi słowy - miejsce zaznaczone w programie  
przy pomocy funkcji main() to takie miejsce, w które komputer  
zagląda zawsze na początku wykonania programu i od tego właśnie  
miejsca rozpoczyna poszukiwanie i wykonywanie rozkazów.  
  

 Entry Point

___________________________________________________________________
Punkt wejścia do programu nazywa się:
Program Entry Point
Taki właśnie punkt wejścia wskazuje słowo main().
Punk wejścia mogą mieć nie tylko programy .EXE ale także biblioteki
(.DLL - dynamicznie dołączanie biblioteki).
____________________________________________________________________

Każda funkcja powinna mieć początek i koniec. Początek funkcji w 
C/C++ zaznacza się przez otwarcie nawiasów klamrowych { a koniec  
funkcji poprzez zamknięcie } . Początek głównej funkcji main()  
to zarazem początek całego programu. Zaczynamy zwykle od  
umieszczenia w oknie edytora C++  znaków początku i końca  
programu.  
 
main()
{
        <-- tu rozbudowuje się tekst programu
}

31

background image

Najpierw naciśnij [Enter] i przejdź do początku nowej linii.  
Umieść w tej nowej linii znak początku programu - nawias { (lewy 
nawias klamrowy). Następnie naciśnij [Enter] powtórnie i umieść  
w następnej linii prawy nawias klamrowy - }.  
 
[!!!] NAJPIERW Save !!! 
________________________________________________________________ 
Zanim jeszcze skończysz redagowanie programu i sięgniesz do  
klawiszy [Alt]+[R], pamiętaj, że przed próbami kompilacji i  
uruchomienia programu zawsze NAJPIERW należy zapisać program na  
dysk. Jeśli przy próbach uruchomienia coś pójdzie nie tak - masz 
pewność, że Twoja praca nie pójdzie na marne. Czasami przy  
próbach uruchamiania programów zdarza się, że błędy mogą  
spowodować zawieszenie komputera. Programista jest wtedy  
zmuszony do restartu komputera, przy wyłączeniu komputera to, co 
było tylko na ekranie i tylko w pamięci operacyjnej - niestety  
znika bezpowrotnie. 
________________________________________________________________ 
 
Aby zapisać tekst programu na dysk należy:   
 
* Wybrać z menu rozkaz File | Save As... albo  
* Nacisnąć klawisz funkcyjny [F2] (działa jak File | Save) 
 
Po wydaniu rozkazu Save możesz być pewien, że Twój program jest 
bezpieczny i komputer może się spokojnie "ZAWIESIĆ" nie czyniąc szkody.  
Aby skompilować i uruchomić ten program należy:   
 
* Wybrać rozkaz Run | Run 
* Nacisnąć kombinację klawiszy [Ctrl]+[F9]   
 
Podobnie jak wcześniej, kompilator wyświetli na ekranie okienko  
zawierające komunikaty o przebiegu kompilacji. Po zakończeniu  
kompilacji nastąpi  wykonanie programu. Na moment mignie roboczy 
ekran użytkownika. Na nieszczęście program nic nie robi, więc  
nic się tam nie wydarzy.  
 
Aby przeanalizować, jak kompilator C++ reaguje na błędy w  
programach, zmień tekst w pierwszej linii programu na błędny:  
  
vod main(void)   
 {  
 }  
 
Spróbuj powtórnie skompilować i uruchomić program.   
 
Kompilator wyświetli okienko, w którym pojawi się komunikat o  
błędach. W taki właśnie sposób kompilator taktownie informuje  
programistę, że nie jest aż taki dobry, jak mu się czasami  
wydaje. Komputer jest niestety pedantem. Oczekuje (my, ludzie  
tego nie wymagamy) absolutnej dokładności i żelaznego  
przestrzegania pewnych zasad. "Zjadając" jedną literę naruszyłeś 
takie zasady, co zauważył kompilator.  
 
W górnej części ekranu kompilator wyróżnił paskiem podświetlenia 
ten wiersz programu, który zawiera błąd. W dolnej części ekranu, 
w tzw. okienku komunikatów (ang. Message window) pojawił się  
komunikat, jaki rodzaj błędu został wykryty w Twoim programie. W 
 
danym przypadku komunikat brzmi:   

32

background image

  
Declaration syntax error - Błąd w składni deklaracji   
  

Co to jest deklaracja?  

 
Pierwsza linia (wiersz) funkcji nazywa się deklaracją funkcji.  
Taka pierwsza linia zawiera informacje ważne dla kompilatora:  
nazwę funkcji oraz tzw. typy wartości używanych przez funkcję.  
Komunikat o błędzie oznacza, że nieprawidłowo została napisana  
nazwa funkcji lub nazwy typów wartości, którymi posługuje się  
funkcja. W naszym przypadku słowo void zostało przekręcone na  
"vod", a słowo to ma w C++ specjalne znaczenie. Słowo "void"  
jest częścią języka C++, a dokładniej - słowem kluczowym (ang.  
keyword).   
  
[S] Function declaration - Deklaracja funkcji.  
    Keyword - Słowo kluczowe. 
________________________________________________________________ 
 
Function declaration - Deklaracja funkcji.  
Pierwszy wiersz funkcji jest nazywany deklaracją funkcji. Ten   
wiersz zawiera informacje dla kompilatora C++ pozwalające  
poprawnie przetłumaczyć funkcję na kod maszynowy.  
 

Keyword - Słowo kluczowe. 

to specjalne słowo wchodzące w skład języka programowania. Słowa 
 
kluczowe to słowa o zastrzeżonym znaczeniu, które można stosować 
w programach wyłącznie w przewidzianym dla nich sensie.  
________________________________________________________________ 
 
 
Popraw błąd w tekście. Aby robocze okienko edytora stało się  
oknem aktywnym, wskaż kursorem myszki dowolny punkt w oknie  
edytora i naciśnij lewy klawisz myszki, albo naciśnij klawisz  
[F6]. Zmień słowo "vod" na "void". Przy pomocy klawiszy ze  
strzałkami umieść migający kursor po prawej stronie nawiasu {  
sygnalizującego ˙początek programu i naciśnij [Enter]. Spowoduje 
to wstawienie pomiędzy początek a koniec programu nowej pustej   
linii i umieszczenie kursora na początku nowego wiersza. Wpisz  
do nowego wiersza instrukcję oczyszczenia ekranu (odpowiednik  
instrukcji CLS w Basicu): 
  
  clrscr();   
  
W C++ clrscr() oznacza wywołanie funkcji czyszczącej roboczy  
ekran programu (User screen). Nazwa funkcji pochodzi od skrótu:  
 
CLeaR SCReen - czyść ekran.   
 
Że to funkcja - można rozpoznać po dodanej za nazwą parze  
nawiasów okrągłych - (). W tym jednak przypadku wiersz: 
  
  clrscr();   
  
stanowi nie deklarację funkcji, lecz wywołanie funkcji (ang.  
function call). C++ znalazłszy w programie wywołanie funkcji  

33

background image

wykona wszystkie rozkazy, które zawiera wewnątrz funkcja  
clrscr(). Nie musisz przejmować się tym, z jakich rozkazów  
składa się funkcja clrscr(). Te rozkazy nie stanowią części  
Twojego programu, lecz są zawarte w jednym z "fabrycznych"  
plików bibliotecznych zainstalowanych wraz z kompilatorem C++.   

Function  - Funkcja  

Fuction call - Wywołanie funkcji 

________________________________________________________________ 
 
Funkcja to coś przypominające mini-program. Funkcja zawiera   
listę rozkazów służących do wykonania typowych operacji (np.   
czyszczenie ekranu, wyświetlanie menu, wydruk, czy sortowanie   
listy imion). W programach posługujemy się zwykle wieloma   
funkcjami. Poznałeś już najważniejszą funkcję główną - main(). W 
C/C++ możesz posługiwać się gotowymi funkcjami (tzw.  
bibliotecznymi) a także tworzyć nowe własne funkcje. Na razie  
będziemy posługiwać się gotowymi funkcjami dostarczanymi przez  
producenta wraz z kompilatorem C++.  
________________________________________________________________ 
 
  
Włącz kompilację i próbę uruchomienia programu.  
Kompilator stwierdzi, że program zawiera błędy. 
Naciśnij dowolny klawisz, by zniknęło okienko kompilacji.  
Kompilator napisał:   
  
Error: Function 'clrscr' should have a prototype   

(Funkcja 'clrscr' powinna mieć prototyp)   
  
[???] O co mu chodzi?   
________________________________________________________________ 
Tzw. PROTOTYP funkcji to coś bardzo podobnego do deklaracji  
funkcji. Prototyp służy do przekazania kompilatorowi pewnych   
informacji o funkcji jeszcze przed użyciem tej funkcji w  
programie. Dla przykładu, gdy pisałeś pierwszą linię programu:   
  
void main(void)   
  
podałeś nie tylko nazwę funkcji - main, lecz także umieściliśmy 
tam dwukrotnie słowo void. Dokładnie o znaczeniu tych słów  
napiszemy w dalszej części książki. Na razie zwróćmy jedynie  
uwagę, że podobnych "dodatkowych" informacji dotyczących funkcji 
clrscr() w naszym programie nie ma.   
________________________________________________________________ 
  

Zwróć uwagę, że zapisy:

main()              int main(void)       main(void) {
{                   {  }                 }

}

są całkowiecie równoważne. Fakt, że słowa kluczowe void (w nawiasie) 
i int (przed funkcją i tylko tam!) mogą zostać pominięte wskazuje, że są 
to wartości domyślne (default settings) przyjmowane automatycznie.

34

background image

Funkcja clrscr() została napisana przez programistów z firmy  
BORLAND i znajduje się gdzieś w osobnym pliku dołączonym do  
kompilatora C++. Aby móc spokojnie posługiwać się tą funkcją w  
swoich programach, powinieneś dołączyć do swojego programu  
informację w jakim pliku dyskowym znajduje się opis funkcji  
clrscr(). Taki (dość szczegółowy) opis funkcji nazywa się  
właśnie prototypem funkcji. Aby dodać do programu tę (niezbędną) 
informację  
 
* naciśnij [F6] by przejść do okna edytora   
* ustaw migający kursor na początku tekstu programu   
* naciśnij [Enter] dwukrotnie, by dodać dwie nowe puste linie do 
 
  tekstu programu   
* na samym początku programu wpisz:   
  
#include <conio.h>   
  
Takie specjalne linie (zwróć uwagę na podświetlenie)  
rozpoczynające się od znaku # (ASCII 35) nie są właściwie  
normalną częścią składową programu. Nie stanowią one jednej z  
instrukcji programu, mówiącej komputerowi CO NALEŻY ROBIĆ, lecz  
stanowią tzw. dyrektywę (rozkaz) dla kompillatora C++ - W JAKI  
SPOSÓB KOMPILOWAĆ PROGRAM. Dyrektywa kompilatora (ang. compiler  
directive) powoduje dokonanie określonych działań przez  
kompilator na etapie tłumaczenia programu na kod maszynowy. W  
danym przypadku dyrektywa 

#include ....

(ang. include - włącz,  dołącz) powoduje włączenie we wskazane 
miejsce zawartości  zewnętrznego tekstowego pliku dyskowego - np.:

CONIO.H, 

(plik CONIO.H   
nazywany ˙także "plikiem nagłówkowym" znajduje się w podkatalogu 
\INCLUDE). Kompilator dołącza zawartość pliku CONIO.H jeszcze  
przed rozpoczęciem procesu kompilacji programu.   
 
Naciśnij kombinację klawiszy [Ctrl]+[F9]. Spowoduje to  
kompilację i uruchomienie programu (Run). Przykładowy program  
powinien tym razem przekompilować się bez błędów. Po dokonaniu  
kompilacji powinien szybko błysnąć ekran użytkownika. Po tym  
błysku powinien nastąpić powrót do roboczego środowiska IDE  
kompilatora C++. Jeśli nie zdążyłeś się przyjrzeć i chcesz  
spokojnie sprawdzić, co zrobił Twój program - naciśnij  
kombinację klawiszy [Alt]+[F5].  
Dzięki działaniu funkcji clrscr() ekran będzie całkowicie  
czysty.   
 

DYREKTYWA KOMPILATORA - Compiler directive

________________________________________________________________ 
 
Dyrektywa kompilatora to rozkaz wyjaśniający kompilatorowi C++ w 
jaki sposób dokonywać kompilacji programu. Dyrektywy kompilatora 
zawsze rozpoczynają się od znaku # (ang. hash).  
Kompilatory C++ posiadają pewien dodatkowy program nazywany  

35

background image

PREPROCESOREM. Preprocesor dokonuje przetwarzania tekstu  
programu jescze przed rozpoczęciem właściwej kompilacji.  
Dokładniej rzecz biorąc #include jest właściwie dyrektywą  
preprocesora (szczegóły w dalszej części książki). 
________________________________________________________________ 
 
  

Propozycje zadań do samodzielnego wykonania.

________________________________________________________________ 
1. Spróbuj poeksperymentować "zjadając" kolejno różne elementy w 
 
poprawnie działającym na początku programie:  
 
- litera w nazwie funkcji 
- średnik na końcu wiersza  
- cudzysłów obejmujący tekst do wydrukowania 
- nawias ( lub ) w funkcji printf()  
- nawias klamrowy { lub }  
- znak dyrektywy #  
- całą dyrektywę #include <stdio.h>  
 
Porównaj komunikaty o błędach i zgłaszaną przez kompilator  
liczbę błędów. Czy po przekłamaniu jednego znaku kompilator  
zawsze zgłasza dokładnie jeden błąd?  
________________________________________________________________ 
 
________________________________________________________________
EOF

LEKCJA 6 - NASTĘPNY PROGRAM - KOMPUTEROWA 
ARYTMETYKA.  

________________________________________________________________ 
W trakcie tej lekcji napiszesz i uruchomisz następny program  
wykonujący proste operacje matematyczne.  
________________________________________________________________ 
 
Aby przystąpić po wyjaśnieniach do pracy nad drugim programem,  
powinieneś wykonać następujące czynności:  
 
1. Zrób porządek na ekranie. Zamknij rozkazem Close z menu  
Window zbędne okna (możesz posłużyć się kombinacją [Alt]-[F3]).  
2. Rozwiń menu File.  
3. Wybierz z menu rozkaz Open...  
4. Wpisz do okienka tekstowego:  
A:\DRUGI.CPP  
5. Naciśnij [Enter].  
6. Wpisz do okienka edytora tekst programu: 
 

[P003.CPP ] 

/* Program przykladowy: _DRUGI.CPP */  
  
 
# include <conio.h>             /* zwróć uwagę, że tu NIE MA [;] ! */  
# include <stdio.h>             /* drugi plik nagłówkowy */ 

36

background image

 
int main()                      /* tu tez nie ma średnika [;] ! */  
{  
float x, y; 
float wynik; 
  
  clrscr();  
  printf("Zamieniam ulamki zwykle na dziesietne\n");  
  printf("\nPodaj licznik ulamka: ");  
  scanf("%f", &x);             /* pobiera liczbę z klawiatury */ 
  printf("\nPodaj mianownik ulamka: ");  
  scanf( "%f", &y); 
  
  wynik = x / y;               /* tu wykonuje sie dzielenie */  
  
  printf("\n %f : %f  = %f", x, y, wynik);  
  printf("\n nacisnij dowolny klawisz...\n");  
  
  getch();                 /* program czeka na nacisniecie klawisza. */  
  
  return 0;         //<-- zwrot zera do systemu 
}  
  

UWAGA:  
_________________________________________________________________ 
* Komentarze ujęte w [/*.....*/] możesz pominąć. Komentarz jest  
przeznaczony dla człowieka. Kompilator ignoruje całkowicie  
komentarze i traktuje komentarz jak puste miejsce, a dokładniej  
- tak samo jak pojedynczą spację. Komentarz w C++ może mieć dwie 
formy:  
 
/* Tekst komentarza */  
// Tekst komentarza  
 
w drugim przypadku ogranicznikiem pola komentarza jest koniec  
wiersza. 
 
* Spacjami i TABami możesz operować dowolnie. Kompilator  
ignoruje także puste miejsca w tekście. Nie należy natomiast  
stosować spacji w obrębie słów kluczowych i identyfikatorów. 
________________________________________________________________ 

7. Skompiluj program [Alt]-[C], [M] lub [Enter].  
 
8. Popraw ewentualne błędy.  
 
9. Uruchom program rozkazem Run, naciskając [Alt]-[R], [R].  
 
10. Zapisz wersję źródłową programu DRUGI.CPP na dyskietkę A:\  
stosując tym razem SHORTCUT KEY - klawisz [F2].  
 
[S!] scanf() - SCANing Function - Funkcja SKANująca.  
________________________________________________________________ 
Funkcja pobiera ze standardowego urządzenia wejścia- zwykle z  
klawiatury podaną przez użytkownika liczbę lub inny ciąg znaków. 
 
Działa podobnie do funkcji INPUT w Basicu, czy readln w Pascalu. 
 

37

background image

float - do Floating Point - "Pływający" - zmienny przecinek.  

Słowo kluczowe służące do tzw. DEKLARACJI TYPU ZMIENNEJ lub  
funkcji. Oznacza liczbę rzeczywistą np.: float x = 3.14;  
 

int - od Integer - całkowity.  

Słowo kluczowe służące do deklaracji typu zmiennej lub funkcji.  
Oznacza liczbę całkowitą np.: 768. 
 

#include - Włącz.  

Dyrektywa włączająca cały zewnętrzny plik tekstowy. W tym  
przypadku włączone zostały dwa tzw. pliki nagłówkowe:  
CONIO.H i STDIO.H.  
 

CONIO.H - CONsole Input/Output.  

Plik nagłówkowy zawierający prototypy funkcji potrzebnych do  
obsługi standardowego Wejścia/Wyjścia na/z konsoli (CONsole).  
Plik zawiera między innymi prototyp funkcji clrscr(), potrzebnej 
nam do czyszczenia ekranu.  
 

STDIO.H - STanDard Input/Output 

Plik nagłówkowy zawierający prototypy funkcji potrzebnych do  
obsługi standardowego Wejścia/Wyjścia na/z konsoli (Input -  
Wejście, Output - Wyjście). Plik zawiera między innymi prototyp  
funkcji printf(), potrzebnej nam do drukowania wyników na  
ekranie.  
 

return - słowo kluczowe:  Powrót, zwrot.  

Po wykonaniu programu liczba 0 (tak kazaliśmy programowi  
rozkazem return 0;) jest zwracana do systemu operacyjnego, w  
naszym przypadku do DOSa. Zwróć uwagę, że nie pojawiło się tym  
razem ostrzeżenie WARNING podczas kompilacji.  
________________________________________________________________ 
 

OPERATORY ARYTMETYCZNE C++. 

 
C++ potrafi oczywiście nie tylko dzielić i mnożyć. Oto tabela  
operatorów arytmetycznych c i C++. 
 
OPERATORY ARYTMETYCZNE języka C++.  
________________________________________________________________ 
 
Operator  Nazwa            Tłumaczenie             Działanie  
________________________________________________________________ 
 
  +       ADDition        

Dodawanie          Suma liczb  

  -       SUBstraction    

Odejmowanie       

Różnica liczb  

  *       MULtiplication   Mnożenie           Iloczyn liczb  
  /       DIVision        

Dzielenie          Iloraz liczb  

  %       MODulus         

Dziel Modulo      

Reszta z dzielenia  

38

background image

________________________________________________________________ 
 
 

Przykładowe wyniki niektórych operacji arytmetycznych.  

________________________________________________________________ 
 
Działanie (zapis w C++)      Wynik działania  
________________________________________________________________ 
 
  5 + 7                         12  
  12 - 7                         5  
  3 * 8                         24  
  10 / 3                         3.333333  
  10 % 3                         1  
________________________________________________________________ 
 
 
 

Czym różni się dzielenie   /    od    %    ?  

________________________________________________________________ 
Operator dzielenia modulo % zamiast wyniku dzielenia - daje  
rzesztę z dzielenia. Dla przykładu, dzielenie liczby 14 przez  
liczbę 4 daje wynik 3, reszta z dzielenia 2. Wynik operacji 

14%4 
 
będzie więc wynosić 2. Operator ten jest niezwykle przydatny np. 
przy sprawdzaniu podzielności, skalowaniu, określaniu zakresów  
liczb przypadkowych, itp.. 

Przykłady generacji liczb pseudolosowych 

wybiegają nieco w przyszłość, 
ale postanowiłem w Samouczku umieścić je razem. Po przestudiowaniu 
tworzenia pętli programowych możesz wrócić do tej lekcji i rozważyć
przykłady po raz wtóry.

Przykład 1:

randomize();
int X=ramdom();
X = X % 10;

Przykład 2:

---------------------
#include <stdlib.h>      /* Zwróc uwagę na dołączony plik */
#include <stdio.h>

main()
{
    int i;

    printf("Dziesięć liczb pseudo-losowych od 0 do 99\n\n");
    for(i=0; i<10; i++)

39

background image

       printf("%d\n", rand() % 100);
    return 0;
}
 

Przykad3

--------------------
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

void main()
 {
    randomize();
    printf("Liczby pseudolosowe z zakresu: 0-99 --> %d\n", random (100));
}

Przykład 4

-----------------
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

int main(void)
{
    int i;

    randomize();
    printf("Liczby pseudolosowe:  0 to 99\n\n");
    for(i=0; i<10; i++)
        printf("%d\n", rand() % 100);
    return 0;
}

Zwróć uwagę, że to randomize() uruchamia generator liczb pseudolosowych,
czyli jakgdyby "włącza bęben maszyny losującej".
________________________________________________________________ 
 
 

Wykonaj z programem DRUGI.CPP kilka eksperymentów.  

 
[Z]  
________________________________________________________________ 
1. Zamień operator dzielenia na operator mnożenia [*]: 
 
wynik = x * y;     /* tu wykonuje sie mnożenie */ 
 
i napis w pierwszej funkcji printf() na np. taki:  
 
printf( "Wykonuje mnozenie liczb" ); 
 
Uruchom program. Sprawdź poprawność działania programu w  
szerokim zakresie liczb. Przy jakiej wielkości liczb pojawiają  
się błędy?  
 
2. Zmień nazwy zmiennych x, y, wynik na inne, np.:  
to_jest_liczba_pierwsza,  

40

background image

to_jest_liczba_druga,  
itp.  
Czy C++ poprawnie rozpoznaje i rozróżnia takie długie nazwy?  
Kiedy zaczynają się kłopoty? Sprawdź, czy można w nazwie  
zmiennej użyć spacji? Jaki komunikat wyświetli kompilator? 
________________________________________________________________ 
 

PRZEPADŁ PROGRAM ???  

________________________________________________________________ 
Nie przejmuj się. Wersja początkowa programu DRUGI.CPP jest na  
dyskietce dołączonej do niniejszej książki (tam nazywa się  
DRUGI.CPP). 
Zwróć uwagę, że kompilator C++ tworzy automatycznie kopie  
zapasowe plików źródłowych z programami i nadaje im standardowe  
rozszerzenie *.BAK. Zanim zatem zaczniesz się denerwować,  
sprawdź, czy kopia np. DRUGI.BAK nie jest właśnie tą wersją  
programu, która Ci "przepadła". 
________________________________________________________________ 
 
__________________________________________________________________
EOF

LEKCJA 7. Z czego składa się program. 

_______________________________________________________________  
W trakcie tej lekcji:  
* Dowiesz się co robić, jeśli tęsknisz za Pascalem.  
* Zapoznasz się wstępnie z preprocesorem C++. 
* Poznasz dokładniej niektóre elementy języka C++.  
_______________________________________________________________  
 
Zanim zagłębimy się w szczegóły działania preprocesora i  
kompilatora, dla zilustrowania mechanizmu działania dyrektyw  
popełnimy żart programistyczny. Nie ma nic gorszego niż spalić  
dobry żart, upewnijmy się więc najpierw, czy nasza  
"czarodziejska kula" jest gotowa do magicznych sztuczek.  
Sprawdź, czy na dyskietce znajdują się pliki  
 
A:\PASCAL.H  
A:\POLTEKST.H 
 
Jeśli nie, to przed zabawą w magiczne sztuczki programistyczne  
musisz odtworzyć te pliki z zapasowej kopii dyskietki, którą  
sporządziłeś przed rozpoczęciem LEKCJI 1.  
 
Jeśli masz już oba pliki, to wykonaj następujące czynności:  
 
1. Włóż do napędu A: dyskietkę z plikami PASCAL.H i POLTEKST.H. 
2. Uruchom kompilator C++.  
 

PROGRAMY HOKUS.EXE i POKUS.EXE - czyli sztuczki z Preprpcesorem C++ 

 
1. Zrób porządek na ekranie - pozamykaj zbędne okna.  
2. Naciśnij klawisz [F3]. Pojawi się znajome okienko dialogowe  

41

background image

"Open".  
3. Wpisz do okienka tekstowego nazwę nowego programu:  
A:\HOKUS.C  
i naciśnij [Enter]. 
4. Wpisz następujący tekst programu:  
  

[P004.CPP] 

 
#include <a:\pascal.h>  
  
Program  
Begin  
  Write("Ten program jest podobny");  
  Write(" do Turbo Pascala ");  
  Write(" tak tez mozna pisac w BORLAND C++ !");  
  Readln;  
End  
  
 
5. Uruchom program [Ctrl]-[F9]. Jeśli wystąpią błędy, skoryguj  
ewentualne niezgodności z oryginałem. Ostrzeżenie "WARNING"  
możesz zignorować. 
 
UWAGA: MUSI ZOSTAĆ ZACHOWANA IDEALNA ZGODNOŚĆ z tekstem  
oryginału! 
 
6. Uruchom program rozkazem Run [Alt]-[R], [Enter]. Zwróć uwagę, 
 
że powtórna kompilacja przebiega szybciej, jeśli w międzyczasie  
nie dokonałeś zmian w programie. 
7. Zamknij okno edytora rozkazem Close (z menu Window). Zapisz  
program HOKUS.CPP w wersji źródłowej na dyskietkę A:. 
 
A teraz następna sztuczka, na którą pozwala C++.  
 

Utworzymy następny program POKUS.CPP.  

 
1. Wykonaj czynności z pp. 1 i 2 z poprzedniego przykładu.  
2. Otwórz okienko nowego programu - File | Open (np. klawiszem  
[F3]) i wpisz nazwę programu. Możesz zastosować również File |  
New.  
 
A:\POKUS.CPP  
 
3. Naciśnij [Enter].  
4. Wpisz tekst programu:  
 

[P005.CPP] 

 
# include <a:\poltekst.h>  
program  
poczatek  
czysty_ekran  
drukuj ("Ten program - POKUS.CPP ");  
drukuj ("Jest napisany po polsku ");  
drukuj ("a mimo to Turbo C++ go rozumie!");  

42

background image

czekaj;   
koniec   
  
5. Uruchom program [Alt]-[R], [R]. Jeśli wystąpią błędy,  
skoryguj ewentualne niezgodności z oryginałem. Ostrzeżenie  
"WARNING" możesz zignorować. 
UWAGA: MUSI ZOSTAĆ ZACHOWANA IDEALNA ZGODNOŚĆ! 
 
6. Zamknij okno edytora rozkazem Close (z menu Window). Zapisz  
program HOKUS.C w wersji źródłowej na dyskietkę A:. 
 

WYJAŚNIENIE SZTUCZEK - PREPROCESOR C++ CPP.EXE.  

 
A teraz wyjaśnienie naszych magicznych sztuczek. Jeśli jesteś  
niecierpliwy, na pewno już sam zajrzałeś do plików PASCAL.H i  
POLTEKST.H, bo jest chyba oczywiste od początku, że to tam  
właśnie musi ukrywać się to wszystko, co pozwala nam robić nasze 
 
hokus-pokus. Skorzystaliśmy z pewnej nie występującej ani w  
Pascalu, ani w Basicu umiejętności języków C i C++ - a  
mianowicie z PREPROCESORA.  
 
Najczęściej stosowanymi dyrektywami preprocesora są:  
 
# include - włącz  
i  
# define - zdefiniuj  
 
Do rozpoznania dyrektyw preprocesora służy znak (#) - HASH.  
 
Zwróć uwagę, że zapisy  
 
#include  
# include  
 
są całkowicie równoważne. Poza tym dyrektywy preprocesora nie  
kończą się średnikiem. 
 
Działanie preprocesora (czyli wstępne przetwarzanie tekstu  
programu jeszcze przed przystąpieniem do kompilacji) polega na  
zastąpieniu w tekście programu jednych łańcuchów znaków przez  
inne. Takie pary możemy "zadać" preprocesorowi właśnie dyrektywą 
 
#define. Nasze nagłówki wyglądają następująco:  
 
PASCAL.H:  
_______________________________________________________________ 
# include <stdio.h> 
# define Program main()  
# define Begin {  
# define Writeln printf  
# define Readln getch()   
# define End }  
________________________________________________________________ 
 
POLTEKST.H:  
________________________________________________________________ 
# include <stdio.h> 
# define program main()   

43

background image

# define poczatek {  
# define koniec }   
# define czysty_ekran clrscr();  
# define drukuj printf  
# define czekaj getch()   
________________________________________________________________ 
 
Zwróć uwagę, że warunkiem poprawnego zadziałania preprocesora  
jest zrezygnowanie ze spacji wewnątrz łańcuchów znakowych,  
spacje bowiem w preprocesorze rozdzielają dwa łańcuchy znaków -  np.

 "drukuj" 

- ten ZA KTÓRY CHCEMY COŚ PODSTAWIĆ oraz np.  

"printf" 

- ten, KTÓRY NALEŻY PODSTAWIAĆ. Często w programach  
zauważysz łańcuchy znaków pisane w dość specjalny sposób: 
 
napisy_w_których_unika_się_spacji.  
 

ELEMENTY PROGRAMU W JĘZYKU C++. 

 
Uogólniając, program w języku C++ składa się z następujących  
elementów:  
 

1. Dyrektyw preprocesora. Przykład:  

 
#define drukuj printf 
 
Działanie: W tekście programu PONIŻEJ niniejszej dyrektywy  
zastąp wszystkie łańcuchy znaków "drukuj" łańcuchami znaków  
"printf".  
 
#include <D:\KATALOG\nazwa.roz>  
 
Działanie: W to miejsce pliku wstaw zawartość pliku tekstowego  
NAZWA.ROZ z katalogu KATALOG na dysku D:. 
 

2. Komentarzy. Przykład:  

 
// Tu obliczamy sumę      lub 

/*To jest komentarz*/ 

 

3. Deklaracji. Przykład:  

KAŻDY PROGRAM musi zawierać deklarację funkcji main (ang. main - 
główna). Funkcja ta często jest bezparametrowa, co można  
zaakcentować wpisując w nawiasy słowo kluczowe void:  

main(void)  

lub pisząc puste nawiasy:  
 
main()  

44

background image

 
4. Instrukcji. 
 
i++;  
 
Działanie: Dokonaj inkrementacji zmiennej i, tzn. wykonaj  
operację i:=i+1  
 

Dla dociekliwych - kilka słów o funkcji main()  

________________________________________________________________ 
Funkcja main() występuje najczęściej w następujących  
(równoważnych) postaciach:  
 
main()   int main()   int main(void)  
 
- program w momencie uruchomienia nie pobiera żadnych argumentów 
 
z wiersza rozkazu --> () lub (void)  
- program zwraca po zakończeniu jedną licznę (int = integer -  
liczba całkowita) do systemu operacyjnego informując go w taki  
sposób, czy wykonał się do końca i bezbłędnie i czy można go  
usunąć z pamięci (bywają także programy rezydujące w pamięci -  
tzw. TSR, o czym system operacyjny powinien "wiedzieć").  
 
void main()    void main(void)  
 
- program nie pobiera i nie zwraca żadnych paramatrów.  
Główna funkcja main() może w środowisku okienkowym przeobrazić  
się w główną funkcję okienkową:  
 
WinMain(.....)  
 
a w środowisku obiektowym w  
 
OwlMain(....)  
 

OWL - biblioteka obiektów dla Windows - Object Windows Library.  

 
W nawiasach funkcji main(), WinMain() i OwlMain() mogą pojawić  
się parametry (argumenty) pobierane przez program w momencie  
uruchomienia z wiersza rozkazu lub od środowiska operacyjnego  
(szczegóły w dalszej części książki).  
Programy w C++ mogą składać się z wielu plików dyskowych. Typowy 
 
program zawiera. Nazywa się to zwykle projektami wielomodułowymi 
 
- a poszczególne pliki - modułami lub elementami składowymi  
projektu: 
  
* plik nagłówkowy - NAZWA.H  
* moduł główny    - NAZWA.CPP  (ten i tylko ten zawiera funkcję  
main())  
* moduły pomocnicze - NAZWA2.CPP, NAZWA3.CPP, itp  
* pliki z zasobami typu menu, okienka dialogowe, itp - NAZWA.RC, 
 
NAZWA.DLG  
* wreszcie plik instruktażowy - jak z tego wszystkiego zrobić  

45

background image

końcową aplikację. W zależności od wersji kompilatora pliki  
instruktażowe mogą mieć nazwy: NAZWA.PRJ (Project - BORLAND),  
NAZWA.IDE, a dla programu MAKE - MAKEFILE, NAZWA.MAK, NAZWA.NMK, 
 
itp.  
W środowisku Windows występuje jeszcze zwykle w składzie  
projektów aplikacji tzw. plik definicji sposobu wykorzystania  
zasobów - NAZWA.DEF. 
________________________________________________________________ 
 
[S!] 

void - czyli nijaki.

  

________________________________________________________________ 
Słowa kluczowe: 
void - pusty, wolny, nieokreślony, avoid - unikać. 
main - główny, główna.  
return - powrót, zwrot.  
Nazwa funkcji: 
exit() - wyjście. 
________________________________________________________________ 
 
Po nazwie funkcji main() NIE NALEŻY stawiać średnika (;).  
Przy pomocy tej funkcji program kontaktuje się z systemem  
operacyjnym. Parametry funkcji main, to te same parametry z  
którymi uruchamiamy nasz program w systemie DOS. Np. rozkaz  
 
FORMAT A:  
 
oznacza, że do programu przekazujemy parametr A:. 
 
Ponieważ w każdym programie oprócz nagłówka funkcji:  
 
main(void)  
 
podajemy również tzw. ciało funkcji, np.:  
 
{  
  printf("wydrukuj cokolwiek");  
  return 0; 
}  
 
jest to jednocześnie DEFINICJA FUNKCJI main(). 
Zwróć uwagę, że funkcja printf() nie jest w powyższym  
przykładzie w żaden sposób ani deklarowana ani definiowana.  
Wiersz:  
 
printf("pisz!");  
 
stanowi WYWOŁANIE funkcji printf() z parametrem 'pisz!' -  
łańcuchem znaków, który należy wydrukować.  
W C++ nawet jeśli nawiasy przeznaczone w funkcji na przekazanie  
jej argumentów są puste - muszą być obecne. Poprawne wywołanie  
funkcji w języku C++ może mieć następującą formę:  
 
nazwa_funkcji();  
 
nazwa_funkcji(par1, par2, par3, .....);  
 
zmienna = nazwa_funkcji(par1, par2, ...);  
 
Funkcja w momencie jej wywołania uzyskuje przekazane jej  

46

background image

parametry. Są to tzw. ARGUMENTY FUNKCJI. Aby to wszystko  
bardziej przypominało to, co znasz ze szkoły popatrzmy na  
analogię. W zapisie: 
 
y = sin(x)       lub     y = sin(90) 
 
x - oznacza argument funkcji, który może być zmienną (w szkole  
nazywałeś zmienne "niewiadomymi")  
y - oznacza wartość zwracaną "po zadziałaniu" funkcji  
sin() - oznacza nazwę funkcji. Zastosowanie funkcji będziemy w  
programach nazywać "wywołaniem funkcji". 
 

Język C++ operuje wyłącznie pojęciem FUNKCJI. W C ani w C++ nie  ma 
podziału na FUNKCJE i PROCEDURY.  

 
Każda funkcja może być w programie wywoływana wielokrotnie.  
Każde wywołanie funkcji może następować z innymi argumentami.  
Funkcja może w wyniku swojego działania zmieniać wartość jakiejś 
 
zmiennej występującej w programie. Mówimy wtedy, że funkcja  
ZWRACA wartość do programu. 

Funkcja main() jest funkcją szczególną, która "zwraca" wartość do systemu 
operacyjnego, w którym pracuje program. 

Zapis:  
 
main()           lub        int main() 
{                           { 
  return 5;                   exit(5); 
}                           } 
 
oznacza:  
1. Funkcja main jest bezparametrowa (nie przyjmuje żadnych  
argumentów z zewnątrz).  
2. Funkcja main zwraca jako wynik swojego działania liczbę  
całkowitą typu int (ang. INTeger - całkowita). Zwróć uwagę, że  
jest to domyślny sposób działania funkcji main(). Jeśli nie  
napiszemy przed funkcją main() słowa "int" - kompilator C++ doda 
 
je sobie automatycznie. Jeśli świadomie nie zamierzamy zwracać  
do systemu operacyjnego żadnych informacji - musimy wyraźnie  
napisać tam "void".  
3. Funkcja zwróci do systemu DOS wartość 5. Zwróć uwagę na  
istotną różnicę formalną, Słowo "return" jest słowem kluczowym  
języka C, natomiast słowo "exit" jest nazwą funkcji exit().  
Zastosowanie tej funkcji w programie wymaga dołączenia pliku  
nagłówkowego z jej prototypem.  
 
Ponieważ nasz kurs języka C++ rozpoczęliśmy od programu z  
funkcją printf() i zapewne będzie nam ona towarzyszyć jeszcze  
długo, pora poświęcić jej trochę uwagi.  
 

FUNKCJA printf().  

 
Jest to funkcja FORMATOWANEGO wyjścia na standardowe urządzenie  
wyjścia (ang. stdout - STandarD OUTput). Definicja - ściślej  

47

background image

tzw. PROTOTYP tej funkcji znajduje się w pliku nagłówkowym  
STDIO.H. Wniosek praktyczny: Każdy program korzystający z  
funkcji printf() powinien zawierać dyrektywę preprocesora:  
 
#include <stdio.h>  
 
zanim nastąpi wywołanie funkcji printf().  
 

A JEŚLI ZAPOMNIAŁEM O <STDIO.H> ???  

________________________________________________________________ 
Możesz nadać plikowi z tekstem żródłowym programu rozszerzenie  
.C zamiast .CPP. W kompilatorach Borlanda powoduje to przy  
domyślnych ustawieniach kompilatora wywołanie kompilatora C  
zamiast C++. C jest bardziej tolerancyjny i dokona kompilacji  
(wyświetli jedynie komunikat ostrzegawczy - Warning). Kompilator 
 
C++ jest mniej tolerancyjny. Jeśli zapomnisz dołączyć odpowiedni 
 
plik nagłówkowy może pojawić się komunikat:  
 
Error: Function printf() should have a prototype in function  
main  
(Funkcja printf() powinna mieć prototyp)  
 
Więcej o zawartości i znaczeniu plików nagłówkowych *.h dowiesz  
się z następnych lekcji. Na razie postaraj się pomiętać o  
dołączeniu wskazanego w przykładzie pliku.  
________________________________________________________________ 
 
[???] Skąd to wiadomo?  
________________________________________________________________ 
Jeśli masz wątpliwości, jaki plik nagłówkowy należałoby dołączyć 
 
- najprościej zajrzeć do systemu pomocy - Help. Na pasku  
głównego menu w IDE masz napis Help. Menu Help możesz rozwinąć  
myszką lub naciskając kombinację klawiszy [Alt]+[H]. Jeśli w  
menu wybierzesz rozkaz Index (Spis) przeniesiesz się do okienka  
z alfabetycznym spisem haseł. Są tam słowa kluczowe, nazwy  
funkcji i jeszcze wiele innych interesujących rzeczy. Powinieneś 
 
teraz wykonać następujące czynności:  
 
* posługując się klawiszami kursora (ze strzałkami) odszukać w  
spisie nazwę funkcji  
albo  
* rozpocząć pisanie nazwy funkcji na klawiaturze (system Help  
sam wyszuka w spisie wypisaną w ten sposób nazwę)  
* nacisnąć [Enter]  
Przeniesiesz się do okienka opisu danej funkcji. Na samym  
początku w okienku każdej funkcji podana jest nazwa pliku  
nagłówkowego, w którym znajduje się prototyp funkcji. Nawet  
jeśli nie jesteś biegłym anglistą, łatwo rozpoznasz pliki  
nagłówkowe - po charakterystycznych rozszerzeniach .H (rzadziej  
.HPP. Charakterystyczne rozszerzenie *.H pochodzi od "plik  
nagłówkowy" - ang. Header file).  
________________________________________________________________ 
 

48

background image

Funkcja printf() zwraca wartość całkowitą typu int:  

 
* liczbę bajtów przesłanych na standardowe urządzenie wyjścia;  
* w przypadku wystąpienia błędu - kod znaku EOF.  
 
[S!]  
EOF - End Of File - znak końca pliku.  
EOL - End Of Line - znak końca linii.  
Indicator - znak, wskaźnik (nie mylić z pointerem !) 
 
[???] SKĄD TO WIADOMO ?  
________________________________________________________________ 

Kody EOF, EOL

 są tzw. predefiniowanymi stałymi. Ich szyfrowanie  
(przypisywanie tym identyfikatorom określonej stałej wartości  
liczbowej) dokonuje się z zastosowaniem preprocesora C++.  
To, że nie musisz się zastanawiać ile to właściwie jest EOF  
(zero ? czy -1 ?) zawdzięczamy też dołączanym plikom typu *.H, w 
 
których np. przy użyciu dyrektywy #define zostały PREDEFINIOWANE 
 
(zdefiniowane wstępnie) niektóre stałe. Jeśli jesteś bardzo  
dociekliwy, zajrzyj do wnętrza pliku STDIO.H (view, edit, type). 
 
Znajdziesz tam między innymi taki wiersz:  
 
#define EOF  (-1)   //End of file indicator  
________________________________________________________________ 
 

Składnia prototypu (ang. syntax):  

 
int printf(const char *format [arg1, arg2,.....]);  
 
lub trochę prościej: 
 
printf(format, arg1, arg2,.....argn);  
 
Liczba argumentów może być zmienna.  
 
C++ oferuje wiele funkcji o podobnym działaniu - np.:  
 
cprintf(), fprintf(), sprintf(), vprintf(), vsprintf(), itp.  
 

Wzorce konwersji w  najprostszym przypadku mają postać %s, %d, %f,

Ponieważ FORMAT brzmi może trochę obco, nazwijmy go WZORCEM. Jak 
 
wiesz, wszystkie informacje przechowywane są w pamięci komputera 
 
jako ciągi zer i jedynek. Jest to forma trochę niewygodna dla  
człowieka, więc zanim informacja trafi na ekran musi zostać  
zamieniona na postać dla nas wygodniejszą - np. na cyfry  
dziesiętne, litery itp.. Taki proces nazywany jest KONWERSJĄ, a  
podany w funkcji printf() FORMAT - WZORZEC to upraszczając,  
rozkaz dokonania takiej właśnie konwersii. Możesz więc zarządać  
przedstawienia liczby na ekranie w postaci np. SZESNASTKOWEJ lub 

49

background image

DZIESIĘTNEJ - tak, jak Ci wygodniej. Wzorce konwersji w  
najprostszym przypadku mają postać %s, %d, %f, itp.:  
I tak: 
 
%s - wyprowadź łańcuch znaków (s - String - łańcuch)  
Przykład:  
 
printf("%s","jakis napis"); 
ale także  
printf("Jakis napis");  
 
ponieważ format "%s" jest formatem domyślnym dla funkcji  
printf(). 
 
Przykład: 
 
printf("%39s","jakis napis"); 
 
spowoduje uzupełnienie napisu spacjami do zadanej długości 39  
znaków (Sprawdź!). Funkcja printf() operuje tzw. POLEM  
WYJŚCIOWYM. Długość pola wyjściowego możemy określić przy pomocy 
 
liczb wpisanych pomiędzy znaki % oraz typ - np. s. 
Możemy także określić ilość cyfr przed i po przecinku. 
 
%c - wyprowadź pojedynczy znak (c - Character - znak)  
Przykład:  
 
printf("%c",'X');  
 
(spowoduje wydrukowanie litery X) 
 
%d - wyprowadź liczbę całkowitą typu int w postaci dziesiętnej  
€€€€€(d - Decimal - dziesiętny).  
Przykład:  
 
printf("%d", 1994);  
 
%f - wyprowadź liczbę rzeczywistą typu float w postaci  
dziesiętnej (f - Floating point - zmienny przecinek).  
 
Przykład:  
 
printf("%f", 3.1416);  
printf("%f3.2", 3.14159);    
 
%o - wyprowadź liczbę całkowitą typu int w postaci ósemkowej  
(o - Octal - ósemkowa).  
Przykład:  
 
printf("%o", 255); 
 
%x - wyprowadź liczbę całkowitą typu int w postaci szesnastkowej 
 
€€€€€(x - heXadecimal - szesnastkowa).  
%x lub %X - cyfry szesnastkowe a,b,c,d,e,f lub A,B,C,D,E,F. 
 
%ld - liczba całkowita "długa" - long int.  
 
%Lf - liczba rzeczywista poczwórnej precyzji typu long double  

50

background image

float. 
 
%e - liczba w formacie wykładniczym typu 1.23e-05 (0.0000123) 
 
%g - automatyczny wybór formatu %f albo %e.  
 
Po przytoczeniu przykładów uogólnijmy sposób zastosowania wzorca 
 
formatu: 
 
%[przełączniki][szerokość_pola][.precyzja][rozmiar]Typ  
 
Posługując się różnymi sposobami formatowania liczb możemy  
zażądać wydrukowania liczb w najwygodniejszej dla nas formie. W  
programie przykładowym dokonujemy zamiany liczb dziesiętnych na  
szesnastkowe.  
 

[P006.CPP] 

 
// Program przykladowy 10na16.CPP 
 
#include <stdio.h> 
#include <conio.h>  
 
int liczba;  
 
int main()  
{  
   clrscr();  
   printf("Podaj liczbe dziesietna calkowita ? \n");  
   scanf("%d", &liczba);  
   printf("\nSzesnastkowo to wynosi:   ");  
   printf("%x",liczba);  
   getch();  
   return 0;  

 
Ten program pozwala zamienić dziesiętne liczby całkowite na  
liczby szesnastkowe. Zakres dostępnych liczb wynika z  
zadeklarowanego typu int. Więcej na ten temat dowiesz się z  
następnych lekcji. Spróbujmy odwrotnie:  
 

[P007.CPP] 

 
// Program przykladowy 16na10.CPP 
//UWAGA: Sam dołącz pliki nagłówkowe 
 
int liczba;  
 
int main()  
{  
  clrscr();  
  printf("Podaj liczbe SZESNASTKOWA-np. AF - DUZE LITERY: \n");  
  scanf("%X", &liczba);  
  printf("%s","\nDziesietnie to wynosi:  ");  
  printf("%d",liczba);  
  getch();  

51

background image

  return 0;  

  
Myślę, że program 16NA10.CPP można pozostawić bez dodatkowego  
komentarza. Zwróć uwagę, że funkcja scanf() "formatuje" dane  
wejściowe bardzo podobnie do funkcji printf(). Pewnie dziwi Cię  
trochę "dualny" zapis:  
 
liczba i &liczba.  
 
Zagadka zostanie niebawem wyjaśniona. W trakcie następnych  
Lekcji zajmiemy się dokładniej zmiennymi, i ich rozmieszczeniem  
w pamięci a na razie wracamy do funkcji printf().  
 
Jako się rzekło wcześniej - funkcja printf() może mieć wiele  
argumentów. Pozwala nam to przy pomocy jednego wywołania funkcji 
 
wyprowadzać złożone napisy.  
 
Przykład:  
 
printf("Iloczyn 3 %c 5 %8s %d", '*', "wynosi ",15); 
 
Działanie:  
"Iloczyn_3_ - wyprowadź jako łańcuch znaków.  
%c - tu wyprowadź pojedynczy znak - '*'.  
_5_ - wyprowadź jako łańcuch znaków.  
%8s - wyprowadź łańcuch "wynosi_" uzupełniając go z przodu  
spacjami do długości 8 znaków.  
%d - wyprowadź 15 jako liczbę dziesiętną.  
 
UWAGA: Znakiem podkreślenia w tekście książki "_" oznaczyłem  
spację, spacja to też znak. 
 
Przykład:  
 
printf("Iloczyn 3 %c 5 %9s %f", 'x', "wynosi ", 3*5);  
 
Zwróć uwagę, że tym razem kazaliśmy komputerowi samodzielnie  
policzyć ile wynosi nasz iloczyn, tzn. zastosowaliśmy jako  
argument funkcji printf() nie stałą, a WYRAŻENIE. Działanie  
możesz prześledzić przy pomocy programu przykładowego:  
 

[P008.CPP]  

 
// Program WYRAZ.CPP - Dołącz pliki nagłówkowe 
 
int main()   
{  
  clrscr();  
    printf("Skomplikowany napis:\n");  
    printf("Iloczyn 3 %c 5 %8s %d", '*', "wyniosi ", 15);  
  getch();  
    printf("\nWyrazenie jako argument:\n");  
    printf("Iloczyn 3 %c 5 %9s %d", 'x', "wynosi ", 3*5);  
    printf("\n\n\n");  
    printf("Przyjrzyj sie i nacisnij klawisz...");  
  getch();  
  return 0;  

52

background image

}  
 
Wyjaśnijmy jeszcze jedno "dziwactwo" - znaki sterujące  
rozmieszczeniem napisów na ekranie. 

Oto tabelka z najczęściej używanymi znakami specjalnymi: 

 
________________________________________________________________ 
 
Znak      Nazwa             Działanie  
________________________________________________________________ 
 
\n        New Line          Przejście na początek nowego wiersza 
 
\b        BackSpace         Cofnięcie kursora o jeden znak   
\f        Form feed         O stronicę w dół   
\r        Carriage return   Powrót na początek bież. wiersza   
\t        Horizontal Tab    Tabulacja pozioma   
\v        Vertical Tab      Tabulacja pionowa   
\a        Sound a beep      Pisk głośniczka   
\\        Displ. backslash  Wyświetl znak \   
\'        Display '         Wyświetl znak ' (apostrof)   
\"        Display "         Wyświetl znak " (cudzysłów)   
________________________________________________________________ 
 
UWAGA: Trzy ostatnie "backlash-kody" pozwalają wyprowadzić na  
ekran znaki specjalne \ ' i ", co czasami się przydaje.  
Szczególnie \\ jest często przydatny.  
 
[Z]  
Spróbuj samodzielnie:  
 
1. Napisać i uruchomić program wykonujący konwersję liczb  
ósemkowych na dziesiętne i odwrotnie.  
2. Przy pomocy pojedynczego wywołania funkcji printf()  
wydrukować kilka złożonych napisów typu:  
* suma 2+4 to 6  
* działanie 5*7*27+6-873 daje wynik...( właśnie, ile?).  
3. Sprawdź działanie tabulacji pionowej \v. Ile to wierszy?  
 
[???] DYSKIETKA NIE JEST Z GUMY !!!  
________________________________________________________________ 
Jeśli podczas kompilacji programów w okienku będzie się  
uporczywie, bez widocznego powodu pojawiał napis "Errors" -  
błędy, a w okienku komunikatów "Message" pojawi się napis:  
 
Fatal A:\PROGRAM.C: Error writing output file  
(Fatalny błąd podczas kompilacji pliku A:\PROGRAM.C: Błąd przy  
zapisie pliku wyjściowego), 
 
to znak, że na dyskietce zabrakło miejsca. Pora zmienić katalog  
wyjściowy kompilatora C++. Aby to zrobić należy:  
1. Rozwinąć menu Option - [Alt]-[O].  
2. Wybrać rozkaz Directories... - [D].  
3. Przejść do okienka "Output Directory" - 2 razy [Tab].  
4. Wpisać do okienka katalog z dysku stałego, np.: C:\  
5. Nacisnąć [Enter].  
6. Powtórzyć kompilację programu, przy której nastąpiło  
przepełnienie dyskietki.  
7. Usunąć z dyskietki A: zbędne pliki *.EXE (TYLKO *.EXE !!!). 

53

background image

 
Oczywiście lepiej posługiwać się własnym katalogiem na dysku  
stałym, ale dysk też niestety nie jest z gumy. Złośliwi twierdzą 
 
nawet, że każdy dysk jest za mały a każdy procesor zbyt wolny  
(to ponoć tylko kwestia czasu...). 
________________________________________________________________ 
 

 Dla dociekliwych - Przykłady programów.  

________________________________________________________________ 
Jeśli zajrzysz już do systemu Help, przwiń cierpliwie tekst  
opisu funkcji do końca. W większości funkcji na końcu  
umieszczony jest krótki "firmowy" program przykładowy.  
Nie musisz go przepisywać!  
W menu Edit IDE masz do dyspozycji rozkaz  
Edit | Copy Example                (Skopiuj przykład)  
Przykład zostanie skopiowany do Schowka (Clipboard).  
Po wyjściu z systemu pomocy warto rozkazem  
File | New  
otworzyć nowe okno robocze a następnie rozkazem  
Edit | Paste                        (Wstaw)  
wstawić program przykładowy ze schowka. Możesz go teraz  
uruchamiać, modyfikować a nawet wstawić jako fragment do swojego 
 
programu.  
Podobnie jak większość edytorów tekstu zintegrowany edytor  
środowiska IDE pozwala manipulować fragmentami blokami tekstu i  
wykonywać typowe operacje edytorskie zarówno w obrębie  
pojedynczego okna, jak i pomiędzy różnymi okienkami. Służą do  
tego celu następujące operacje:  
 
* Select/Mark text block - zaznaczenie fragmentu tekstu.  
Możesz dokonać tego klawiszami- np.: [Shift]+[-->], bądź  
naciskając i przytrzymując lewy klawisz myszki i "przejeżdżając  
nad odpowiednim fragmentem tekstu". Wybrany fragment tekstu  
zostanie wyróżniony podświetleniem.  
* Edit | Cut - wytnij.  
Zaznaczony wcześniej fragment tekstu zostanie skopiowany do  
Schowka i jednocześnie usunięty z ekranu.  
* Edit | Copy - skopiuj.  
Zaznaczony wcześniej fragment tekstu zostanie skopiowany do  
Schowka i bez usuwania z ekranu.  
* Edit | Paste - wstaw.  
Zaznaczony wcześniej w Schowku fragment tekstu zostanie  
skopiowany na ekran począwszy od miejsca wskazanego w danej  
chwili kursorem. 

LEKCJA 8. Jakich słów kluczowych używa C++. 

 
W trakcie tej lekcji dowiesz się:  
* Jakie znaczenie mają słowa kluczowe języka C++. 
* Jakie jeszcze dziwne słowa mogą pojawiać się w programach w  
pisanych C++. 
* Trochę więcej o wczytywaniu i wyprowadzaniu danych.  
* Co to jest i do czego służy zmienna. 
_______________________________________________________________  

54

background image

 

Każdy język musi operować tzw. słownikiem - zestawem słów 

zrozumiałych w danym języku. Jak wiesz z doświadczenia, komputer 
jest pedantem i wymaga dodatkowo (my, ludzie, tego nie  
wymagamy), aby znaczenie słów było absolutnie jednoznaczne i  
precyzyjne. Aluzje, kalambury i zabawne niedomówienia są na  
razie w dialogu z komputerem niedopuszczalne. Pamięci  
asocjatywne (oparte na skojarzeniach), sieci neuronowe (neural  
networks), tworzone bardzo często właśnie przy pomocy C++  
- systemy expertowe,  
- systemy z tolerancją błędów - np. OCR - systemy optycznego  
rozpoznawania pisma,  
- "rozmyta" arytmetyka i logika (fuzzy math)  
- logika większościowa i mniejszościowa  
- algorytmy genetyczne (genetic algorithms) 
i inne pomysły matematyków oraz informatyków rozpoczęły już  
proces "humanizowania" komputerowego myślenia. Powstała nawet  
specjalna "mutacja" neural C i neural C++, ale to temat na  
oddzielną książkę. Na razie traktujemy nasz komputer jako  
automat cyfrowy pozbawiony całkowicie wyobraźni i poczucia  
humoru, a język C++, jako środek porozumiewania się z tym  
"ponurakiem".  
 
Podobnie do słów języka naturalnego (rzeczowników, czasowników)  
i słowa języka programowania można podzielić na kilka grup  
różniących się przeznaczeniem. Takie niby - słowa czasem nazywa  
się również tokenami lub JEDNOSTKAMI LEKSYKALNYMI (leksykon -  
inaczej słownik) a sposoby tworzenia wyrażeń (expressions)  
nazywane są syntaktyką języka (stąd bierze się typowy komunikat  
o błędach "Syntax Error" - błąd syntaktyczny, czyli niewłaściwa  
składnia). 

Słownik języka C++ składa się z:  

 
* Słów kluczowych  
* Identyfikatorów  
* Stałych liczbowych i znakowych  
* Stałych tekstowych (łańcuchów znaków - napisów)  
* Operatorów (umownych znaków operacji)  
* Znaków interpunkcyjnych  
* Odstępów  
 
UWAGA: Zarówno pojedyncza spacja czy ciąg spacji, tabulator  
poziomy, znak nowej linii, jak i komentarz dowolnej długości (!) 
są traktowane przez kompilator jak pojedyncza spacja. 
Od zarania dziejów informatyki twórcy uniwersalnych języków 
programowania starali się upodobnić słowa tych języków  do  
zrozumiałych dla człowieka słów języka naturalnego - niestety -  
angielskiego (swoją drogą, może to i lepiej, że C++ nie  
wymyślili Japończycy...). Najważniejszą częścią słownika są tzw. 
 
SŁOWA KLUCZOWE (keywords). 
 

SŁOWA KLUCZOWE w C++.  

 
Oto pełna lista słów kluczowych Turbo C++ v 1.0 z krótkim  
wyjaśnieniem ich znaczenia. Zaczynam od listy podstawowej wersji 

55

background image

kompilatora, ponieważ rozważania o niuansach dotyczących kilku  
specyficznych słów kluczowych (np. friend, template) pozostawiam 
sobie na póżniej. Krótkie wyjaśnienie - jak to krótkie  
wyjaśnienie - pewnie nie wyjaśni wszystkiego od razu, ale na  
pewno pomoże zrozumieć znaczenie większości słów kluczowych.  
 
[S] Keywords - słowa kluczowe.  

asm  
Pozwala wstawić kod w ASEMBLERZE bezpośrednio do programu  
napisanego w C lub C++. 
 
auto - zmienna lokalna. Przyjmowane domyślnie. 
 
break - przerwij.  
 
case - w przypadku.  
 
cdecl - spec. konwencja nazewnictwa/przekazania parametrów  
           zgodna ze standardem jęz. C. 
 
char - znak, typ zmiennej - pojedynczy bajt.  
 
class - klasa.  
 
const - stała, konstanta.  
 
continue - kontynuuj.  
 
default - przyjmij domyślnie.  
 
delete - skasuj obiekt.  
 
do - wykonaj.  
 
double - podwójna (długość/precyzja).  
 
else - w przeciwnym wypadku.  
 
enum - wylicz kolejno.  
 
_export - dotyczy tylko OS/2, ignorowany. 
 
extern - zewnętrzna.  
 
far - dalekie. Wskaźnik - podwójne słowo (w zakresie do 1 MB). 
 
float - zmiennoprzecinkowy, rzeczywisty.  
 
for - dla (wskazanie zmiennej roboczej w pętli).  
 
friend - zaprzyjaźniona funkcja z dostępem do prywatnych i  
€€€€€€€€€€chronionych członków danej klasy. 
 
goto - skocz do (skok bezwarunkowy).  
 
huge - daleki, podobnie do far. 
 
if - jeżeli (pod warunkiem, że...).  
 

56

background image

inline - funkcja z rozwiniętym wstawionym kodem 
 
int - typ zmiennej, liczba całkowita, dwa bajty 
  
interrupt - przerwanie.  
 
_loadds - podobne do huge, ustawia rejestr DS (Data Segment). 
 
long - długi.  
 
near - bliski, wskaźnik o dł. 1 słowa. Obszar max. 64 K. 
 
new - nowy, utwórz nowy obiekt.  
 
operator - operator, określa nowy sposób działania operatora.  
 
pascal - deklar. funkcji zgodnej ze standardem  przekazywania  
           parametrów przyjętym w Pascalu. 
 
private - prywatna, wewnętrzna, niedostępna z zewnątrz.  
 
protected - chroniona, część danych i funkcji, do których  
dostęp. jest ograniczony. 
 
public - publiczna, dostępna z zewnątrz.  
 
register - zmienną przechwaj nie w pamięci a w rejestrze CPU. 
 
return - powrót, zwrot wartości.  
 
_saveregs - save registers, zachowaj zawartość rejestrów a  
następnie odtwórz rejestry przed powrotem. 
 
_seg - segment.  
 
short - krótka (mała ilość cyfr).  
 
signed - ze znakiem (+/-).  
 
unsigned - bez znaku (+/-).  
 
sizeof - podaj wielkość.  
 
static - statyczna.  
 
struct - struktura.  
 
switch - przełącz.  
 
this - ten, wstazanie bieżącego, własnego obiektu (tylko C++). 
 
typedef - definicja typu.  
 
union - unia, zmienna wariantowa.  
 
virtual - wirtualna, pozorna.  
 
void - nieokreślona.  
 
volatile - ulotna.  

57

background image

 
while - dopóki. 

Panuje mnienanie, że język C++ posługuje się stosunkowo skromnym 
zestawem słów kluczowych. To prawda, ale nie cała prawda o  
języku C++. Zauważyłeś zapewne, że nie ma tu:  
 
define, include, printf  
 
i innych znanych Ci już słów. To po prostu jeszcze nie cały  
słownik języka. Zdając sobie sprawę z nieprecyzyjności tego  
porównania możesz przyjąć, że to coś na kształt listy  
czasowników. A są przecież jeszcze i inne słowa - o innej roli i 
przeznaczeniu. 
 

GDZIE SIĘ PODZIAŁY REJESTRY ???  

Nazwy rejestrów mikroprocesora Intel 80X86:  
 
_AX€€€€€€€_AL€€€€€€€_AH€€€€€€€_SI€€€€€€€_CS  
_BX€€€€€€€_BL€€€€€€€_BH€€€€€€€_SP€€€€€€€_DS  
_CX€€€€€€€_CL€€€€€€€_CH€€€€€€€_BP€€€€€€€_ES  
_DX€€€€€€€_DL€€€€€€€_DH€€€€€€€_DI€€€€€€€_SS  
_FLAGS  
 
Takie oznaczenia wynikają z architektury konkretnej rodziny  
mikroprocesorów, nie mogą stanowić uniwersalnego standardu  
języka C++. Efekt dostosowania C++ do IBM PC to np. odnoszące  
się do modeli pamięci słowa kluczowe near, far i huge.  
Wymóg zgodności ze standardem ANSI C spowodował, że w C++ nazwy 
rejestrów pozostają nazwami o zastrzeżonym znaczeniu, ale  
nazywają się PSEUDOZMIENNYMI REJESTROWYMI (ang.: Register  
Pseudovariables).  

Próba użycia słowa o zastrzeżonym znaczeniu w jakiejkolwiek 
innej roli (np. jako nazwa Twojej zmiennej) może spowodować  
wadliwe działanie programu lub uniemożliwić kompilację. Unikaj  
przypadkowego zastosowania słów o zastrzeżonym znaczeniu!  
 
[???] A SKĄD MAM WIEDZIEC ?  

Listę nazw, które mają już nadane ściśle określone znaczenie w  
C++ znajdziesz w Help. Dostęp do spisu uzyskasz przez:  
 
* Rozwinięcie menu Help  [Alt]-[H];  
* Wybranie z menu Help rozkazu Index (spis). 
 
Wrócić do edytora IDE C++ możesz przez [Esc]. 

SŁOWA TYPOWE DLA PROGRAMÓW OBIEKTOWYCH.  

 
W porównaniu z klasycznym językiem C (wobec którego C++ jest  
nadzbiorem - ang. superset), w nowoczesnych programach  
obiektowych i zdarzeniowych pisanych w C++ mogą pojawiać się i  
inne słowa. Przyjrzyjmy się na trochę inną technikę  
programowania - bardziej charakterystyczną dla C++.  

58

background image

 
Procesy wprowadzania i wyprowadzania danych do- i z- komputera  
nazywają się Input i Output - w skrócie I/O (lub bardziej  
swojsko We/Wy). Obsługa We/Wy komputera to sała obszerna wiedza, 
na początek będzie nam jednak potrzebne tylko kilka najbardziej  
istotnych informacji.  
  

PROBLEM ˙WEJŚCIA/WYJŚCIA W PROGRAMACH – trochę bardziej ogólnie. 

 
Operacje wejścia i wyjścia są zwykle kontrolowane przez 
pracujący właśnie program. Jeśli uruchomiłeś program, który nie  
korzysta z klawiatury i nie oczekuje na wprowadzenie przez  
użytkownika żadnych informacji - możesz naciskać dowolne  
klawisze - program i tak ma to w nosie. Podobnie, jeśli w  
programie nie przewidziano wykorzystania drukarki, choćbyś  
"wyłaził ze skóry", żadne informacje nie zostaną przesłane do  
drukarki, dla programu i dla użytkownika drukarka pozostanie  
niedostępna. Aby programy mogły zapanować nad Wejściem i  
Wyjściem informacji, wszystkie języki programowania muszą  
zawierać specjalne rozkazy przeznaczone do obsługi  
Wejścia/Wyjścia (ang. Input/Output commands, lub I/O  
instructions). Bez umiejętności obsługi We/Wy, czyli bez  
możliwości porozumiewania się ze światem zewnętrznym psu na budę 
zdałby się każdy język programowania. Każdy program musi w  
większym, bądź mniejszym stopniu pobierać informacje ze świata  
zewnętrznego do komputera i wysyłać informacje z komputera na  
zewnątrz.  
 
Podobnie, jak wszystkie uniwersalne języki programowania - język 
C++ zawiera pewną ilość rozkazów przeznaczonych do zarządzania  
obsługą wejścia i wyjścia. Dla przykładu, możemy w języku C++   
zastosować OBIEKT cout obsługujący strumień danych wyjściowych.  
Obiekt cout (skonstruowany przez producenta i zdefiniowany w  
pliku nagłówkowym IOSTREAM.H) pozwala programiście  
przesłać dane tekstowe i/lub numeryczne do strumienia wyjściwego 
i umieścić tekst na ekranie monitora.  
 
Wczytaj plik źródłowy z programem COUT1.CPP lub wpisz  
samodzielnie następujący program przykładowy. Program drukuje  
tekst na ekranie monitora.  
 

[P009.CPP] 

 
#include <iostream.h>       <-- zwróć uwagę na inny, nowy plik 
#include <conio.h>   
 
void main(void)   
{   
   clrscr();   
   cout << "Stosujemy obiekt cout:\n";   
   cout << "Tekst pierwszy\n";   
   cout << "Tekst drugi...\n";   
   getch(); 
}   
 
Jak widzisz, każdy rozkaz z użyciem obiektu cout tworzy  
pojedynczą linię tekstu (wiersz) na ekranie monitora. Kompilator 

59

background image

języka C++ wie, że chcesz wysłać tekst na ekran monitora dzięki  
słowu cout i znakowi << (znak << to tzw. operator przesyłania do 
strumienia). Wysłany na ekran zostaje tekst umieszczony po  
operatorze << i (obowiązkowo, podobnie jak w funkcji printf())  
ujęty w cudzysłów ("). Tekst ujęty w cudzysłów nazywa się  
łańcuchem znakowym (ang. string literal).   
  

String literal - łańcuch znaków. 

Łańcuch znaków to grupa znaków alfanumerycznych (tekstowych).   
Łańcuch znaków to taki ciąg znaków, który komputer może  
rozpatrywać wyłącznie jako całość i posługiwać się nim tylko  
tak, jak go wpisałeś. Aby komputer poprawnie rozpoznawał  
łańcuchy tekstowe - należy ujmować je w cudzysłów. Łańcuch  
znaków może być nazywany również literałem, bądź literałem  
łańcuchowym.  
 

Dla dociekliwych - jak C++ zapamiętuje tekst?  

Pojedyncze znaki można zapisywać w C++ tak:  
 
'A' - pojedynczy znak reprezentowany w pamięci komutera jako  
jeden bajt zawierający liczbę - numer litery A według kodu  
ASCII. W tym przypadku byłaby to liczba 65 (dwójkowo i  
szesnastkowo- odpowiednio: 0100 0001 i 41).  
"A" - jednoelementowy łańcuch znaków zajmujący w pamięci dwa  
bajty (kod litery A i znak końca łańcucha - \0). Reprezentacja w 
 
pamięci wyglądałaby tak:  
 
Bajt Nr X     0100 0001        - kod ASCII litery A 
Bajt Nr X+1   0000 0000        - kod ASCII 0 - znak końca 

 
Wiesz już, że clrscr(); stanowi wywołanie gotowej funkcji (tzw.  
funkcji bibliotecznej). Informacja dotycząca tej funkcji (tzw.  
prototyp funkcji) znajduje się w pliku CONIO.H, dlatego  
dołączyliśmy ten plik nagłówkowy na początku programu dyrektywą  
#include. A cóż to za dziwoląg ten "cout" ?  
Po cout nie ma pary nawiasów okrągłych (gdyby to była 
funkcja - powinno być  cout()) - nie jest to zatem wywołanie   
funkcji. ˙Strumień danych wyjściowych cout - JEST OBIEKTEM (ang. 
I/O stream object - obiekt: strumień Wejścia/Wyjścia). Ale nie   
przestrasz się. Popularne wyobrażenie, że programowanie  
obiektowe jest czymś bardzo skomplikowanym nie ma z prawdą  
więcej wspólnego, niż powszechny dość pogląd, że baba z pustym  
wiadrem jest gorsza od czarnego kota. W gruncie rzeczy jest  
to proste. Strumień to nic innego jak zwyczajny przepływ   
informacji od jednego urządzenia do innego. W tym przypadku   
strumień (przepływ) danych oznacza przesłanie informacji   
(tekstu) z pamięci komputera na ekran monitora. Trójkątne   
nawiasy (<< lub >>) wskazują kierunek przepływu informacji.   
Przesyłanie następuje w naszym przypadku z pamięci do strumienia 
 
Pojawiło się tu ważne słowo - OBIEKT. Obiekt podobnie jak 
program komputerowy jest to grupa danych i funkcji działających  
wspólnie i przeznaczonych razem do wykonania jakichś zadań. Dla  

60

background image

przykładu obiekt cout służy do obsługi przesyłania danych na  
ekran monitora. Słowo "obiekt" jest często używane w opisach  
nowoczesnych technik programowania - tzw. PROGRAMOWANIA  
OBIEKTOWEGO. Programowanie obiektowe, ta "wyższa szkoła jazdy"  
dla programistów z lat 80-tych jest już właściwie w naszych  
czasach normą. Zresztą widzisz sam - napisałeś program obiektowy 
i co - i nic strasznego się nie stało. Na początek musisz  
wiedzieć tylko tyle, że aby posługiwać się obiektami -  
strumieniami wejście i wyjścia - należy dołączyć w C++ plik  
nagłówkowy IOSTREAM.H. Dlatego dyrektywa #include <iostream.h>  
znajduje się na początku przykładowego programu.  
 

KILKA ARGUMENTÓW FUNKCJI w praktyce. 

 
Jak starałem się wykazać w przykładzie z sinusem, funkcja może  
otrzymac jako argument stałą - np. określoną liczbę, bądź  
zmienną (niewiadomą). Niektóre funkcje mogą otrzymywać w  
momencie ich wywołania (użycia w programie) więcej niż jeden  
argument. Rozważmy to dokładniej na przykładzie funkcji  
fprintf() zbliżonej w działaniu do printf(), lecz bardziej  
uniwersalnej. Funkcja fprintf() pozwala wyprowadzać dane nie  
tylko na monitor, ale także na drukarkę. Skoro urządzenia  
wyjścia mogą być różne, trzeba funkcji przekazać jako jeden z  
jej argumentów informację o tym - na które urządzenie życzymy  
sobie w danej chwili wyprowadzać dane. 
 
Słowo stdout jest pierwszą informację (tzw. parametrem, bądź  
argumentem funkcji) przekazanym do funkcji fprintf(). Słowo   
stdout jest skrótem od Standard Output - standardowe wyjście.   
Oznacza to w skrócie typowe urządzenie wyjściowe podłączone do   
komputera ˙i umożliwiające wyprowadzenie informacji z komputera. 
 
W komputerach osobistych zgodnych ze standardem IBM PC tym   
typowym urządzeniem wyjściowym jest prawie zawsze ekran   
monitora.   
  
Tekst, który ma zostać wydrukowany na ekranie monitora jest   
drugą informacją przekazywaną do funkcji fprintf() - inaczej -   
stanowi drugi parametr funkcji. Tekst - łańcuch znaków - musi  
zostać ujęty w znaki cudzysłowu.   
  
A jeśli zechcesz wyprowadzić tekst na drukarkę?  
W C++ zapisuje się to bardzo łatwo. Wystarczy słowo stdout  
(oznaczające monitor) zamienić na słowo stdprn. Słowo stdprn to  
skrót od Standard Printer Device - standardowa drukarka. Oto  
przykład praktycznego użycia funkcji fprintf(). Program przesyła 
tekst na drukarkę. Przed uruchomieniem programu pamiętaj o  
włączeniu drukarki. 
   

[P010.CPP]  

 
#include <stdio.h>   
#include <conio.h>  
 
int main(void)   
{   
clrscr(); 

61

background image

   fprintf(stdout, "Drukuje...\n");   
   fprintf(stdprn, "Pierwsza proba drukowania\n");   
   fprintf(stdprn, "Autor: ....................");   
   fprintf(stdout, "Koniec drukowania."); 
   fprintf(stdout, "Skonczylem, nacisnij cosik...");  
   getch(); 
return 0;  
}   
   
Gdyby w programie nie było wiersza: 
 
fprintf(stdout, "Drukuje...\n");   
 
- użytkownik przez pewien czas nie mógłby się zorientować,  
czym ˙właściwie zajmuje się komputer. Wszystko stałoby się jasne 
 
dopiero wtedy, gdy drukarka rozpoczęłaby drukowanie tekstów.   
Jest uznawane za dobre maniery praktyczne stosowanie dwóch  
prostych zasad:  
 
BZU - Bez Zbędnych Udziwnień  
DONU - Dbaj O Nerwy Użytkownika 
 
Jeśli efekty działania programu nie są natychmiast zauważalne,  
należy poinformować użytkownika CO PROGRAM ROBI. Jeśli  
użytkownik odnosi wrażenie, że komputer nic nie robi - ma zaraz  
wątpliwości. Często próbuje wtedy wykonać reset komputera i  
wypowiada mnóstwo słów, których nie wypada mi tu zacytować.  
 
Nietrudno zgadnąć, że C++ powinien posiadać także środki obsługi 
 
wejścia. W C++ jest specjalny obiekt (ang. input stream object)  
o nazwie cin służący do pobierania od użytkownika tekstów i  
liczb. Zanim zajmiemy się dokładniej obiektem cin i obsługą  
strumienia danych wejściowych - powinieneś zapoznać się ze  
ZMIENNYMI (ang. variables).  
 

ZMIENNE.  

 
Gdy wprowadzisz jakieś informacje do komputera - komputer  
umieszcza je i przechowuje w swojej pamięci (ang. memory -  
pamięć). Pamięć komputera może być jego pamięcią stałą. Taka  
pamięć "tylko do odczytu" nazywa się ROM (read only memory - to  
właśnie "tylko do odczytu"). Pamięć o swobodnym dostępie, do  
której i komputer i Ty możecie zapisywać wszystko, co Wam się  
spodoba - nazywa się RAM (od Random Access Memory - pamięć o  
swobodnym dostępie). Pamięci ROM i RAM podzielone są na małe  
"komóreczki" nazywane Bajtami, Każdy bajt w pamięci ma swój  
numer. Ten numer nazywany jest adresem w pamięci. Ponieważ nie  
wszystko da się pomieścić w jednym bajcie (to tylko 8 bitów -  
miejsca wystarczy na zapamiętanie tylko jednej litery), bajty  
(zwykle kolejne) mogą być łączone w większe komórki - tzw. pola  
pamięci (ang. memory fields). Najczęściej łączy się bajty:  
 
2 Bajty = 16 bitów = Słowo   (WORD)  
4 Bajty = 32 bity  = Podwójne słowo (DOUBLE WORD - DWORD)  
 
W uproszczeniu możesz wyobrazić sobie pamięć komputera jako  
miliony pojedynczych komórek, a w każdej z komórek jakaś jedna  

62

background image

wartość (ang. value) zakodowana w postaci ZER i JEDYNEK. Każda  
taka "szara" komórka ma numer-adres. Numeracja komórek  
rozpoczyna się nie od 1 lecz od zera (pierwsza ma numer 0).  
Ilość tych komórek w Twoim komputerze zależy od tego ile pamięci 
 
zainstalujesz (np. 4MB RAM to 4x1024x124x8 bitów - chcesz -  
policz sam ile to bitów). Przeliczając zwróć uwagę, że kilobajt  
(KB to nie 1000 - lecz 1024 bajty a megabajt - 1024 kB). 
 
Zastanówmy się, skąd program może wiedzieć gdzie, w której  
komórce zostały umieszczone dane i jak się do nich dobrać, gdy  
staną się potrzebne. Właśnie do takich celów potrzebne są  
programowi ZMIENNE (ang. variables).   
  
Dawno, dawno temu rozwiązywałeś zapewne zadania typu:  
 
3 + [ ] = 5  
 
Otóż to [ ] było pierwszym sposobem przedstawienia Ci zmiennej.  
Jak widać - zmienna to miejsce na wpisanie jakiejś (czasem  
nieznanej w danej chwili wartości). Gdy przeszedłeś do następnej 
 
klasy, zadania skomplikowały się:  
 
3 + [ ] = 5  
147.968 + [      ] = 123876.99875  
 
Na różne zmienne może być potrzeba różna ilość miejsca i na  
kartce i w pamięci komputera. Gdy "zestarzałeś się" jeszcze  
trochę - te same zadania zaczęto Ci zapisywać tak:  
 
3 + x = 5  
147.968 + y = 123876.99875  
 
Jak widać, zmienne mogą posiadać także swoje nazwy -  
identyfikatory (z których już niestety nie wynika jasno, ile  
miejsca potrzeba do zapisania bieżącej wartości zmiennej).  
 
 

Jak C++ wskazuje adres w pamięci?  

Podobnie, jak w bajeczce o zabawie w chowanego kotka i myszki  
(myszka mówiła: "Gdybyś mnie długo nie mógł znaleść - będę  
czekać na czwartej półce od góry..."), niektórzy producenci gier 
 
komputerowych życzą sobie czasem przy uruchamianiu gry podania  
hasła umieszczonego:  
"W instrukcji na str. 124 w czwartym wierszu do góry"  
No cóż. Zamiast nazywać zmienne - niewiadome x, y, czy z, bądź  
rezerwować dla nich puste miejsce [     ], możemy jeszcze  
wskazać miejsce, w którym należy ich szukać. Takie wskazanie to  
trzeci sposób odwoływania się do danych. W C++ może się to  
nazywać referencją do zmiennej lub wskazaniem adresu zmiennej w  
pamięci przy pomocy wskaźnika. Wskaźnik w C++ nazywa się  
"pointerem". Pointerem można wskazać także funkcje - podając ich 
adres startowy (początek kodu funkcji w pamięci RAM). 

 
Zmienne możesz sobie wyobrazić jako przegródki w pamięci  

63

background image

komputera zaopatrzone w nazwę - etykietkę. Ponieważ nazwy dla  
tych przegródek nadaje programista w programie - czyli Ty sam,  
możesz wybrać sobie prawie każdą, dowolną nazwę. Zwykle nazwy  
nadaje się w taki sposób, by program stał się bardziej czytelny  
i łatwiejszy do zrozumienia. Dla przykładu, by nie przepadły z  
pamięci komputera wyniki gier komputerowych często stosuje się  
zmienną o nazwie WYNIK (ang. Score). Za każdym razem, gdy  
zmienia się wynik gracza (ang. player's score) w pamięci  
komputera (w to samo miejsce) zostaje zapisana nowa liczba. W  
taki sposób pewien niewielki (a zawsze ten sam) fragment pamięci 
komputera przechowuje dane potrzebne do pracy programu.  
  

PRZYPISYWANIE ZMIENNYM KONKRETNEJ WARTOŚCI. 

  
Aby komputer mogł pobrać informacje od użytkownika, możesz  
zastosować w programie np. obiekt - strumień wejściowy - cin  
(ang. input stream object). Obiekt cin i zmienne chodzą zwykle  
parami. Przy obiekcie cin musisz zawsze podać operator  
pobierania ze strumienia wejściowego >> i nazwę zmiennej. Zapis  
  
cin >> nazwa_zmiennej;   
  
oznacza ˙w C++ : pobierz dane ze strumienia wejściowego i umieść 
w zmiennej o nazwie "nazwa_zmiennej".Te informacje, które  
zostaną ˙wczytane, C++ przechowuje w przgródce oznaczonej nazwą, 
którą nadajesz zmiennej. Oto program przykładowy ilustrujący  
zapamiętywanie danych wprowadzonych przez użytkownika z  
klawiatury, wczytanych do programu przy pomocy obiektu cin i  
zapamiętanych w zadeklarowanej wcześniej zmiennej x: 
 

[P011.CPP] 

 
#include <iostream.h>   
#include <conio.h>   
  
void main(void)   
{   
int x;   
cout << "Podaj liczbe calkowita 0 - 1000 do zapamietania: ";  
cin >> x; 
cout << "Pamietam! "; 
cout << "Wielokrotnosci liczby: \n": 
cout << "x, 2x, 3x: " << x << " " << 2*x << " " << 3*x;   
cout << "\n ...Nacisnij dowolny klawisz...";  
getch();  
}   
  
  
Zapis cin >> x oznacza: "pobierz dane ze strumienia danych  
wejściowych i umieść je w pamięci przeznaczonej dla zmiennej x". 
 
x - to nazwa (identyfikator) zmiennej. Ta nazwa jest stosowana  
przez komputer do identyfikacji przegródki w pamięci, w której  
będzie przechowywana liczba wpisana przez użytkownika jako  
odpowiedź na zadane pytanie. Kompilator C++ zarezerwuje dla  
zmiennej x jakąś komórkę pamięci i umieści tam wpisaną przez  
Ciebie liczbę. W trakcie pracy kompilator C++ tworzy dla  

64

background image

własnego użytku tzw. tablicę symboli, którą posługuje się do  
rozmieszczania danych w pamięci. Jeśli chcesz, możesz sprawdzić  
przy pomocy Debuggera (Debug | Inspect) w których bajtach RAM  
C++ umieścił Twoją zmienną.  
  

Ile miejsca trzeba zarezerwować?  

To, ile miejsca trzeba zarezerwować dla danej zmiennej  
kompilator "wie" dzięki Twojej deklaracji, jakiego typu dane  
będą przechowywane w miejscu przeznaczonym dla zmiennej. Dla  
przykładu:  
- jeśli napiszesz    int x; 
Kompilatoer zarezerwuje 2 bajty 
- jeśli napiszesz     float y; 
Kompilatoer zarezerwuje 4 bajty 
itp...(szczegóły - patrz niżej). 
 
Zwykle nie musisz się przejmować tym, w którym miejscu  
kompilator rozmieścił Twoje dane. Wszystkie czynności C++ wykona 
automatycznie. Aby jednak wszystko przebiegało poprawnie - zanim 
zastosujesz jakąkolwiek zmienną w swoim programie - musisz  
ZADEKLAROWAĆ ZMIENNĄ. Deklaracja zmiennej to informacja dla  
kompilatora, ile i jakich zmiennych będziemy stosować w  
programie. Deklaracja zawiera nie tylko nazwę zmiennej, ale  
również typ wartości, jakie ta zmienna może przybierać.  
Przykładem deklaracji jest wiersz:  
  
int x;  
  
Słowo kluczowe int określa typ danych. Tu oznacza to, że zmienna 
x może przechowywać jako wartości liczby całkowite (ang. INTeger 
- całkowity) o wielkości zawartej w przedziale - 32768...+32767. 
Po określeniu typu danych następuje w deklaracji nazwa zmiennej  
i średnik.  
 

Dekaracja Zmiennej - Variable Declaration

Deklaracja zmiennej w C++ to określenie typu wartości zmiennej i 
podanie nazwy zmiennej.   

Zwróć uwagę w przykładowym programie, że kierując kolejno dane  
do strumienia wyjściwego cout możemy je poustawiać w tzw.  
łańcuch (ang. chain). Przesyłanie danych do obiektu cout  
operatorem << jest bardzo elastyczne. Wysyłamy na ekran zarówno  
tekst jak i liczbę - bieżącą wartość zmiennej x oraz wyniki  
obliczenia wartości wyrażeń ( 2*x i 3*x). Posługując się  
łączonym w "łańcuch" operatorem << można wyprowadzać na ekran  
wiersz zbudowany z różnych elementów. Operator przesyłania  
danych do strumienia wyjściowego << (ang. insertor - dosł. -  
operator wstawiania) powoduje przesłanie do obiektu cout kolejno 
wszystkich (różnego typu) elementów. Zwróć uwagę na użycie znaku 
\n na początku nowego wiersza, na końcu wiersza tekstu (można go 
zastosować nawet w środku wiersza tekstu - sprawdź).  
Zwróć uwagę w jaki sposób C++ rozpoznaje różnicę pomiędzy: 
 
- łańcuchem znaków - napisem (napis powinien być podany tak):   

65

background image

  cout << "x, 2x, 3x";   
- wartością zmiennej:   
  cout << x;   
 
Widać tu wyraźnie, dlaczego znak cudzysłowu jest dla kompilatora 
istotny. Jeśli pominiemy cudzysłów, C++ będzie próbował  
zinterpretować literę (tekst) jako nazwę zmiennej a nie jako  
napis.   
  

RODZAJE ZMIENNYCH: ZMIENNE NUMERYCZNE I ZMIENNE TEKSTOWE.   

  
Zmienne mogą w C++ być bardzo elastyczne. Dokładnie rzecz  
biorąc, zmienne mogą być:  
 
RÓŻNYCH TYPÓW - mogą być liczbami, mogą także być tekstami.  
 
Uruchom program jeszcze raz i zamiast liczby naciśnij w  
odpowiedzi na pytanie klawisz z literą. Program wydrukuje jakieś 
bzdury. Dzieje się tak dlatago, że program oczekuje podania  
liczby i zakłada, że wprowadzone przez użytkownika dane są  
liczbą.   
  

A jeśli użytkownik nie czyta uważnie??? 

C++ zakłada, że użytkownik wie co robi gdy podaje wartość  
zmiennej. Jeśli wprowadzone zostaną dane niewłaściwego typu -  
C++ nie przerywa działania programu i nie ostrzega przed  
niebezpieczeństwem błędu. Sam dokonuje tzw. konwersji typów -  
tzn. przekształca dane na wartość typu zgodnego z zadeklarowanym 
w programie typem zmiennej. To programista musi dopilnować, by  
pobrane od użytkownika dane okazały się wartością odpowiedniego, 
oczekiwanego przez program typu, lub przewidzieć w programie  
sposób obsługi sytuacji błędnych.  

  
Można utworzyć zmienną przeznaczoną do przechowywania w pamięci  
tekstu - napisu. Aby to zrobić musimy zadeklarować coś  
jakościowo nowego tzw. 

TABLICĘ ZNAKOWĄ (ang. character array).  

Jest to nazwa, przy pomocy której komputer lokalizuje w pamięci  
zbiór znaków. Aby zadeklarować zmienną (tablicę) znakową w C++  
musimy zacząć od słowa kluczowego char (ang. CHARacter - znak).  
Następnie podajemy nazwę zmiennej a po nazwie w nawiasach  
kwadratowych ilość znaków, z których może składać się zmienny  
tekst, który zamierzamy przechowywać w pamięci pod tą nazwą.  
 
W programie poniżej zmienna x nie jest już miejscem w pamięci  
służącym do przechowywania pojedynczej liczby. Tym razem nazwa  
(identyfikator zmiennej) x oznacza tablicę znakową, w której  
można przechowywać tekst o długości do 20 znaków. W C++ ostatnim 
znakiem w łańcuchu znakowym (tekście) bądź w tablicy znakowej  
zwykle jest tzw. NULL CHARACTER - niewidoczny znak o kodzie  
ASCII 0 (zero). W C++ znak ten podaje się przy pomocy szyfru  
'\0'. Przy pomocy tego znaku C++ odnajduje koniec tekstu,  
łańcucha znaków, bądź koniec tablicy znakowej. Tak więc w  
tablicy x[20] w rzeczywistości można przechować najwyżej 19  

66

background image

dowolnych znaków plus na końcu obowiązkowy NULL (wartownik).  
  

[P012.CPP]  

 
#include <conio.h>   
#include <iostream.h>   
  
void main(void)   
{   
char x[20];        //<---- deklaracja tablicy znakowej.   
  
clrscr();  
cout << "Podaj mi swoje imie: : ";   
cin >> x;  
cout << "\nNazywasz sie " << x << ", ladne imie!\n";  
cout << "...Nacisnij dowolny klawisz...";  
getch();  
}   
  
[Z] 

1. Spróbuj w przykładowych programach z poprzednich lekcji  
zastąpić funkcje obiektami - strumieniami We/Wy:  
 
printf()  -  cout <<  
scanf()   -  cin >>  
 
2. Spróbuj napisać program zawierający i funkcje i obiekty. Czy  
program pracuje bezkonfliktowo? Pamiętaj o dołączeniu  
odpowiednich plików nagłówkowych.

LEKCJA 9: O SPOSOBACH ODWOŁYWANIA SIĘ DO DANYCH.  

________________________________________________________________ 
W trakcie tej lekcji poznasz:  
* sposoby wyprowadzania napisów w różnych kolorach  
* sposoby zapamiętywania tekstów  
* sposoby odwoływania się do danyc i zmiennych przy pomocy ich  
nazw - identyfikatorów.  
________________________________________________________________ 
 
Możemy teraz poświęcić chwilę na zagadnienie kolorów, które  
pojawiają się na monitorze. Po uruchomieniu program przykładowy  
poniżej wygeneruje krótki dźwięk i zapyta o imię. Po wpisaniu  
imienia program zapiszczy jeszcze raz i zapyta o nazwisko. Po  
wpisaniu nazwiska program zmieni kolor na ekranie monitora i  
wypisze komunikat kolorowymi literami. Różne kolory zobaczysz  
oczywiście tylko wtedy, gdy masz kolorowy monitor. Dla  
popularnego zestawu VGA mono będą to różne odcienie szarości.  
Tekst powinien zmieniać kolor i "migać" (ang. - blinking text). 
 

[P012.CPP] 

 

67

background image

#include <conio.h>  
#include <iostream.h>  
 
main()  
{  
char imie[20];  
char nazwisko[20];  
 
clrscr();  
cout << "\aPodaj imie:  ";  
cin >> imie;  
cout << "\aPodaj nazwisko:  ";  
cin >> nazwisko;  
cout << '\n' << imie << ' ' << nazwisko << '\n'; 
textcolor(4+128);   
cprintf("\nPan(i), %s %s? Bardzo mi milo!", imie, nazwisko);  
getch(); 
cout << '\a'; 
return 0; 
}  
 
Wyjaśnijmy kilka szczegółów technicznych:  
 
cout << "\aPodaj nazwisko? ";  
/* \a to kod pisku głośniczka (beep) */  
 
cin >> nazwisko;  
textcolor(4+128);    <---- funkcja zmienia kolor tekstu  
 
cprintf("\nPan(i), %s %s? Bardzo mi milo!", imie, nazwisko);  
                     ___ tu funkcja wstawi "string" nazwisko 
   |                |________ a tu wstawi "string" imie  
   |_________ funkcja wyprowadza tekst na ekran w kolorach  
                (cprintf = Color PRINTing Function) 
 
 
Operator >> pobiera ze strumienia danych wejściowych cin wpisane 
 
przez Ciebie imię i zapisuje ten tekst do tablicy znakowej  
imie[20]. Po wypisaniu na ekranie następnego pytania następuje  
pobranie drugiego łańcucha znaków (ang. string) wpisanego przez  
Ciebie jako odpowiedź na pytanie o nazwisko i umieszczenie tego  
łańcucha w tablicy znakowej nazwisko[]. Wywołana następnie  
funkcja textcolor() powoduje zmianę roboczego koloru  
wyprowadzanego tekstu. Tekst nie tylko zmieni kolor, lecz także  
będzie "migać" (blink). Funkcja cprintf() wyprowadza na ekran  
końcowy napis. Funkcja cprintf() to Color PRINTing Function -  
funkcja drukowania w kolorze. 
 

Funkcja textcolor() pozwala na zmianę koloru tekstu  

wyprowadzanego na monitor. Można przy pomocy tej funkcji także  
"zmusić" tekst do migotania. Aby funkcja zadziałała - musimy  
przekazać jej ARGUMENT. Argument funkcji to numer koloru. Zwróć  
jednak uwagę, że zamiast prostego, zrozumiałego zapisu:  
 
textcolor(4);         /* 4 oznacza kolor czerwony */ 
 
mamy w programie podany argument w postaci wyrażenia (sumy dwu  
liczb):  

68

background image

 
textcolor(4+128);       // to samo, co:  textcolor(132);  
 
Wbrew pierwszemu mylnemu wrażeniu te dwie liczby stanowią jeden  
argument funkcji. C++ najpierw dokona dodawania 4+128 a dopiero  
uzyskany wynik 132 przekaże funkcji textcolor jako jej argument  
(parametr). Liczba 4 to kod koloru czerwonego, a zwiększenie  
kodu koloru o 128 powoduje, że tekst będzie migał.  
 
Numery (kody) kolorów, które możesz przekazać jako argumenty  
funkcji textcolor() podano w tabeli poniżej. Jeśli tekst ma  
migać - należy dodać 128 do numeru odpowiedniego koloru.  
 

Kod koloru przekazywany do funkcji textcolor().  

________________________________________________________________ 
 
Kod           Kolor (ang)         Kolor (pol)    Stała 
n                                                (przykład) 
________________________________________________________________ 
 
0             Black               Czarny         BLACK 
1             Blue                Niebieski      BLUE 
2             Green               Zielony        GREEN 
3             Cyan                Morski         CYAN 
4             Red                 Czerwony        
5             Magenta             Fioletowy 
6             Brown               Brązowy  
7             White               Biały  
8             Gray                Szary  
9             Light blue          Jasno niebieski  
10            Light green         Jasno zielony  
11            Light cyan          Morski - jasny 
12            Light red           Jasno czerwony  
13            Light magenta       Jasno fio;etowy (fiol-różowy) 
14            Yellow              Żółty  
15            Bright white        Biały rozjaśniony  
128 + n       Blinking            Migający       BLINK  
________________________________________________________________ 
 
 
[!!!]UWAGA: 
________________________________________________________________ 
* W pliku CONIO.H są predefiniowane stałe (skrajna prawa kolumna 
 
- przykłady), które możesz stosować jako argumenty funkcji.  
Kolor tła możesz ustawić np. przy pomocy funkcji  
textbackground() - np. textbacground(RED); 
 
* Manipulując kolorem tekstu musisz pamiętać, że jeśli kolor  
napisu:  
- foreground color, text color 
i kolor tła:  
- background color  
okażą się identyczne - tekst zrobi się NIEWIDOCZNY. Jeśli każesz 
 
komputerowi pisać czerwonymi literami na czerwonym tle -  
komputer wykona rozkaz. Jednakże większość ludzi ma kłopoty z  
odczytywaniem czarnego tekstu na czarnym tle. Jest to jednak  
metoda stosowana czasem w praktyce programowania do kasowania  

69

background image

tekstów i elementów graficznych na ekranie.  
_______________________________________________________________

Powołując się na nasze wcześniejsze porównanie (NIE TRAKTUJ GO  
ZBYT DOSŁOWNIE!),zajmiemy się teraz czymś, co trochę przypomina  
rzeczowniki w normalnym języku.  
 

O IDENTYFIKATORACH - DOKŁADNIEJ.  

 
Identyfikatorami (nazwami) mogą być słowa, a dokładniej ciągi  
liter, cyfr i znaków podkreślenia rozpoczynające się od litery  
lub znaku podkreślenia (_). Za wyjątkiem słów kluczowych, (które 
 
to słowa kluczowe - MUSZĄ ZAWSZE BYĆ PISANE MAŁYMI LITERAMI)  
można stosować i małe i duże litery. Litery duże i małe są  
rozróżniane. Przykład:  
 

[P013.CPP]  

 
#include <stdio.h>  
#include <conio.h>  
 
float PI = 3.14159;          <-- stała PI 
float r;                     <-- zmienna r 
 
int main(void)  
{  
  clrscr(); 
    printf("Podaj promien ?\n");  
    scanf("%f", &r);  
    printf("\nPole wynosi P = %f", PI*r*r );  
    getch();  
  return 0;  
}  
 
* Użyte w programie słowa kluczowe:  
int, float, void, return.  
 
* Identyfikatory  
- nazwy funkcji (zastrzeżone):  
main, printf, scanf, getch, clrscr.  
- nazwy zmiennych (dowolne):  
PI, r.  
 
* Dyrektywy preprocesora:  
# include  
 
Zwróć uwagę, że w wierszu:  
float PI = 3.14159;  
nie tylko DEKLARUJEMY, zmienną PI jako zmiennoprzecinkową, ale  
także od razu nadajemy liczbie PI jej wartość. Jest to tzw.  
ZAINICJOWANIE zmiennej. 
 
[Z] 
________________________________________________________________ 
1. Uruchom program przykładowy. Spróbuj zamienić identyfikator  
zmiennej PI na pisane małymi literami pi. Powinien wystąpić  

70

background image

błąd.  
________________________________________________________________ 
 
Dla porównania ten sam program w wersji obiektowo-strumieniowej: 

[P013-1.CPP] 

 
#include <stdio.h>  
#include <conio.h>  
 
const float PI = 3.14159;          <-- stała PI 
float r;                           <-- zmienna r 
 
int main(void)  
{  
  clrscr(); 
    cout << "Podaj promien ?\n";  
    cin >> r;  
    cout << "\nPole wynosi P = " << PI*r*r;  
    getch();  
  return 0;  
}  
 

LITERAŁY.  

 
Literałem nazywamy reprezentujący daną NAPIS, na podstawie  
którego można jednoznacznie zidentyfikować daną, jej typ,  
wartość i inne atrybuty. W języku C++ literałami mogą być:  
* łańcuchy znaków - np. "Napis";  
* pojedyncze znaki - np. 'X', '?';  
* liczby - np. 255, 3.14  
 
[!!!] Uwaga:  BARDZO WAŻNE !!! 
________________________________________________________________ 
* Rolę przecinka dziesiętnego spełnia kropka. Zapis Pi=3,14 jest 
 
nieprawidłowy.  
* Próba zastosowania przecinka w tej roli SPOWODUJE BŁĘDY !  
________________________________________________________________ 
 

Liczby całkowite mogą być: 

* Dziesiętne (przyjmowane domyślnie - default); 
* Ósemkowe - zapisywane z zerem na początku: 
017 = 1*8 + 7 = 15 (dziesiętnie); 
* Szesnastkowe - zapisywane z 0x na początku: 
0x17 = 1*16 + 7 = 23 (dziesiętnie); 
0x100 = 16^2 + 0 + 0 = 256 . 
 

Liczby rzeczywiste mogą zawierać część ułamkową lub być zapisane 

 
w postaci wykładniczej (ang. scientific format) z literą "e"  
poprzedzającą wykładnik potęgi. 
 
Przykład: 

71

background image

 
Zapis liczby€€€€€€€€Wartość dziesiętna  
 
.0123€€€€€€€€€€€€€€€0.0123 
123e4€€€€€€€€€€€€€€€123 * 10^4 = 1 230 000 
1.23e3€€€€€€€€€€€€€€€1.23 * 10^3 = 1230  
123e-4€€€€€€€€€€€€€€0.0123  
 
Literały składające się z pojedynczych znaków mają jedną z  
trzech postaci:  
 
* 'z' - gdzie z oznacza znak "we własnej osobie";  
* '\n' - symboliczne oznaczenie znaku specjalnego - np.  
sterującego - tu: znak nowej linii;  
* '\13' - nr znaku w kodzie ASCII.  
 
UWAGA:  
'\24' - kod Ósemkowy !         (dziesiętnie 20) 
'\x24' - kod SZESNASTKOWY !    (dziesiętnie 36) 
  
 
[S]€€SLASH, BACKSLASH.  
€€€€€Kreska "/" nazywa się SLASH (czyt. "slasz") - łamane,  
ukośnik zwykły. Kreska "\" nazywa się BACKSLASH (czyt.  
"bekslasz") - ukośnik odwrotny.  
 
Uzupełnimy teraz listę symboli znaków z poprzedniej lekcji.  
 
Znak €€€€ÓSEMKOWO€€ASCII (10)€€€€€ZNACZENIE 
\a€€€€€€€'\7'€€€€€€7€€€€€€€€€€€€€€- sygn. dźwiękowy BEL 
\n€€€€€€€'\12'€€€€€10€€€€€€€€€€€€€- nowy wiersz LF 
\t€€€€€€€'\11'€€€€€9€€€€€€€€€€€€€€- tabulacja pozioma HT 
\v€€€€€€ '\13'€€€€€11€€€€€€€€€€€€€- tabulacja pionowa VT 
\b€€€€€€€'\10'€€€€€8€€€€€€€€€€€€€€- cofnięcie kursora o 1 znak  
\r€€€€€€€'\15'€€€€€13€€€€€€€€€€€€€- powrót do początku linii CR 
\f€€€€€€€'\14'€€€€€12€€€€€€€€€€€€€- nowa strona (form feed) FF 
\\€€€€€€€'\134'€€€€92€€€€€€€€€€€€€- poprostu znak backslash "\"  
\'€€€€€€€'\47'€€€€€39€€€€€€€€€€€€€- apostrof "'"  
\"€€€€€€€'\42'€€€€€34€€€€€€€€€€€€€- cudzysłów (")  
\0€€€€€€€'\0'€€€€€€0€€€€€€€€€€€€€€- NULL (znak pusty) 
 
Komputer przechowuje znak w swojej pamięci jako "krótką", bo  
zajmującą tylko jeden bajt liczbę całkowitą (kod ASCII znaku).  
Na tych liczbach wolno Ci wykonywać operacje arytmetyczne !  
(Od czego mamy komputer?) Przekonaj się o tym uruchamiając  
następujący program.  
 

[P014.CPP]  

 
# include <stdio.h>      //prototypy printf() i scanf() 
# include <conio.h>      //prototypy clrscr() i getch() 
int liczba;              //deklaracja zmiennej "liczba" 
 
int main(void)  
{  
  clrscr();  
  printf("Wydrukuje A jako \nLiteral znakowy:\tKod ASCII:\n");  
  printf("%c", 'A');  
  printf("\t\t\t\t%d", 'A');  

72

background image

  printf("\nPodaj mi liczbe ?    ");  
  scanf("%d", &liczba);  
  printf("\n%c\t\t\t\t%d\n", 'A'+liczba, 'A'+liczba);  
  scanf("%d", &liczba);  
  printf("\n%c\t\t\t\t%d", 'A'+liczba, 'A'+liczba);  
  getch();  
  return 0;  
}  
 
Uruchom program kilkakrotnie podając różne liczby całkowite z  
zakresu od 1 do 100.  
 
Przyjrzyj się sposobowi formatowania wyjścia:  
%c, %d, \t, \n  
 
Jeśli pamiętasz, że kody ASCII kolejnych liter A,B,C... i  
kolejnych cyfr 1, 2, 3 są kolejnymi liczbami, to zauważ, że  
wyrażenia:  
 
'5' + 1 = '6' oraz 'A' + 2 = 'C'  
(czytaj: kod ASCII "5" + 1 = kod ASCII "6") 
są poprawne.  
 

Jak sprawdzić kod ASCII znaku?  

________________________________________________________________ 
Można oczywście nauczyć się tabeli kodów ASCII na pamięć (dla  
początkowych i najważniejszych stronic kodowych - przede  
wszystkom od 0 do 852). Dla hobbystów - stronica kodowa 1250 i  
1252 też czasem się przydaje.  
(to oczywiście żart - autor nie zna ani jednego faceta o tak  
genialnej pamięci)  
Można skorzystać z edytora programu Norton Commander. W trybie  
Edit [F4] po wskazaniu kursorem znaku w górnym wierszu po prawej 
 
stronie zostanie wyświetlony jego kod ASCII.  
________________________________________________________________ 
 

CZY PROGRAM NIE MÓGŁBY CHODZIĆ W KÓŁKO? 

 
Twoja intuicja programisty z pewnością podpowiada Ci, że gdyby  
zmusić komputer do pracy w pętli, to nie musiałbyś przykładowych 
 
programów uruchamiać wielokrotnie. Spróbujmy nakazać programowi  
przykładowemu chodzić "w kółko". To proste - dodamy do programu: 
 
 
* na końcu rozkaz skoku bezwarunkowego goto (idź do...),  
* a żeby wiedział dokąd ma sobie iść - na początku programu  
zaznaczymy miejsce przy pomocy umownego znaku - ETYKIETY.  
 
Zwróć uwagę, że pisząc pliki wsadowe typu *.BAT w języku BPL  
(Batch Programming Language - język programowania wsadowego)  
stawiasz dwukropek zawsze na początku etykiety:  
 
:ETYKIETA  (BPL)  
 
a w języku C++ zawsze na końcu etykiety:  

73

background image

 
ETYKIETA:  (C/C++) 
 
Przystępujemy do opracowania programu. 
 

[P015.CPP]  

 
# include <stdio.h>  
 
short int liczba;  
 
int main(void)  
{  
  clrscr();  
  printf("Wydrukuje A jako \nLiteral znakowy:\tKod ASCII:\n");  
  printf("%c", 'A');  
  printf("\t\t\t\t%d", 'A');  
etykieta:  
  printf("\npodaj mi liczbe ?    ");  
  scanf("%d", &liczba);  
  printf("\n%c\t\t\t\t%d\n", 'A'+liczba, 'A'+liczba);  
goto etykieta;  
  return 0;  

 

Skompiluj program do wersji *.EXE: 

Compile | Make 
 (rozkazem Make EXE file z menu Compile). Musisz nacisnąć  
następujące klawisze:  
[Alt]-[C], [M]. (lub [F9]) 
 
* Jeśli wystąpiły błędy, popraw i powtórz próbę kompilacji.  
* Uruchom program [Alt]-[R], [R] (lub [Ctrl]-[F9]). 
* Podaj kilka liczb: np. 1,2,5,7,8 itp.  
* Przerwij działanie programu naciskając kombinację klawiszy  
[Ctrl]+[Break] lub [Ctrl]+[C].  
* Sprawdź, jaki jest katalog wyjściowy kompilatora.  
- Rozwiń menu Options [Alt]-[O],  
- Otwórz okienko Directories... [D],  
- Sprawdź zawartość okienka tekstowego Output Directory.  
Teraz wiesz już gdzie szukać swojego programu w wersji *.EXE. 
 
- Uruchom program poza środowiskiem IDE.  
- Sprawdź reakcję programu na klawisze:  
[Esc], [Ctrl]-[C], [Ctrl]-[Break].  
 
Uruchom powtórnie kompilator C++ i załaduj program rozkazem:  
BC A:\GOTOTEST.CPP  
 
Wykonaj od nowa kompilację programu [F9].  
 
[???] ... is up to date... 
________________________________________________________________ 

Jeśli C++ nie zechce powtórzyć kompilacji i odpowie Ci:  

 
€€€€€€€€€€€€€€€€€€€€Making  

74

background image

A:\GOTOTEST.CPP  
 
€€€€€€€€€€€€€€€is up to date  
 
(Program w takiej wersji już skompilowałem, więcej nie będę!) 
 
nie przejmuj się. Dokonaj jakiejkolwiek pozornej zmiany w  
programie (np. dodaj spację lub pusty wiersz w dowolnym  
miejscu). Takich pozornych zmian wystarczy by oszukać C++. C++  
nie jest na tyle inteligentny, by rozróżniać zmiany rzeczywiste  
w pliku źródłowym od pozornych.  
________________________________________________________________ 
 
 
Powtórz kompilację programu. Nie musisz uruchamiać programu.  
Zwróć uwagę tym razem na pojawiające się w okienku komunikatów  
ostrzeżenie:  
 
Warning: A:\GOTOTEST.CPP 14: Unreachable code in function main.  
(Uwaga: Kod programu zawiera takie rozkazy, które nigdy nie  
zostaną wykonane inaczej - "są nieosiągalne").  
 
O co chodzi? Przyjrzyj się tekstowi programu. Nawet jeśli po  
rozkazie skoku bezwarunkowego: 
 
goto etykieta;  
 
dopiszesz jakikolwiek inny rozkaz, to program nigdy tego rozkazu 
 
nie wykona. Właśnie o to chodzi. Program nie może nawet nigdy  
wykonać rozkazu "return 0", który dodaliśmy "z przyzwyczajenia". 
 
Pętla programowa powinna być wykonywana w nieskończoność. Taka  
pętla nazywa się pętlą nieskończoną (ang. infinite loop). 
 
Mimo to i w środowisku IDE (typowy komunikat: User break) i w  
środowisku DOS tę pętlę uda Ci się przerwać.  
Kto wobec tego przerwał działanie Twojego programu? Nieskończoną 
 
pętlę programową przerwał DOS. Program zwrócił się do systemu  
DOS, a konkretnie do którejś z DOS'owskich funkcji obsługi  
WEJŚCIA/WYJŚCIA i to DOS wykrył, że przycisnąłeś klawisze  
[Ctrl]-[C] i przerwał obsługę Twojego programu. Następnie DOS  
"wyrzucił" twój program z pamięci operacyjnej komputera i  
zgłosił gotowość do wykonania dalszych Twoich poleceń - swoim  
znakiem zachęty C:\>_ lub A:\>_.  
 
Spróbujmy wykonać taki sam "face lifting" i innych programów  
przykładowych, dodając do nich najprostszą pętlę. Zanim jednak  
omówimy szczegóły techniczne pętli programowych w C++ rozważmy  
prosty przykład. Wyobraźmy sobie, że chcemy wydrukować na  
ekranie kolejne liczby całkowite od 2 do np. 10. Program  
powinien zatem liczyć ilość wykonanych pętli, bądź sprawdzać,  
czy liczba przeznaczona do drukowania nie stała się zbyt duża.  
 
W C++ do takich konstrukcji używa się kilku bardzo ważnych słów  
kluczowych:  
 

75

background image

kilka ważnych słów kluczowych - some important keywords

________________________________________________________________ 
for - dla (znaczenie jak w Pascalu i BASICu)  
while - dopóki  
do - wykonuj  
if - jeżeli  
break - przerwij wykonywanie pętli  
continue - kontynuuj pętelkowanie  
goto - skocz do wskazanej etykiety  
________________________________________________________________ 
 
Nasz program mógłby przy zastosowaniu tych słów zostać napisany  
np. tak:  
 

[LOOP-1]  

 
#include <iostream.h>  
void main()  
{  
int x = 2;  
petla:  
cout << x << '\n';  
x = x + 1;  
if (x < 11) goto petla;  
}  
 
Możemy zastosować rozkaz goto w postaci skoku bezwarunkowego, a  
pętelkowanie przerwać rozkazem break:  
 

[LOOP-2]  

 
#include <iostream.h>  
void main()  
{  
int x = 2;  
petla:  
cout << x << '\n';  
x = x + 1;  
if(x > 10) break; 
goto petla;  
}  
 

Możemy zastosować pętlę typu for:  

 

[LOOP-3]  

 
#include <iostream.h>  
int main(void)  
{  
for(int x = 2; x < 11; x = x + 1)  
 { 
   cout << x << '\n';  
 } 
return 0; 

76

background image

}  
 

Możemy zastosować pętlę typu while:  

 

[LOOP-4]  

 
#include <iostream.h>  
int main(void)  
{  
int x = 2;  
while (x < 11) 
 { 
   cout << x << '\n';  
   x = x + 1; 
 } 
return 0; 
}  
 

Możemy także zastosować pętlę typu do-while:  

 

[LOOP-5]  

 
#include <iostream.h>  
int main(void)  
{  
int x = 2;  
do 
 { 
   cout << x << '\n';  
   x = x + 1; 
 }while (x < 11); 
return 0; 
}  
 
Możemy wreszcie nie precyzować warunków pętelkowania w nagłówku  
pętki for, lecz przerwać pętlę w jej wnętrzu (po osiągnięciu  
określonego stanu) przy pomocy rozkazu break:  

Przy pomocy rozkazu break

[LOOP-6]  

 
#include <iostream.h>  
int main(void)  
{  
for(;;)  
 { 
   cout << x << '\n';  
   x++; 
   if( x > 10) break; 
 } 
return 0; 
}  

77

background image

 
Wszytkie te pętle (sprawdź!) będą działać tak samo. Spróbuj przy 
 
ich pomocy, zanim przejdziesz dalej, wydrukować np. liczby od 10 
 
do 100 i wykonaj jeszcze kilka innych eksperymentów.  
Dokładniejszy opis znajdziesz w dalszej części książki, ale  
przykład - to przykład.  
 
Wróćmy teraz do "face-liftingu" naszych poprzednich programów.  
Ponieważ nie możemy sprecyzować żadnych warunków, każemy  
programowi przykładowemu wykonywać pętlę bezwarunkowo. 
 
Wpisz tekst programu:  
 

[P016.CPP]  

 
// Przyklad FACELIFT.CPP 
// Program przykladowy 10na16.CPP / 16na10.CPP FACE LIFTING. 
 
# include <stdio.h> 
 
int liczba;  
 
int main()  
{  
  clrscr();  
  printf("Kropka = KONIEC \n"); 
  for(;;) 
  { 
    printf("Podaj liczbe dziesietna calkowita ? \n");  
    scanf("%d", &liczba);  
    printf("Szesnastkowo to wynosi:\n");  
    printf("%X",liczba);  
    getch();  
    printf("Podaj liczbe SZESNASTKOWA-np.DF- DUZE LITERY: \n");  
    scanf("%X", &liczba);  
    printf("%s","Dziesietnie to wynosi:  ");  
    printf("%d",liczba);  
    if(getch() == '.') break;  
  } 
  return 0;  

 
- Uruchom program Run, Run.  
- Dla przetestowania działania programu:  
* podaj kolejno liczby o różnej długości 1, 2, 3, 4, 5, 6  
cyfrowe;  
* zwróć uwagę, czy program przetwarza poprawnie liczby dowolnej  
długości?  
- Przerwij program naciskając klawisz z kropką [.]  
- Zapisz program na dysk [F2]. 
- Wyjdź z IDE naciskając klawisze [Alt]-[X].  
 
Zwróć uwagę na dziwny wiersz: 
 
    if(getch() == '.') break;  
 
C++ wykona go w następującej kolejności:  

78

background image

1) - wywoła funkcję getch(), poczeka na naciśnięcie klawisza i  
wczyta znak z klawiatury:  
                                            getch()  
2) - sprawdzi, czy znak był kropką: 
                                          (getch() == '.') ? 
3) - jeśli TAK - wykona rozkaz break i przerwie pętlę,  
            if(getch() == '.') break; 
   - jeśli NIE - nie zrobi nic i pętla "potoczy się" dalej.  
if(getch() != '.') ...--> printf("Podaj liczbe dziesietna... 
 

Zmiany

 
________________________________________________________________ 
2. Opracuj program pobierający znak z klawiatury i podający w  
odpowiedzi kod ASCII pobranego znaku dziesiętnie.  
3. Opracuj program pobierający liczbę dziesiętną i podający w  
odpowiedzi:  
* kod ósemkowy,  
* kod szesnastkowy,  
* znak o zadanym  
** dziesiętnie 
** szesnastkowo 
kodzie ASCII.  

LEKCJA 10 Jakie operatory stosuje C++. 

_______________________________________________________________  
Podczas tej lekcji:  
* Poznasz operatory języka C++.  
* Przetestujesz działanie niektórych operatorów.  
* Dowiesz się więcej o deklarowaniu i inicjowaniu zmiennych. 
_______________________________________________________________  
 
Słów kluczowych jest w języku C++ stosunkowo niewiele, za to  
operatorów wyraźnie więcej niż np. w Basicu. Z kilku operatorów  
już korzystałeś w swoich programach.  pełną listę operatorów  
wraz z krótkim wyjaśnieniem przedstawiam poniżej. Operatory C++  
są podzielone na 16 grup i można je scharakteryzować:  
 
* priorytetem  
** najwyższy priorytet ma grupa  1 a najniższy grupa 16 -  
 przecinek, np. mnożenie ma wyższy priorytet niż dodawanie; 
** wewnątrz każdej z 16 grup priorytet operatorów jest równy; 
* łącznością (wiązaniem). 
 

kolejność, priorytet. Precedence

________________________________________________________________ 
Dwie cechy opertorów C++ priorytet i łączność decydują o  
sposobie obliczania wartości wyrażeń.  
Precedence - kolejność, priorytet. 
Associativity - asocjatywność, łączność, wiązanie. Operator jest 
łączny lewo/prawo-stronnie, jeśli w wyrażeniu zawierającym na  

79

background image

tym samym poziomie hierarchii nawiasów min. dwa identyczne  
operatory najpierw jest wykonywany operator lewy/prawy. Operator 
jest łączny, jeśli kolejność wykonania nie wpływa na wynik.  
________________________________________________________________ 
 
Przykład:  
a+b+c+d = (a+d)+(c+b) 
 
[S] 
________________________________________________________________ 
ASSIGN(ment) - Przypisanie. 
EQAL(ity) - Równy, odpowiadający. 
BITWISE - bit po bicie (bitowo).  
REFERENCE - odwołanie do..., powołanie się na..., wskazanie  
na... .  
 

Funkcje logiczne:  

OR - LUB - suma logiczna (alternatywa).  
AND - I - iloczyn logiczny.  
XOR (eXclusive OR) - ALBO - alternatywa wyłączająca. 
NOT - NIE - negacja logiczna. 
________________________________________________________________ 
 
 

Oznaczenia łączności przyjęte w Tabeli: 

 
{L->R} €€€(Left to Right) z lewa na prawo.  
{L<<-R} €€(Right to Left) z prawa na lewo. 
 
 

Lista operatorów języka C++. 

________________________________________________________________ 
Kategoria€| Operator€€€€€| €€€Co robi / jak działa  
----------|--------------|-------------------------------------- 
 
1. Highest| ()€€€€€€€€€€€| * ogranicza wyrażenia,  
(Najwyższy|Parentheses   | * izoluje wyrażenia warunkowe,  
priorytet)|€€€€€€€€€€€€€€| * wskazuje na wywołanie funkcji,  
 {L->R}€€€|€€€€€€€€€€€€€€|   grupuje argumenty funkcji.  
€€€€€€€€€€|--------------|-------------------------------------- 
€€€€€€€€€€| []€€€€€€€€€€€| zawartość jedno- lub wielowymiarowych 
€€€€€€€€€€|Brackets      | tablic  
€€€€€€€€€€|--------------|-------------------------------------- 
€€€€€€€€€€| .            |(direct component selector)  
€€€€€€€€€€| ->           |(indirect, or pointer, selection) 
€€€€€€€€€€|€€€€€€€€€€€€€€| Bezpośrednie lub pośrednie wskazanie  
          |              |  elementu unii bądź struktury.  
          |--------------|-------------------------------------- 
          | ::           | Operator specyficzny dla C++.  
          |              | Pozwala na dostęp do nazw GLOBALNYCH, 
          |              | nawet jeśli zostały "przysłonięte"  
          |              | przez LOKALNE.  
----------|--------------|-------------------------------------- 
2. €€€€€€€| ! €€€€€€€€€€€| Negacja logiczna (NOT)  
 Jednoar-€|--------------|------------------------------------  
gumentowe | ~            | Zamiana na kod KOMPLEMENTARNY bit po  

80

background image

(Unary)   |              | bicie. Dotyczy liczb typu int.  
 {L<<-R}  |--------------|-------------------------------------- 
          | +            | Bez zmiany znaku (Unary plus)  
          |--------------|-------------------------------------- 
          | -            | Zmienia znak liczby / wyrażenia  
          |              | (Unary minus)  
          |--------------|-------------------------------------- 
          | ++           | PREinkrementacja/POSTinkrementacja  
          |--------------|-------------------------------------- 
          | --           | PRE/POSTdekrementacja  
          |--------------|-------------------------------------- 
          | &            | Operator adresu(Referencing operator) 
          |--------------|-------------------------------------- 
          | *            | Operator wskazania  
          |              | (Dereferencing operator)  
          |--------------|-------------------------------------- 
          | sizeof       | Zwraca wielkość argumentu w bajtach  
          |--------------|-------------------------------------- 
          | new          | Dynamiczne zarządzanie pamięcią:  
          | delete       | new - przydziela pamięć,  
          |              | delete - likwiduje przydział pamięci  
----------|--------------|-------------------------------------- 
3. Multi- | *            | Mnożenie (UWAGA: Druga rola "*") 
plikatywne|--------------|-------------------------------------- 
 {L->R}   | /            | Dzielenie  
          |--------------|-------------------------------------- 
          | %            | Reszta z dzielenia (modulo)  
----------|--------------|-------------------------------------- 
4. Dostępu| .*           | Operatory specyficzne dla C++. 
(Member   |(dereference) | Skasowanie bezpośredniego wskazania 
access)   |              | na członka klasy (Class Member).  
 {L->R}   |--------------|-------------------------------------- 
          | ->*          | Skasowanie pośredniego wskazania typu 
objektowe |              | "wskaźnik do wskaźnika" 
----------|--------------|-------------------------------------- 
5. Addy - | +            | Dodawanie dwuargumentowe. 
tywne     |--------------|-------------------------------------- 
 {L->R}   | -            | Odejmowanie dwuargumentowe.  
----------|--------------|-------------------------------------- 
6. Przesu-| <<           | Binarne przesunięcie w lewo. 
nięcia    |--------------|-------------------------------------- 
(Shift)   | >>           | Binarne przesunięcie w prawo.  
 {L->R}   |              | (bit po bicie)  
----------|--------------|-------------------------------------- 
7. Relacji| <            | Mniejsze niż...  
 {L->R}   |--------------|-------------------------------------- 
          | >            | Większe niż....  
          |--------------|-------------------------------------- 
          | <=           | Mniejsze lub równe.  
          |--------------|-------------------------------------- 
          | >=           | Większe lub równe.  
----------|--------------|-------------------------------------- 
8.Równości| ==           | Równe (równa się).  
 {L->R}   | !=           | Nie równe.  
----------|--------------|-------------------------------------- 
9.        | &            | AND binarnie (Bitwise AND)  
 {L->R}   |              | UWAGA: Druga rola "&".  
----------|--------------|-------------------------------------- 
10.       | ^            | XOR binarnie (Alternatywa wyłączna).  
 {L->R}   |              | UWAGA: To nie potęga !  

81

background image

----------|--------------|-------------------------------------  
11.{L->R} | |            | OR binarnie (bit po bicie)  
----------|--------------|-------------------------------------  
12.{L->R} | &&           | Iloczyn logiczny (Logical AND).  
----------|--------------|-------------------------------------  
13.{L->R} | ||           | Suma logiczna (Logical OR).  
----------|--------------|-------------------------------------- 
14. Oper. | ?:           | Zapis  a ? x : y  oznacza:  
Warunkowy |              | "if a==TRUE then x else y"  
Conditional              | gdzie TRUE to logiczna PRAWDA "1".  
 {L<<-R}  |              |  
----------|--------------|-------------------------------------- 
15. Przy- | =            | Przypisz wartość (jak := w Pascalu)  
pisania   |--------------|-------------------------------------- 
 {L<<-R}  | *=           | Przypisz iloczyn. Zapis X*=7  
          |              | oznacza: X=X*7 (o 1 bajt krócej!).  
          |--------------|-------------------------------------- 
          | /=           | Przypisz iloraz.  
          |--------------|-------------------------------------- 
          | %=           | Przypisz resztę z dzielenia.  
          |--------------|-------------------------------------- 
          | +=           | Przypisz sumę X+=2 oznacza "X:=X+2"  
          |--------------|-------------------------------------- 
          | -=           | Przypisz różnicę X-=5 ozn. "X:=X-5" 
          |--------------|-------------------------------------- 
          | &=           | Przypisz iloczyn binarny ( Bitwise  
          |              |  AND) 
          |              | bit po bicie.  
          |--------------|-------------------------------------- 
          | ^=           | Przypisz XOR bit po bicie.  
          |--------------|-------------------------------------- 
          | |=           | Przypisz sumę log. bit po bicie.  
          |--------------|-------------------------------------- 
          | <<=          | Przypisz wynik przesunięcia o jeden  
          |              | bit w lewo.  
          |--------------|-------------------------------------- 
          | >>=          | j. w. o jeden bit w prawo.  
----------|--------------|-------------------------------------- 
16. Prze- | ,            | Oddziela elementy na liście argu - 
cinek     |              | mentów funkcji,                       
(Comma)   |              | Stosowany w specjalnych wyrażeniach 
 {L->R}   |              | tzw. "Comma Expression".             
----------|--------------|-------------------------------------  
UWAGI: 
* Operatory # i ## stosuje się tylko w PREPROCESORZE. 
* Operatory << i >> mogą w C++ przesyłać tekst do obiektów cin i 
 
cout dzięki tzw. Overloadingowi (rozbudowie, przeciążeniu)  
operatorów. Takiego rozszerzenia ich działania dokonali już  
programiści producenta w pliku nagłówkowym IOSTREAM.H>  
 
 
Gdyby okazało się, że oferowane przez powyższy zestaw operatory  
nie wystarczają Ci lub niezbyt odpowiadają, C++ pozwala na tzw.  
OVERLOADING, czyli przypisanie operatorom innego, wybranego  
przez użytkownika działania. Można więc z operatorami robić  
takie same sztuczki jak z identyfikatorami. Sądzę jednak, że ten 
zestaw nam wystarczy, w każdym razie na kilka najbliższych  
lekcji.  
 

82

background image

Podobnie, jak pieniądze na bezludnej wyspie, niewiele warta jest 
wiedza, której nie można zastosować praktycznie. Przejdźmy więc  
do czynu i przetestujmy działanie niektórych operatorów w  
praktyce. 
 

TEST OPERATORÓW JEDNOARGUMENTOWYCH. 

 
Otwórz plik nowego programu:  
* Open [F3],  
* Wpisz:  
 
A:\UNARY.CPP  
 
* Wybierz klawisz [Open] w okienku lub naciśnij [Enter].  
 
Wpisz tekst programu:  
 

[P017.CPP ]  

  
// UNARY.CPP - operatory jednoargumentowe   
 
# include <stdio.h>   
# include <conio.h>  
float x;   
  
void main(void)   
{   
clrscr();  
  for(;;) 
    {  
     printf("\n Podaj liczbe...\n");  
     scanf("%f", &x);  
     printf("\n%f\t%f\t%f\n", x, +x, -x );  
     printf("\n%f", --x );  
     printf("\t%f", x );  
     printf("\t%f", ++x);  
     if(getch() = '.') break; 
    }; 
}  
  
 
Zwróć uwagę, że po nawiasie zamykającym pętlę nie ma tym razem  
żadnego rozkazu. Nie wystąpi także ostrzeżenie (Warning:) przy  
kompilacji.  
 
Uruchom program Run | Run. Popraw ewentualne błędy. 
 
Podając różne wartości liczby x:  
- dodatnie i ujemne,  
- całkowite i rzeczywiste,  
przeanalizuj działanie operatorów. 
Przerwij program naciskając klawisz [.] 
 
Zmodyfikuj w programie deklarację typu zmiennej X wpisując  
kolejno:  
- float x;     (rzeczywista) 
- int x; €€€€€€(całkowita) 

83

background image

- short int x; €€€€€(krótka całkowita) 
- long int x;€€€€€(długa całkowita)  
 
Zwróć uwagę, że zmiana deklaracji zmiennej bez JEDNOCZESNEJ  
zmiany formatu w funkcjach scanf() i printf() spowoduje  
komunikaty o błędach. 
 
Spróbuj samodzielnie dobrać odpowiednie formaty w funkcjach  
scanf() i printf(). Spróbuj zastosować zamiast funkcji printf()  
i scanf() strumienie cin i cout. Pamiętaj o dołączeniu  
właściwych plików nagłówkowych.  
 
Jeśli miałeś kłopot z dobraniem stosownych formatów, nie  
przejmuj się. Przyjrzyj się następnym przykładowym programom.  
Zajmijmy się teraz dokładniej INKREMENTACJĄ, DEKREMENTACJĄ i  
OPERATORAMI PRZYPISANIA.  
 
1. Zamknij zbędne okna na ekranie. Pamuiętaj o zapisaniu  
programów na dyskietkę/dysk w tej wersji, która poprawnie działa 
 
lub w ostatniej wersji roboczej. 
2. Otwórz plik:  
ASSIGN.CPP  
3. Wpisz tekst programu:  
 

[P018.CPP]  

  
# include <stdio.h>   
# include <conio.h>   
  
long int x;  
short int krok;  
char klawisz;  
 
int main()  
{  
  clrscr();  
  printf("Test operatora przypisania += \n");  
  x=0;  
  printf("Podaj KROK ?      \n");  
  scanf("%d",&krok);  
    for(;;) 
     {  
      printf("\n%d\n", x+=krok);  
      printf("[Enter] - dalej        [K] - Koniec\n");  
      klawisz = getch();  
         if (klawisz=='k'|| klawisz=='K') goto koniec;  
     }  
koniec:  
  printf("\n Nacisnij dowolny klawisz...");  
  getch();  
  return 0;  
}  
  
W tym programie już sami "ręcznie" sprawdzamy, czy nie pora  
przerwać pętlę. Zamiast użyć typowej instrukcji break (przerwij) 
 
stosujemy nielubiane goto, gdyż jest bardziej uniwersalne i w  
przeciwieństwie do break pozwala wyraźnie pokazać dokąd  

84

background image

następuje skok po przerwaniu pętli. Zwróć uwagę na nowe elementy 
 
w programie:  
 

DEKLARACJE ZMIENNYCH:  

long int x;    (długa, całkowita) 
short int krok; €€€€(krótka, całkowita)  
char klawisz;€€€€€(zmienna znakowa)  
 

INSTRUKCJĘ WARUNKOWĄ:  

if (KLAWISZ=='k'|| KLAWISZ=='K') goto koniec;  
(JEŻELI zmienna KLAWISZ równa się "k" LUB równa się "K"  
idź do etykiety "koniec:")  
* Warunek sprawdzany po słowie if jest ujęty w nawiasy. 
* Nadanie wartości zmiennej znakowej char klawisz przez funkcję: 
 
klawisz = getch();  
 
4. Skompiluj program. Popraw ewentualne błędy.  
5. Uruchom program. Podając różne liczby (tylko całkowite!)  
prześledź działanie operatora.  
6. Zapisz poprawną wersję programu na dysk/dyskietkę [F2].  
7. Jeśli masz już dość, wyjdź z TC - [Alt]-[X], jeśli nie,  
pozamykaj tylko zbędne okna i możesz przejść do zadań do  
samodzielnego rozwiązania -> [Z]!  
 
[Z]  
________________________________________________________________ 
1. Do programu przykładowego wstaw kolejno różne operatory  
przypisania:  
 
*=, -=, /= itp.  
 
Prześledź działanie operatorów.  
 
2. W programie przykładowym zmień typ zmiennych:  
long int x;             na          float x; 
short int KROK;                     float KROK;  
Przetestuj działanie operatorów w przypadku liczb  
zmiennoprzecinkowych.  
 
3. Zapisz w języku C++  
* negację iloczynu logicznego,  
* sumę logiczną negacji dwu warunków. 
________________________________________________________________ 
 

TEST OPERATORÓW PRE/POST-INKREMENTACJI. 

 
W następnym programie zilustrujemy działanie wszystkich pięciu  
operatorów inkrementacji (dekrementacja to też inkrementacja  
tylko w przeciwną stronę). 
 

[P019.CPP]  

 

85

background image

# include <stdio.h>  
# include <conio.h>  
 
int b,c,d,e; 
int i;  
int STO = 100;  
 
void main(void)  
{  
  clrscr();  
  printf("Demonstruje dzialanie \n");  
  printf("        PREinkrementacji   POSTinkrementacji");  
  printf("\nNr€€€€--X€€€€€€++X€€€€€€€€€€€€X--€€€€€€€X++ \n");  
 
b = c = d = e = STO;  
  for(i=1; i<6; i++) 
   {  
    printf("%d\t%d\t%d\t\t%d\t%d\t\n", i,--b,++c,d--,e++);  
   }  
getch();  

 

PRE / POSTINKREMENTACJA.  

________________________________________________________________ 

INKREMENTACJA

 oznacza zwiększenie liczby o jeden,  

DEKREMENTACJA

 oznacza zmniejszenie liczby o jeden.  

PRE oznacza wykonanie in/de-krementacji przed użyciem zmiennej,  
POST - in/de-krementację po użyciu zmiennej.  
________________________________________________________________ 
 
Działanie możesz prześledzić na wydruku, który powinien Ci dać  
program przykładowy INDEKREM.CPP:  
 
Demonstruje dzialanie   
        PREinkrementacji   POSTinkrementacji  
Nr€€€€--X€€€€€€++X€€€€€€€€€€€€X--€€€€€€€X++   
1      99      101           100        100  
2      98      102            99        101  
3      97      103            98        102  
4      96      104            97        103  
5      95      105            96        104  
  

JAK KORZYSTAĆ Z DEBUGGERA? 

 
Uruchom program powtórnie naciskając klawisz [F7]. Odpowiada to  
poleceniu Trace into (włącz śledzenie) z menu Run. Prześledzimy  
działanie programu przy pomocy Debuggera.  
 
Po wykonaniu kompilacji (lub odstąpieniu od kompilacji, jeśli  
nie dokonałeś zmian w programie) pojawił się na ekranie pasek  
wyróżnienia wokół funkcji main(), bo to od niej rozpoczyna się  
zawsze wykonanie programu. Naciśnij powtórnie [F7].  
 
Pasek przesunął się na funkcję clrscr();. Mignął na chwilę ekran 
 
użytkownika, ale na razie nie ma po co tam zaglądać, więc  
wykonamy kolejne kroki. Podam klejno: [Klawisz]-[wiersz]. 

86

background image

 
[F7] - printf("Demonstruję...");  
 
Zaglądamy na ekran użytkownika [Alt]-[F5].....[Enter] - wracamy  
do edytora.  
 
[F7],[F7]... doszliśmy do wiersza  
b=c=d=e=STO;  
 
Zapraszamy teraz debugger do pracy wydając mu polecenie "Wykonaj 
Inspekcję" [Alt]-[D] | Inspect. Pojawia się okienko dialogowe  
"Inspect".  
* Wpisz do okienka tekstowego nazwę zmiennej b i naciśnij  
[Enter].  
 
Pojawiło się okienko dialogowe "Inspecting b" zawierające  
fizyczny adres pamięci RAM, pod którym umieszczono zmienną b i  
wartość zmiennej b (zero; instrukcja przypisania nada jej  
wartość 100). Naciśnij [Esc]. Okienko zniknęło.  
 
[F7] - for(i=1; i<6; i++);  
 
* Naprowadź kursor na zmienną d w tekście programu i wykonaj  
inspekcję powtórnie [Alt]-[D], [I]. Jak widzisz w okienku  
zmiennej d została nadana wartość 100. Naciśnij [Esc].  
 
Dokonamy teraz modyfikacji wartości zmiennej przy pomocy  
polecenia Evaluate and Modify (sprawdź i zmodyfikuj) z menu  
Debug.  
* Naciśnij klawisze [Alt]-[D], [E]. Pojawiło się okienko  
dialogowe "Evaluate and Modify". W okienku tekstowym  
"Expression" (wyrażenie) widzisz swoją zmienną d.  
* Przejdź przy pomocy [Tab] do okienka tekstowego "New Value"  
(nowa wartość) i wpisz tam liczbę 1000. Naciśnij [Enter] a  
następnie [Esc]. Okienko zamknęło się. Zmiana wartości zmiennej  
została dokonana.  
 
[F7] - printf("...") - wnętrze pętli for.  
 
[F7] - wykonała się pętla.  
 
Obejrzyjmy wyniki [Alt]-[F5].  
 
W czwartej kolumnie widzisz efekt wprowadzonej zmiany:  
 

Demonstruje dzialanie   

        PREinkrementacji   POSTinkrementacji  

Nr€€€€--X€€€€€€++X€€€€€€€€€€€€X--€€€€€€€X++   
1      99       101          1000      100  
2      98       102           999      101  
3      97        103          998      102  
4      96        104          997      103  
5      95        105          996      104  
  
Zwróć uwagę w programie przykładowym na: 
 
* Zliczanie ilości wykonanych przez program pętli.  

87

background image

 
int i;  (deklaracja, że i będzie zmienną całkowitą) 
... 
i=1;  (zainicjowanie zmiennej, nadanie początkowej wartości) 
...  
i++;  (powiększanie i o 1 w każdej pętli) 
...  
i<6  (warunek kontynuacji) 
 
 
* Możliwość grupowej deklaracji zmiennych tego samego typu:  
int b,c,d,e;  
 
[Z]  
________________________________________________________________ 
4. Zmień w programie przykładowym wartość początkową STO na  
dowolną inną - np. zero. Przetestuj działanie programu.  
5. Sprawdź, czy można wszystkie zmienne używane w programie  
przykładowym zadeklarować wspólnie (jeden wiersz zamiast  
trzech).  
________________________________________________________________ 

LEKCJA 11. Jak deklarować zmienne. Co to jest wskaźnik. 

________________________________________________________________ 
 
W trakcie tej lekcji:  
1. Dowiesz się więcej o deklaracjach. 
2. Poprawisz trochę system MS DOS. 
3. Dowiesz się co to jest wskaźnik i do czego służy. 
________________________________________________________________ 
 
 

Więcej o deklaracjach.  

 
Deklarować można w języku C++:  
* zmienne;  
* funkcje;  
* typy (chodzi oczywiście o typy "nietypowe").  
 
Zmienne w języku C++ mogą mieć charakter:  
* skalarów - którym przypisuje się nierozdzielne dane np.  
całkowite, rzeczywiste, wskazujące (typu wskaźnik) itp.  
* agregatów - którym przypisuje się dane typu strukturalnego np. 
 
obiektowe, tablicowe czy strukturowe.  
 
Powyższy podział nie jest tak całkiem precyzyjny, ponieważ  
pomiędzy wskaźnikami a tablicami istnieje w języku C++ dość  
specyficzna zależność, ale więcej na ten temat dowiesz się z  
późniejszych lekcji. 
  

88

background image

Zmienne mogą być:  

 
* deklarowane,  
* definiowane i  
* inicjowane.  
 
Stała to to taka zmienna, której wartość można przypisać tylko  
raz. Z punktu widzenia komputera niewiele się to różni, bo  
miejsce w pamięci i tak, stosownie do zadeklarowanego typu  
zarezerwować trzeba, umieścić w tablicy i zapamiętać sobie  
identyfikator i adres też. Jedyna praktyczna różnica polega na  
tym, że zmiennej zadeklarowanej jako stała, np.: 
 
const float PI = 3.142;  
 
nie można przypisać w programie żadnej innej wartości, innymi  
słowy zapis:  
 
const float PI = 3.14;  
 
jest jednocześnie DEKLARACJĄ, DEFINICJĄ i ZAINICJOWANIEM stałej  
PI.  
 
Przykład :  
 
float x,y,z;€€€€€€€€€€€€€€€€€€€€€€€(DEKLARACJA)  
const float TEMP = 36.6;€€€€€€€€€€€(DEFINICJA) 
x = 42;€€€€€€€€€€€€€€€€€€€€€€€€€€€€€(ZAINICJOWANIE zmiennej) 
 
[S!] constant/variable - STAŁA czy ZMIENNA.  
________________________________________________________________ 
const - (CONSTant) - stała. Deklaracja stałej, słowo kluczowe w  
języku C.  
var - (VARiable) - zmienna. W języku C przyjmowane domyślnie.  
Słowo var (stosowane w Pascalu) NIE JEST słowem kluczowym języka 
 
C ani C++ (!).  
________________________________________________________________ 
 
Skutek praktyczny:  
* Ma sens i jest poprawna deklaracja:  
const float PI = 3.1416;  
* Niepoprawna natomiast jest deklaracja:  
var float x;  
Jeśli nie zadeklarowano stałej słowem const, to "zmienna" (var)  
przyjmowana jest domyślnie.  
 
Definicja powoduje nie tylko określenie, jakiego typu  
wartościami może operować dana zmienna bądź funkcja, która  
zostaje od tego momentu skojarzona z podanym identyfikatorem,  
ale dodatkowo powoduje:  
* w przypadku zmiennej - przypisanie jej wartości,  
* W przypadku funkcji - przyporządkowanie ciała funkcji.  
Zdefiniujmy dla przykładu kilka własnych funkcji.  
 
Przykład: 
 
void UstawDosErrorlevel(int n) /* nazwa funkcji*/ 

exit(n);   /* skromne ciało funkcji */ 

89

background image

}  
 
Przykład  
 
int DrukujAutora(void)  
{  
printf("\nAdam MAJCZAK AD 1993/95 - C++ w 48 godzin!\n");  
printf("\n Wydanie II Poprawione i uzupełnione.") 
return 0;  
}  
 
Przykład  
 
void Drukuj_Pytanie(void)  
{  
printf("Podaj liczbe z zakresu od 0 do 255");  
printf("\nUstawie Ci ERRORLEVEL\t");  
}  
 
W powyższych przykładach zwróć uwagę na:  
* sposób deklarowania zmiennej, przekazywanej jako parametr do  
funkcji - n i err;  
* definicje funkcji i ich wywołanie w programie (podobnie jak w  
Pascalu). 
 
Zilustrujemy zastosowanie tego mechanizmu w programie  
przykładowym. Funkcje powyższe są PREDEFINIOWANE w pliku  
FUNKCJE1.H na dyskietce dołączonej do książki. Wpisz i uruchom  
program:  

[P020.CPP]  

 
# include "stdio.h" 
# include "A:\funkcje1.h"  
 
int err; 
 
void main(void)  
{  
DrukujAutora(); 
Drukuj_Pytanie();  
scanf("%d", &err);  
UstawDosErrorlevel(err);  
}  
 
Wykorzystajmy te funkcje praktycznie, by zilustrować sposób  
przekazywania informacji przez pracujący program do systemu DOS. 
 
Zmienna otoczenia systemowego DOS ERRORLEVEL może być z wnętrza  
programu ustawiona na zadaną - zwracaną do systemu wartość. 
 
[Z]  
________________________________________________________________ 
1. Sprawdź, w jakim pliku nagłówkowym znajduje się prototyp  
funkcji exit(). Opracuj najprostszy program PYTAJ.EXE  
ustawiający zmienną systemową ERRORLEVEL według schematu:  
 
main()  
{  
printf("....Pytanie do użytkownika \n..."); 

90

background image

scanf("%d", &n);  
exit(n);  
}  
 
2. Zastosuj program PYTAJ.EXE we własnych plikach wsadowych typu 
 
*.BAT według wzoru:  
 
@echo off  
:LOOP 
cls 
echo 1. Wariant 1  
echo 2. Wariant 2  
echo 3. Wariant 3  
echo Wybierz wariant działania programu...1,2,3 ?  
PYTAJ  
IF ERRORLEVEL 3 GOTO START3  
IF ERRORLEVEL 2 GOTO START2  
IF ERRORLEVEL 1 GOTO START1  
echo Chyba zartujesz...?  
goto LOOP  
:START1  
'AKCJA WARIANT 1  
GOTO KONIEC  
:START2  
'AKCJA WARIANT 2  
GOTO KONIEC  
:START3  
'AKCJA WARIANT 3  
:KONIEC  
 
'AKCJA WARIANT n - oznacza dowolny ciąg komend systemu DOS, np.  
COPY, MD, DEL, lub uruchomienie dowolnego programu. Do  
utworzenia pliku wsadowego możesz zastosować edytor systemowy  
EDIT.  
 
3. Skompiluj program posługując się oddzielnym kompilatorem  
TCC.EXE. Ten wariant kompilatora jest pozbawiony zintegrowanego  
edytora. Musisz uruchomić go pisząc odpowiedni rozkaz po  
DOS-owskim znaku zachęty C:\>. Zastosowanie przy kompilacji  
małego modelu pamięci pozwol Ci uzyskać swój program w wersji  
*.COM, a nie *.EXE. Wydaj rozkaz:  
 
c:\borlandc\bin\bcc -mt -lt c:\pytaj.cpp 
 
Jeśli pliki znajdują się w różnych katalogach, podaj właściwe  
ścieżki dostępu (path). 
________________________________________________________________ 
 

 CO TO ZA PARAMETRY ???  

________________________________________________________________ 
Przez swą "ułomność" - 16 bitową szynę i segmentację pamięci  
komputery IBM PC wymusiły wprowadzenie modeli pamięci:  
TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE. Więcej informacji na  
ten temat znajdziesz w dalszej części książki. 
Parametry dotyczą sposobu kompilacji i zastosowanego modelu  
pamięci:  
-mt - kompiluj (->*.OBJ) wykorzystując model TINY  
-lt - konsoliduj (->*.COM) wykorzystując model TINY i zatem  

91

background image

odpowiednie biblioteki (do każdego modelu jest odpowiednia  
biblioteka *.LIB).  
Możesz stosować także:  
ms, mm, ml, mh, ls, lm, ll, lh.  
________________________________________________________________ 
 
 
Po instalacji BORLAND C++/Turbo C++ standardowo jest przyjmowany 
 
model SMALL. Zatem kompilacja, którą wykonujesz z IDE daje taki  
sam efekt, jak zastosowanie kompilatora bcc/tcc w następujący  
sposób: 
 
tcc -ms -ls program.c  
 
Mogą wystąpić kłopoty z przerobieniem z EXE na COM tych  
programów, w których występują funkcje realizujące arytmetykę  
zmiennoprzecinkową (float). System DOS oferuje Ci do takich  
celów program EXE2BIN, ale lepiej jest "panować" nad tym  
problemem na etapie tworzenia programu. 
 

PODSTAWOWE TYPY DANYCH W JĘZYKU C++. 

 
Język C/C++ operuje pięcioma podstawowymi typami danych:  
 
* char (znak, numer znaku w kodzie ASCII) - 1 bajt;  
* int (liczba całkowita) - 2 bajty;  
* float (liczba z pływającym przecinkiem) - 4  bajty;  
* double (podwójna ilość cyfr znaczących) - 8 bajtów;  
* void (nieokreślona) 0 bajtów.  
 
Zakres wartości przedstawiono w Tabeli poniżej.  
 
Podstawowe typy danych w C++.  
________________________________________________________________ 
 
Typ         Znak     Bajtów   Zakres wartości  
________________________________________________________________ 
 
char        signed    1       -128...+127  
int         signed    2       -32768...+32767  
float       signed    4       +-3.4E+-38 (dokładność: 7 cyfr)  
double      signed    8       1.7E+-308 (dokładność: 15 cyfr)  
void     nie dotyczy  0       bez określonej wartości.  
________________________________________________________________ 
 
signed - ze znakiem, unsigned - bez znaku.  
 
Podstawowe typy danych mogą być stosowane z jednym z czterech  
modyfikatorów:  
 
* signed / unsigned - ze znakiem albo bez znaku   
* long / short - długi albo krótki  
 
Dla IBM PC typy int i short int są reprezentowane przez taki sam 
 
wewnętrzny format danych. Dla innych komputerów może być  
inaczej. 
 

92

background image

Typy zmiennych w języku C++ z zastosowaniem modyfikatorów  
(dopuszczalne kombinacje).  
________________________________________________________________ 
 
Deklaracja        Znak    Bajtów   Wartości     Dyr. assembl. 
________________________________________________________________ 
 
char              signed    1  -128...+127             DB 
int               signed    2  -32768...+32767         DB 
short             signed    2  -32768...+32767         DB 
short int         signed    2  -32768...+32767         DB 
long              signed    4  -2 147 483 648...       DD 
                                   +2 147 483 647  
long int          signed    4  -2 147 483 648...       DW 
                                   +2 147 483 647  
unsigned char     unsigned  1   0...+255               DB 
unsigned          unsigned  2   0...+65 535            DW 
unsigned int      unsigned  2   0...+65 535            DW 
unsigned short    unsigned  2   0...+65 535            DW 
signed int        signed    2   -32 768...+32 767      DW 
signed            signed    2   -32 768...+32 767      DW 
signed long       signed    4   -2 147 483 648...      DD 
                                    +2 147 483 647  
enum              unsigned  2   0...+65 535            DW 
float             signed    4   3.4E+-38 (7 cyfr)      DD 
double            signed    8   1.7E+-308 (15 cyfr)    DQ 
long double       signed   10   3.4E-4932...1.1E+4932  DT 
far * (far pointer, 386)    6   unsigned  2^48 - 1     DF, DP 
________________________________________________________________ 
 
UWAGI: 
* DB - define byte - zdefiniuj bajt;  
  DW - define word - zdefiniuj słowo (16 bitów);  
  DD - double word - podwójne słowo (32 bity);  
  DF, DP - define far pointer - daleki wskaźnik w 386;  
  DQ - quad word - poczwórne słowo (4 * 16 = 64 bity);  
  DT - ten bytes - dziesięć bajtów. 
* zwróć uwagę, że typ wyliczeniowy enum występuje jako odrębny  
  typ danych (szczegóły w dalszej części książki).  
________________________________________________________________ 
 
 
 
Ponieważ nie ma liczb ani short float, ani unsigned short float, 
 
słowo int może zostać opuszczone w deklaracji. Poprawne są zatem 
 
deklaracje:  
 
short a;  
unsigned short b;  
 
Zapis +-3.4E-38...3.4E+38 oznacza:  
-3.4*10^+38...0...+3.4*10^-38...+3.4*10^+38  
Dopuszczalne są deklaracje i definicje grupowe z zastosowaniem  
listy zmiennych. Zmienne na liście należy oddzielić przecinkami: 
 
int a=0, b=1, c, d;  
float PI=3.14, max=36.6;  
 

93

background image

Poświęcimy teraz chwilę drugiej funkcji, którą już wielokrotnie  
stosowaliśmy - funkcji wejścia - scanf().  
 

FUNKCJA scanf().  

 
Funkcja formatowanego wejścia ze standardowego strumienia  
wejściowego (stdin). Funkcja jest predefiniowana w pliku STDIO.H 
 
i korzystając z funkcji systemu operacyjnego wczytuje dane w  
postaci tekstu z klawiatury konsoli. Interpretacja pobranych  
przez funkcję scanf znaków nastąpi zgodnie z życzeniem  
programisty określonym przez zadany funkcji format (%f, %d, %c  
itp.). Wywołanie funkcji scanf ma postać:  
 
scanf(Format, Adres_zmiennej1, Adres_zmiennej2...);  
 
dla przykładu  
 
scanf("%f%f%f", &X1, &X2, &X3);  
 
wczytuje trzy liczby zmiennoprzecinkowe X1, X2 i X3. 
 
Format decyduje, czy pobrane znaki zostaną zinterpretowane np.  
jako liczba całkowita, znak, łańcuch znaków (napis), czy też w  
inny sposób. Od sposobu interpretacji zależy i rozmieszczenie  
ich w pamięci i późniejsze "sięgnięcie do nich", czyli odwołanie 
 
do danych umieszczonych w pamięci operacyjnej komputera.  
 
Zwróć uwagę, że podając nazwy (identyfikatory) zmiennych należy  
poprzedzić je w funkcji scanf() operatorem adresowym [&].  
Zapis:  
 
int X; 
... 
scanf("%d", &X);  
 
oznacza, że zostaną wykonane następujące działania:  
* Kompilator zarezerwuje 2 bajty pomięci w obszarze pamięci  
danych programu na zmienną X typu int;  
* W momencie wywołania funkcji scanf funkcji tej zostanie  
przekazany adres pamięci pod którym ma zostać umieszczona  
zmienna X, czyli tzw. WSKAZANIE DO ZMIENNEJ;  
* Znaki pobrane z klawiatury przez funkcję scanf mają zostać  
przekształcone do postaci wynikającej z wybranego formatu %d -  
tzn. do postaci zajmującej dwa bajty liczby całkowitej ze  
znakiem.  
 

A JEŚLI PODAM INNY FORMAT ?  

________________________________________________________________ 
C++ wykona Twoje rozkazy najlepiej jak umie, niestety nie  
sprawdzając po drodze formatów, a z zer i jedynek zapisanych w  
pamięci RAM żaden format nie wynika. Otrzymasz błędne dane. 
________________________________________________________________ 
 
Poniżej przykład skutków błędnego formatowania. Dołącz pliki  
STDIO.H i CONIO.H. 

94

background image

 
[P021.CPP]  
//UWAGA: Dołącz właściwe pliki nagłówkowe !  
 
void main()  
{  
  float A, B;  
  clrscr();  
  scanf("%f %f", &A, &B);  
  printf("\n%f\t%d", A,B);  
  getch();  
}  
 
[Z]  
________________________________________________________________ 
3 Zmień w programie przykładowym, w funkcji printf() wzorce  
formatu na %s, %c, itp. Porównaj wyniki. 
________________________________________________________________ 
 
Adres w pamięci to taka sama liczba, jak wszystkie inne i wobec  
tego można nią manipulować. Adresami rządzą jednak dość  
specyficzne prawa, dlatego też w języku C++ występuje jeszcze  
jeden specjalny typ zmiennych - tzw. ZMIENNE WSKAZUJĄCE (ang.  
pointer - wskaźnik). Twoja intuicja podpowiada Ci zapewne, że są 
 
to zmienne całkowite (nie ma przecież komórki pamięci o adresie  
0.245 ani 61/17). Pojęcia "komórka pamięci" a nie np. "bajt"  
używam świadomie, ponieważ obszar zajmowany w pamięci przez  
zmienną może mieć różną długość. Aby komputer wiedział ile  
kolejnych bajtów pamięci zajmuje wskazany obiekt (liczba długa,  
krótka, znak itp.), deklarując wskaźnik trzeba podać na co  
będzie wskazywał. W sposób "nieoficjalny" już w funkcji scanf  
korzystaliśmy z tego mechanizmu. Jest to zjawisko specyficzne  
dla języka C++, więc zajmijmy się nim trochę dokładniej.  
 

POJĘCIE ZMIENNEJ WSKAZUJĄCEJ I ZMIENNEJ WSKAZYWANEJ.  

 
Wskaźnik to zmienna, która zawiera adres innej zmiennej w  
pamięci komputera. Istnienie wskaźników umożliwia pośrednie  
odwoływanie się do wskazywanego obiektu (liczby, znaku, łańcucha 
 
znaków itp.) a także stosunkowo proste odwołanie się do obiektów 
 
sąsiadujących z nim w pamięci. Załóżmy, że:  
 
 x - jest umieszczoną gdzieś w pamięci komputera zmienną  
całkowitą typu int zajmującą dwa kolejne bajty pamięci, a  
 
 px - jest wskaźnikiem do zmiennej x.  
 
Jednoargumentowy operator & podaje adres obiektu, a zatem  
instrukcja:  
 
px = &x;  
 
przypisuje wskaźnikowi px adres zmiennej x. Mówimy, że:  
 
 px wskazuje na zmienną x lub  
 px jest WSKAŹNIKIEM (pointerem) do zmiennej x.  

95

background image

 
Jednoargumentowy operator * (naz. OPERATOREM WYŁUSKANIA)  
powoduje, że zmienna "potraktowana" tym operatorem jest  
traktowana jako adres pewnego obiektu. Zatem, jeśli przyjmiemy,  
że y jest zmienną typu int, to działania:  
 
y = x;  
 
oraz 
 
px = &x; 
y = *px;  
 
będą mieć identyczny skutek. Zapis y = x oznacza:  
"Nadaj zmiennej y dotychczasową wartość zmiennej x";  
a zapis y=*px oznacza:  
"Nadaj zmiennej y dotychczasową wartość zmiennej, której adres w 
 
pamięci wskazuje wskaźnik px" (czyli właśnie x !).  
 
Wskaźniki także wymagają deklaracji. Poprawna deklaracja w  
opisanym powyżej przypadku powinna wyglądać tak:  
 
int x,y; 
int *px;  
main()  
......  
 
Zapis int *px; oznacza:  
"px jest wskaźnikiem i będzie wskazywać na liczby typu int".  
 
Wskaźniki do zmiennych mogą zamiast zmiennych pojawiać się w  
wyrażeniach po PRAWEJ STRONIE, np. zapisy: 
 
int X,Y; 
int *pX; 
... 
pX = &X; 
....... 
Y = *pX + 1;  €€€€€€/* to samo, co  Y = X + 1 */  
printf("%d", *pX);€€€€€€€/* to samo, co printf("%d", X); */  
Y = sqrt(*pX);€€€€€€€€€€€/* pierwiastek kwadrat. z X */  
...... 
 
są w języku C++ poprawne.  
 
Zwróć uwagę, że operatory & i * mają wyższy priorytet niż  
operatory arytmetyczne, dzięki czemu  
* najpierw następuje pobranie spod wskazanego przez  
wskaźnik adresu zmiennej;  
* potem następuje wykonanie operacji arytmetycznej;  
(operacja nie jest więc wykonywana na wskaźniku, a na  
wskazywanej zmiennej!). 
 
W języku C++ możliwa jest także sytuacja odwrotna:  
 
Y = *(pX + 1);  
 
Ponieważ operator () ma wyższy priorytet niż * , więc:  
 

96

background image

 najpierw wskaźnik zostaje zwiększony o 1;  
 potem zostaje pobrana z pamięci wartość znajdująca się pod  
wskazanym adresem (w tym momencie nie jest to już adres zmiennej 
 
X, a obiektu "następnego" w pamięci) i przypisana zmiennej Y.  
 
Taki sposób poruszania się po pamięci jest szczególnie wygodny,  
jeśli pod kolejnymi adresami pamięci rozmieścimy np. kolejne  
wyrazy z tablicy, czy kolejne znaki tekstu.  
 
Przyjrzyjmy się wyrażeniom, w których wskaźnik występuje po  
LEWEJ STRONIE. Zapisy:  
 
*pX = 0;€€€€€€€€€€€€i€€€€€€€€€X = 0; 
*pX += 1;€€€€€€€€€€€i€€€€€€€€€X += 1;  
(*pX)++;€€€€€€€€€€€€i€€€€€€€€€X++;     /*3*/ 
 
mają identyczne działanie. Zwróć uwagę w przykładzie /*3*/, że  
ze względu na priorytet operatorów  
 
() - najwyższy - najpierw pobieramy wskazaną zmienną; 
++ - niższy, potem zwiększmy wskazaną zmienną o 1;  
 
Gdyby zapis miał postać:  
 
*pX++;  
 
najpierw nastąpiłoby  
- zwiększenie wskaźnika o 1 i wskazanie "sąsiedniej" zmiennej,  
potem  
- wyłuskanie, czyli pobranie z pamięci zmiennej wskazanej przez  
nowy, zwiększony wskaźnik, zawartość pamięci natomiast, tj.   
wszystkie zmienne rozmieszczone w pamięci pozostałyby bez zmian. 
 
 

JAK TO WŁAŚCIWIE JEST Z TYM PRIORYTETEM ?  

________________________________________________________________ 
Wszystkie operatory jednoargumentowe (kategoria 2, patrz Tabela) 
 
mają taki sam priorytet, ale są PRAWOSTRONNIE ŁĄCZNE {L<<-R}.  
Oznacza to, że operacje będą wykonywane Z PRAWA NA LEWO. W  
wyrażeniu *pX++; oznacza to:  
 
najpierw ++  
potem *  
 
Zwróć uwagę, że kolejność {L<<-R} dotyczy WSZYSTKICH operatorów  
jednoargumentowych.  
________________________________________________________________ 
 
 
Jeśli dwa wskaźniki wskazują zmienne takiego samego typu, np. po 
 
zadeklarowaniu:  
 
int *pX, *pY;  
int X, Y; 
 
i zainicjowaniu: 

97

background image

 
pX = &X; pY = &Y;  
 
można zastosować operator przypisania:  
 
pY = pX;  
 
Spowoduje to skopiowanie wartości (adresu) wskaźnika pX do pY,  
dzięki czemu od tego momentu wskaźnik pY zacznie wskazywać  
zmienną X. Zwróć uwagę, że nie oznacza to bynajmniej zmiany  
wartości zmiennych - ani wielkośc X, ani wielkość Y, ani ich  
adresy w pamięci NIE ULEGAJĄ ZMIANIE. Zatem działanie  
instrukcji:  
 
pY = pX;          i         *pY = *pX; 
  
jest RÓŻNE a wynika to znowu z priorytetu operatorów:  
 
 najpierw * wyłuskanie zmiennych spod podanych adresów,  
 potem = przypisanie wartości (ale już zmiennym a nie  
wskaźnikom!)  
 
C++ chętnie korzysta ze wskazania adresu przy przekazywaniu  
danych - parametrów do/od funkcji.  
 
Asekurując się na całej linii i podkreślając, że nie zawsze  
wygląda to tak prosto i ładnie, posłużę się do zademonstrowania  
działania wskaźników przykładowym programem. Wpisz i uruchom  
następujący program:  
 

[P022-1.CPP wersja 1]  

 
# include "stdio.h"  
# include "conio.h"  
 
int a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,x=9,y=10,i;  
int *ptr1;  
long int *ptr2;  
 
void main()  
{  
   clrscr();  
    ptr1=&a;  
    ptr2=&a;  
    printf("Skok o 2Bajty   Skok o 4Bajty");  
  
    for(i=0; i<=9; i++)  
     {  
        printf("\n%d", *(ptr1+i));  
        printf("\t\t%d",  *(ptr2+i));  
     } 
    getch();  

 

[P022-2.CPP wersja 2]  

 
int a=11,b=22,c=33,d=44,e=55,f=66,g=77,h=88,x=99,y=10,i;  

98

background image

int *ptr1;  
long int *ptr2;  
 
void main()  
{  
  clrscr();  
  ptr1=&a;  
  ptr2=&a;  
    for (i=0; i<=9; i++)  
     { 
        printf("\n%d", *(ptr1+i));  
        printf("\t%d",  *(ptr2+i));  
        getch(); 
     }  

 
W programie wykonywane są następujące czynności:  
 
1. Deklarujemy zmienne całkowite int (każda powinna zająć 2  
bajty pamięci) i nadajemy im wartości w taki sposób aby łatwo  
można je było rozpoznać.  
2. Deklarujemy dwa wskaźnki:  
ptr1 - poprawny - do dwubajtowych zmiennych typu int;  
ptr2 - niepoprawny - do czterobajtowych zmiennych typu long int. 
 
3. Ustawiamy oba wskaźniki tak by wskazywały adres w pamięci  
pierwszej liczby a=11.  
4. Zwiększamy oba wskaźniki i sprawdzamy, co wskazują.  
 
Jeśli kompilator rozmieści nasze zmienne w kolejnych komórkach  
pamięci, to powinniśmy uzyskać następujący wydruk:  
 
Skok o 2B   Skok o 4B  
11€€€€€€€€€€€€€11  
22€€€€€€€€€€€€€33  
33€€€€€€€€€€€€€55  
44€€€€€€€€€€€€€77  
55€€€€€€€€€€€€€99  
66€€€€€€€€€€€€€27475  
77€€€€€€€€€€€€€28448  
88€€€€€€€€€€€€€8258  
99€€€€€€€€€€€€€27475  
10€€€€€€€€€€€€€2844  
 
Zwróć uwagę, że to deklaracja wskaźnika decyduje, co praktycznie 
 
oznacza operacja *(ptr + 1). W pierwszym przypadku wskaźnik  
powiększa się o 2 a w drugim o 4 bajty. Te odpowiednio 2 i 4  
bajty stanowią długość komórki pamięci lub precyzyjniej, pola  
pamięci przeznaczonego dla zmiennych określonego typu. 
Wartości pojawiające się w drugiej kolumnie po 99 są  
przypadkowe i u Ciebie mogą okazać się inne.  
 
C++ pozwala wskaźnikom nie tylko wskazywać adres zmiennej w  
pamięci. Wskaźnik może również wskazywać na inny wskaźnik. Takie 
 
wskazania:  
 
int X; €€€int pX; €€int ppX; 
pX = &X;€€ppX = &pX;  

99

background image

oznaczamy:  
*pX€€- pX wskazuje BEZPOŚREDNIO zmienną X;  
**ppX€- ppX skazuje POŚREDNIO zmienną X (jest wskaźnikiem do  
wskaźnika). 
***pppX - pppX wskazuje pośrednio wskaźnik do zmiennej X itd. 
 

Zadania

________________________________________________________________ 
4 Wybierz dowolne dwa przykładowe programy omawiane wcześniej i  
przeredaguj je posługując się zamiast zmiennych - wskaźnikami do 
 
tych zmiennych. Pamiętaj, że przed użyciem wskaźnika należy:  
* zadeklarować na jaki typ zmiennych wskazuje wskaźnik;  
* przyporządkować wskaźnik określonej zmiennej.  
 
5 Zastanów się, co oznacza ostrzeżenie wypisywane podczas  
uruchomienia programu przykładowego:  
Warning 8: Suspicious pointer conversion in function main.  
________________________________________________________________ 
 

LEKCJA 12. Wskaźniki i tablice w C i C++. 

________________________________________________________________ 
 
W czasie tej lekcji:  
1. Dowiesz się więcej o zastosowaniu wskaźników.  
2. Zrozumiesz, co mają wspólnego wskaźniki i tablice w języku  
C/C++. 
________________________________________________________________ 
 
 

WSKAŹNIKI I TABLICE W C i C++.  

 
W języku C/C++ pomiędzy wskaźnikami a tablicami istnieje bardzo  
ścisły związek. Do ponumerowania elementów w tablicy służą tzw.  
INDEKSY. W języku C/C++ 
  
* KAŻDA OPERACJA korzystająca z indeksów może zostać wykonana  
przy pomocy wskaźników;  
* posługiwanie się wskaźnikiem zamiast indeksu na ogół  
przyspiesza operację.  
 
Tablice, podobnie jak zmienne i funkcje wymagają przed użyciem  
DEKLARACJI. Upraszczając problem - komputer musi wiedzieć ile  
miejsca zarezerwować w pamięci i w jaki sposób rozmieścić  
kolejne OBIEKTY, czyli kolejne elementy tablicy.  
 

CO Z TYMI OBIEKTAMI ?  

________________________________________________________________ 
OBIEKTEM w szerokim znaczeniu tego słowa jest każda liczba,  
znak, łańcuch znaków itp.. Takimi klasycznymi obiektami języki  
programowania operowały już od dawien dawna. Prawdziwe  

100

background image

programowanie obiektowe w dzisiejszym, węższym znaczeniu  
rozpoczyna się jednak tam, gdzie obiektem może stać się także  
coś "nietypowego" - np. rysunek. Jest to jednak właściwy chyba  
moment, by zwrócić Ci uwagę, że z punktu widzenia komputera  
obiekt to coś, co zajmuje pewien obszar pamięci i z czym wiadomo 
jak postępować.  
________________________________________________________________ 
 
Deklaracja:  
 
int A[12];  
oznacza:  
należy zarezerwować 12 KOLEJNYCH komórek pamięci dla 12 liczb  
całkowitych typu int (po 2 bajty każda). Jednowymiarowa tablica  
(wektor) będzie się nazywać "A", a jej kolejne elementy zostaną  
ponumerowane przy pomocy indeksu:  
- zwróć uwagę, że w C zaczynamy liczyć OD ZERA A NIE OD JEDYNKI; 
 
 
A[0], A[1], A[2], A[3], .... A[11].  
 
Jeśli chcemy zadeklarować:  
- indeks i;  
- wskaźnik, wskazujący nam początek (pierwszy, czyli zerowy  
element) tablicy;  
- samą tablicę;  
to takie deklaracje powinny wyglądać następująco:  
 
int i;  
int *pA; 
int A[12];  
 
Aby wskaźnik wskazywał na początek tablicy A[12], musimy go  
jeszcze zainicjować:  
 
pA = &A[0];  
 
Jeśli poszczególne elementy tablicy są zawsze rozmieszczane  
KOLEJNO, to:  
 
*pA[0] 
 
oznacza:  
"wyłuskaj zawartość komórki pamięci wskazanej przez wskaźnik",  
czyli inaczej - pobierz z pamięci pierwszy (zerowy!) element  
tablicy A[]. Jeśli deklaracja typów elementów tablicy i  
deklaracja typu wskaźnika są zgodne i poprawne, nie musimy się  
dalej martwić ile bajtów zajmuje dany obiekt - element tablicy.  
Zapisy:  
 
*pA[0];€€€€€€€€*pA;€€€€€€€€€€€A[0]  
*(pA[0]+1)€€€€€*(pA+1)€€€€€€€€A[1]  
*(pA[0]+2)€€€€€*(pA+2)€€€€€€€€A[2]€€€€€€itd.  
 
są równoważne i oznaczają kolejne wyrazy tablicy A[].  
 
Jeśli tablica jest dwu- lub trójwymiarowa, początek tablicy  
oznacza zapis:  
 
A[0][0];  

101

background image

A[0][0][0];  
itd. 
 
Zwróć uwagę, że wskaźnik do tablicy *pA oznacza praktycznie  
wskaźnik do POCZĄTKOWEGO ELEMENTU TABLICY:  
 
*pA == *pA[0]  
 
To samo można zapisać w języku C++ w jeszcze inny sposób. Jeśli  
A jest nazwą tablicy, to zapis:  
 
*A  
 
oznacza wskazanie do początku tablicy A, a zapisy:  
 
*(A+1)€€€€€€€€€€€€*(pA+1)€€€€€€€€A[1]  
*(A+8)€€€€€€€€€€€€*(pA+8)€€€€€€€€A[8] itd.  
 
są równoważne. Podobnie identyczne znaczenie mają zapisy:  
 
x = &A[i]€€€€€€€€x=A+i  
*pA[i]€€€€€€€€€*(A+i)  
 
Należy jednak podkreślić, że pomiędzy nazwami tablic (w naszym  
przykładzie A) a wskaźnikami istnieje zasadnicza różnica.  
Wskaźnik jest ZMIENNĄ, zatem operacje:  
 
pA = A;  
pA++;  
 
są dopuszczalne i sensowne. Nazwa tablicy natomiast jest STAŁĄ,  
zatem operacje:  
 
A = pA;€€€€€€€€€€€€€ŹLE !  
A++;€€€€€€€€€€€€€€€€ŹLE !  
 
są niedopuszczalne i próba ich wykonania spowoduje błędy !  
 

DEKLAROWANIE I INICJOWANIE TABLIC.  

 
Elementom tablicy, podobnie jak zmiennym możemy nadawać  
watrości. Wartości takie należy podawać w nawiasach klamrowych,  
a wielkość tablicy - w nawiasach kwadratowych.  
 
Przykład  
 
int WEKTOR[5];  
Tablica WEKTOR jest jednowymiarowa i składa się z 5 elementów  
typu int: WEKTOR[0]....WEKTOR[4].  
 
Przykład  
 
float Array[10][5];  
Tablica Array jest dwuwymiarowa i składa się z 50 elementów typu 
 
float: Array[0][0], Array[0][1]......Array[0][4]  
€€€€€€€Array[1][0], Array[1][1]......Array[1][4]  
€€€€€...........................................  
€€€€€  Array[9][0], Array[9][1]......Array[9][4]  

102

background image

 
Przykład  
 
const int b[4]={1,2,33,444};  
Elementom jednowymiarowej tablicy (wektora) b przypisano  
wartośći: b[0]=1; b[1]=2; b[2]=33; b[3]=444;  
 
Przykład  
 
int TAB[2][3]={{1, 2, 3},{2, 4, 6}};  
€€€€€TAB[0][0]=1€€€€TAB[0][1]=2€€€€TAB[0][2]=3  
€€€€€TAB[1][0]=2€€€€TAB[1][1]=4€€€€TAB[1][2]=6  
 
Przykład : Tablica znakowa. Obie formy zapisu dają ten sam  
efekt. 
 
char hej[5]="Ahoj"; 
char hej[5]={'A', 'h', 'o', 'j'};  
€€€€€hej[0]='A'€€€€€hej[1]='h'€€€€€hej[2]='o'  itp.  
 
Przykład : Tablica uzupełniona zerami przez domniemanie.  
 
float T[2][3]={{1, 2.22}, {.5}};  
kompilator uzupełni zerami do postaci: 
 
€€€€€T[0][0]=1€€€€€€T[0][1]=2.22€€€€€€€€T[0][2]=0  
€€€€€T[1][0]=0.5€€€€T[1][1]=0€€€€€€€€€€€T[1][2]=0 
 
Jeśli nawias kwadratowy zawierający wymiar pozostawimy pusty, to 
 
kompilator obliczy jego domniemaną zawartość w oparciu o podaną  
zawartość tablicy. Nie spowoduje więc błędu zapis:  
 
char D[]="Jakis napis"  
int A[][2]={{1,2}, {3,4}, {5,6}}  
 
Jeśli nie podamy ani wymiaru, ani zawartości:  
 
int A[];  
 
kompilator "zbuntuje się" i wykaże błąd. 
 
Dla przykładu, skompiluj program przykładowy. Zwróć uwagę na  
sposób zainicjowania wskaźnika.  
  

[P023.CPP]  

  
# include "stdio.h"   
# include <conio.h>   
int a[][2]={ {1,2},{3,4},{5,6},{7,8},{9,10},{11,12} };   
char b[]={ "Poniedzialek" };   
int i;   
int *pa;   
char *pb;  
 
void main()  
{   
  pa = &a[0][0];  
  pb = b;              // lub pb = b[0];  

103

background image

  clrscr();   
  for (i=0; i<12; i++)   
      printf("%d\t%c\n", *(pa+i), *(pb+i));   
  getch();   
}   
   
Zwróć uwagę, że w C++ każdy wymiar tablicy musi mieć swoją parę  
nawiasów kwadratowych. Dla przykładu, tablicę trójwymiarową  
należy deklarować nie tak TAB3D[i, j, k] lecz tak:  
 
int i, j, k;  
 ... 
TAB3D[i][j][k];  
 
Jest w dobrym stylu panować nad swoimi danymi i umieszczać je w  
tzw. BUFORACH, czyli w wydzielonych obszarach pamięci o znanym  
adresie, wielkości i przeznaczeniu. W następnym programie  
przykładowym utworzymy taki bufor w postaci tablicy bufor[20] i  
zastosujemy zamiast funkcji scanf() czytającej bezpośrednio z  
klawiatury parę funkcji:  
 
gets() - GET String - pobierz łańcuch znaków z klawiatury do  
bufora;  
sscanf(bufor) - odczytaj z bufora (z pamięci). 
 
Aby uniknąć nielubianego goto stosujemy konstrukcję for - break. 
 
Dokładniej pętlę for omówimy w trakcie następnej lekcji.  
Ponieważ mam nadzieję, że "podstawową" postać pętli for  
pamiętasz z przykładów LOOP-n: 
 
for(i=1; i<100; i++) 
{  
 ...  
}  
 
pozwalam sobie trochę wyprzedzająco zastosować ją w programie. 
Niepodobny do Pascala ani do Basica zapis wynika właśnie z tego, 
 
że skok następuje bezwarunkowo. Nagłówek pętli for 
 
* nie inicjuje licznika pętli (zbędne typowe i=1); 
* nie sprawdza żadnego warunku (zbędne i<100),  
* nie liczy pęti (i=i+1 lub i++ też zbędne !). 
 

[P024.CPP] 

 
# include <stdio.h>  
# include <conio.h> 
 
int liczba, ile = 0, suma = 0; 
 
void main()  
{  
char bufor[20];  
  clrscr(); 
  printf("podaj liczby - ja oblicze  SREDNIA i SUMA\n"); 
  printf("ZERO = KONIEC\n");  
 

104

background image

   for(;;)    // Wykonuj petle BEZWARUNKOWO 
    { 
       gets(bufor);  
       sscanf(bufor, "%d", &liczba);  
       suma += liczba;  
       ile++;  
       if (liczba == 0) break;   // JESLI ==0 PRZERWIJ PETLE 
     } 
  printf("Suma wynosi:  %d\n", suma);  
  printf("Srednia wynosi:  %d\n", (suma / ile));  
  getch(); 
}  
 
Poniżej trochę bardziej "elegancka wersja" z zastosowaniem pętli 
 
typu while. Więcej o pętlach dowiesz się z następnej Lekcji.  
 

[P025.CPP]  

  
# include <stdio.h>   
# include <conio.h>  
 
int liczba, ile=1, suma=0;  
 
void main()   
{   
char bufor[20];   
 clrscr();  
 printf("podaj liczby - ja oblicze  SREDNIA i SUMA\n");  
 printf("ZERO = KONIEC\n");   
  
 gets(bufor);   
 sscanf(bufor, "%d", &liczba);   
  
 while (liczba != 0)   
  {   
      suma += liczba;   
      gets(bufor);   
      sscanf(bufor, "%d", &liczba);   
      if(liczba == 0)  
         printf("I to by bylo na tyle...\n");  
      else  
         ile++;   
  }   
 printf("Suma wynosi:  %d\n", suma);   
 printf("Srednia wynosi:  %d\n", suma / ile);   
 getch(); 
}   
 
Program powyższy, choć operuje tablicą, robi to trochę jakby za  
kulisami. Utwórzmy zatem inną - bardziej "dydaktyczną" tablicę,  
której elementy byłyby łatwo rozpoznawalne.  
 

PRZYKŁADY TABLIC WIELOWYMIAROWYCH.  

 
Dzięki matematyce bardziej jesteśmy przyzwyczajeni do zapisu  
tablic w takiej postaci:  

105

background image

 
€€€€€€€€€€a11€€a12€€a13€€a14€€a15€€a16  
€€€€€€€€€€a21€€a22€€a23€€a24€€a25€€a26  
€€€€€€€€€€a31€€a32€€a33€€a34€€a35€€a36  
€€€€€€€€€€a41€€a42€€a43€€a44€€a45€€a46  
 
gdzie a i,j /** indeks**/ oznacza element tablicy zlokalizowany  
w:  
- wierszu i  
- kolumnie j  
Przypiszmy kolejnym elementom tablicy następujące wartości:  
 
€€€€€€€€€€11€€€12€€€13€€€14€€€15€€€16  
€€€€€€€€€€21€€€22€€€23€€€24€€€25€€€26  
€€€€€€€€€€31€€€32€€€33€€€34€€€35€€€36  
€€€€€€€€€€41€€€42€€€43€€€44€€€45€€€46  
 
Jest to tablica dwuwymiarowa o wymiarach 4WIERSZE X 6KOLUMN,  
czyli krócej 4X6. Liczby będące elementami tablicy są typu  
całkowitego. Jeśli zatem nazwiemy ją TABLICA, to zgodnie z  
zasadami przyjętymi w języku C/C++ możemy ją zadeklarować:  
 
int TABLICA[4][6];  
 
Pamiętajmy, że C++ liczy nie od jedynki a od zera, zatem  
 
TABLICA[0][0] = a11 = 11,  
TABLICA[2][3] = a34 = 34 itd.  
 
Znając zawartość tablicy możemy ją zdefiniować/zainicjować:  
 
int TABLICA[4][6]={{11,12,13,14,15,16},{21,22,23,24,25,26} 
{31,32,33,34,35,36},{41,42,43,44,45,46}};  
 
Taki sposób inicjowania tablicy, aczkolwiek pomaga wyjaśnić  
metodę, z punktu widzenia programistów jest trochę  
"nieelegancki". Liczbę przypisywaną danemu elementowi tablicy  
można łatwo obliczyć.  
 
TABLICA[i][j] = (i+1)*10 + (j+1);  
 
Przykładowo:  
 
TABLICA[2][5] = (2+1)*10 +(5+1) = 36  
 
Najbardziej oczywistym rozwiązaniem byłoby napisanie pętli  
 
int i, j; 
for (i=0; i<=3; i++)  
€€€€€{ for (j=0; j<=5; j++)  
€€€€€€€€€€{ TABLICA[i][j] = (i+1)*10 + (j+1);}  
€€€€€}  
 
Spróbujmy prześledzić rozmieszczenie elementów tablicy w pamięci 
 
i odwołać się do tablicy na kilka sposobów.  
 

[P026.CPP]  

 

106

background image

int TABLICA[4][6]={{11,12,13,14,15,16},{21,22,23,24,25,26},  
{31,32,33,34,35,36},{41,42,43,44,45,46}};  
# include <stdio.h>  
# include <conio.h>  
int *pT;  
int i, j;  
void main()  
{  
clrscr();  
printf("OTO NASZA TABLICA \n");  
   for (i=0; i<=3; i++)  
       {  
       for (j=0; j<=5; j++)  
         printf("%d\t", TABLICA[i][j]);  
       printf("\n");  
       }  
printf("\n\Inicjujemy wskaźnik na poczatek tablicy\n");  
printf("i INKREMENTUJEMY wskaźnik *pT++ \n");  
 
pT=&TABLICA[0][0];  
for (i=0; i<4*6; i++)  
  printf("%d  ", *(pT+i));  
  
getch();  

 
Zwróć uwagę, że jeśli tablica ma wymiary A * B (np. 3 * 4) i  
składa się z k=A*B elementów, to w C++ zakres indeksów wynosi  
zawsze 0, 1, 2, .....A*B-2, A*B-1. Tak więc tablica 10 x 10  
(stuelementowa) będzie składać się z elementów o numerach  
0...99, a nie 1...100. 
 

[P027.CPP]  

 
# include <stdio.h>  
# include <conio.h>  
int TABLICA[4][6]; 
int *pT;  
int i, j;  
 
void main()  
{  
clrscr();  
printf("Inicjujemy tablice\n");  
for (i=0; i<4; i++)  
€€€€€for (j=0; j<6; j++)  
€€€€€{ TABLICA[i][j] = (i+1)*10 + (j+1); } // INDEKS! 
  printf("OTO NASZA TABLICA \n");  
        for (i=0; i<=3; i++)  
          {  
            for (j=0; j<=5; j++)  
            printf("%d\t", TABLICA[i][j]);  
          printf("\n");  
          }  
printf("\n\Inicjujemy wskaźnik na poczatek tablicy\n");  
printf("i INKREMENTUJEMY wskaźnik *pT++ \n");  
pT=&TABLICA[0][0];  
  for (i=0; i<4*6; i++)  
  printf("%d  ", *(pT+i));  

107

background image

  
getch();  

 

RĘCZNE I AUTOMATYCZNE GENEROWANIE TABLIC WIELOWYMIAROWYCH. 

 
Aby nabrać wprawy, spróbujmy pomanipulować inną tablicą, znaną  
Ci prawie "od urodzenia" - tabliczką mnożenia. Jest to  
kwadratowa tablica 10 x 10, której każdy wyraz opisuje się  
prostą zależnością T(i,j)=i*j. Jeśli przypomnimy sobie, że  
indeksy w C++ zaczną się nie od jedynki a od zera, zapis ten  
przybierze następującą formę:  
 
int T[10][10];  
T[i][j] = (i+1)*(j+1);  
 
Do pełni szczęścia brak jeszcze wskaźnika do tablicy:  
 
int *pT;  
 
i jego zainicjowania  
 
pT = &T[0][0];  
 
I już możemy zaczynać. Moglibyśmy oczywiście zainicjować tablicę 
 
"na piechotę", ale to i nieeleganckie, i pracochłonne, i o  
pomyłkę łatwiej. Pamiętaj, że komputer myli się rzadziej niż  
programista, więc zawsze lepiej jemu zostawić możliwie jak  
najwięcej roboty.  
 

[P028.CPP] 

 
# include <stdio.h>  
# include <conio.h>  
int T[10][10];  
int *pT;  
int i, j, k;  
char spacja = ' '; 
void main()  
{  
 clrscr();  
 printf("\t TABLICZKA MNOZENIA (ineksy)\n");  
 for (i=0; i<10; i++)  
  {  
  for (j=0; j<10; j++)  
    { T[i][j] = (i+1)*(j+1);  
      if (T[i][j]<10)  
           printf("%d%c  ", T[i][j], spacja);  
      else  
           printf("%d  ", T[i][j]);  
    }  
  printf("\n");  
  }  
 printf("\n Inicjujemy i INKREMENTUJEMY wskaźnik *pT++ \n\n");  
 pT=&T[0][0];  
 for (k=0; k<10*10; k++)    

108

background image

  {  
    if (*(pT+k) < 10)  
      printf("%d%c  ", *(pT+k) , spacja);  
    else  
      printf("%d  ", *(pT+k));  
         if ((k+1)%10 == 0) printf("\n");  
  }  
getch();  

 
Po wynikach jednocyfrowych dodajemy trzy spacje a po  
dwucyfrowych dwie spacje. Po dziesięciu kolejnych wynikach  
trzeba wstawić znak nowego wiersza. Sprawdzamy te warunki:  
 
if (*(pT+k) < 10) - jeśli wynik jest mniejszy niż 10...  
lub if (T[i][j] < 10);  
if ((k+1) % 10 == 0) - jeśli k jest całkowitą wielokrotnością  
10, czyli - jeśli reszta z dzielenia równa się zero...  
 
Zastosowane w powyższych programach nawiasy klamrowe {}  
spełniają rolę INSTRUKCJI GRUPUJĄCEJ i pozwalają podobnie jak  
para BEGIN...END w Pascalu zamknąć w pętli więcej niż jedną  
instrukcję. Instrukcje ujęte w nawiasy klamrowe są traktowane  
jak pojedyncza instrukcja prosta.  
 
Tablice mogą zawierać liczby, ale mogą zawierać także znaki.  
Przykład prostej tablicy znakowej zawiera następny program  
przykładowy.  
 

[P029.CPP]  

 
# include <stdio.h>  
# include <conio.h>  
  
char T[7][12]={"Poniedzialek",  
"Wtorek",  
"Sroda",  
"Czwartek",  
"Piatek",  
"Sobota",  
"Niedziela"};  
  
char *pT;  
int i, j, k;  
char spacja=' ';  
void main()  
{  
clrscr();  
pT =&T[0][0];  
printf("\t TABLICA znakowa (ineksy)\n\n");  
  
for (i=0; i<7; i++)  
  {  
  for (j=0; j<12; j++)  
      printf("%c ", T[i][j] );  
  printf("\n");  
  }  
printf("\n\t Przy pomocy wskaźnika \n\n");  
  

109

background image

for (k=0; k<7*12; k++)    
  {  
    printf("%d ", *(pT+k) );    //TU! - opis w tekście 
      if ((k+1)%12 == 0) printf("\n");  
  }  
getch();  

 
Nazwy dni mają różną długość, czym więc wypełniane są puste  
miejsca w tablicy? Jeśli w miejscu zaznaczonym komentarzem //TU! 
 
zmienisz format z 
 
printf("%c ", *(pT+k) );   na   printf("%d ", *(pT+k) );  
 
uzyskasz zamiast znaków kody ASCII. 
 
 TABLICA znakowa (ineksy)  
  
P o n i e d z i a l e k   
W t o r e k         
S r o d a          
C z w a r t e k       
P i a t e k         
S o b o t a         
N i e d z i e l a      
  
 Przy pomocy wskaźnika:   
  
80 111 110 105 101 100 122 105 97 108 101 107   
87 116 111 114 101 107 0 0 0 0 0 0   
83 114 111 100 97 0 0 0 0 0 0 0   
67 122 119 97 114 116 101 107 0 0 0 0   
80 105 97 116 101 107 0 0 0 0 0 0   
83 111 98 111 116 97 0 0 0 0 0 0   
78 105 101 100 122 105 101 108 97 0 0 0   
  
 
Okaże się, że puste miejsca zostały wypełnione zerami. Zero w  
kodzie ASCII - NUL - '\0' jest znakiem niewidocznym, nie było  
więc widoczne na wydruku w formie znakowej printf("%c"...). 
 

Zadania

________________________________________________________________ 
1. Posługując się wskaźnikiem i inkrementując wskaźnik z różnym  
krokiem - np. pT += 2; pT += 3 itp., zmodyfikuj programy  
przykładowe tak, by uzyskać wydruk tylko części tablicy.  
2. Spróbuj zastąpić inkrementację wskaźnika pT++ dekrementacją,  
 odwracając tablicę "do góry nogami". Jak należałoby poprawnie  
zainicjować wskaźnik?  
3. Napisz program drukujący tabliczkę mnożenia w układzie  
szesnastkowym - od 1 * 1 do F * F.  
4. Wydrukuj nazwy dni tygodnia pionowo i wspak.  
5. Zinterpretuj następujące zapisy:  
 
int *pt_int;  
float *pt_float;  
int   p = 7, d = 27;  
float x = 1.2345, Y = 32.14;  

110

background image

void *general;  
  
pt_int = &p;  
*pt_int += d;  
  
general = pt_int;  
  
pt_float = &x;  
Y += 5 * (*pt_float);  
  
general = pt_float;  
  
const char *name1 = "Jasio";   // wskaźnik do STALEJ  
char *const name2 = "Grzesio";   // wskaźnik do STALEGO ADRESU  
________________________________________________________________ 
  

LEKCJA 13. Jak tworzyć w programie pętle i rozgałęzienia. 

_______________________________________________________________  
W trakcie tej lekcji:  
1. Dowiesz się znacznie więcej o pętlach.  
2. Przeanalizujemy instrukcje warunkowe i formułowanie warunków. 
 
_______________________________________________________________  
 
Zaczniemy tę lekcję nietypowo - od słownika, ponieważ dobrze  
jest rozumieć dokładnie co się pisze. Tym razem słownik jest  
trochę obszerniejszy. Pozwalam sobie przytoczyć niektóre słowa  
powtórnie - dla przypomnienia i Twojej wygody. Do organizacji  
pętli będą nam potrzebne następujące słowa:  
 

wyrażenia warunkowe - conditional expressions

pętle strukturalne - structural loops

________________________________________________________________ 
if - jeżeli (poprzedza warunek do sprawdzenia);  
else - a jeśli nie, to (w przeciwnym wypadku...);  
for - dla;  
while - dopóki (dopóki nie spełnimy warunku);  
do - wykonaj, wykonuj;  
break - przerwij (wykonanie pętli);  
switch - przełącz;  
case - przypadek, wariant (jedna z możliwości);  
goto - idź do... 
default - domyślny, (automatyczny, pozostały);  
continue - kontynuuj (pętlę);  
________________________________________________________________ 
UWAGA: W C/C++ nie stosuje się słowa THEN. 
 

PĘTLA TYPU for.  

 
Ogólna postać pętli for jest następująca:  
 
for (W_inicjujące; W_logiczne; W_kroku) Instrukcja;  
 
gdzie skrót W_ oznacza wyrażenie. Każde z tych wyrażeń może  

111

background image

zostać pominięte (patrz --> for(;;)). 
 
Wykonanie pętli for przebiega następująco: 
 
1. Wykonanie JEDEN raz WYRAŻENIA INICJUJĄCEGO. 
2. Obliczenie wartości LOGICZNEJ wyrażenia logicznego. 
3. Jeśli W_logiczne ma wartość PRAWDA (TRUE) nastąpi wykonanie  
Instrukcji.  
4. Obliczenie wyrażenia kroku. 
5. Powtórne sprawdzenie warunku - czy wyrażenie logiczne ma  
wartość różną od zera. Jeśli wyrażenie logiczne ma wartość zero, 
nastąpi zakończenie pętli.  
 
Warunek jest testowany PRZED wykonaniem instrukcji. Jeśli zatem  
nie zostanie spełniony warunek, instrukcja może nie wykonać się  
ANI RAZ.  
 
Instrukcja może być INSTRUKCJĄ GRUPUJĄCĄ, składającą się z  
instrukcji prostych, deklaracji i definicji zmiennych lokalnych: 
 
 
{ ciąg deklaracji lub definicji;  
  ciąg instrukcji; }  
 
Ogromnie ważny jest fakt, że C++ ocenia wartość logiczną  
wyrażenia według zasady:  
 
0 - FALSE, FAŁSZ, inaczej ZERO LOGICZNE jeśli WYRAŻENIE == 0 lub 
 
jest fałszywe w znaczeniu logicznym; 
1 - TRUE, PRAWDA, JEDYNKA LOGICZNA, jeśli wyrażenie ma DOWOLNĄ  
WARTOŚĆ NUMERYCZNĄ RÓŻNĄ OD ZERA (!) lub jest prawdziwe w sensie 
logicznym. 
 
Przykład:  
 
"Klasycznie" zastosowana pętla for oblicza pierwiastki  
kwadratowe kolejnych liczb całkowitych.  
 
#include <math.h>  
#include <stdio.h> 
 
void main()   

int n;  
for (n=0; n<=100; n++) printf("%f\t", sqrt(n)); 
getch(); 

 
Wyrażenie inicjujące może zostać pominięte. Innymi słowy zmienna 
 
może zostać zainicjowana na zewnątrz pętli, a pętla przejmie ją  
taką jaka jest w danym momencie. Przykładowo: 
.....  
{  
float n;  
n=(2*3)/(3*n*n - 1.234);  
......  
for (; n<=100; n++) printf("%f4.4\t", sqrt(n));  
 

112

background image

Przykład:   
 
Warunek przerwania pętli może mieć także inny charakter. W  
przykładzie pętla zostanie przerwana, jeśli różnica pomiędzy  
kolejnymi pierwiastkami przekroczy 3.0.  
 
void main()  
{  
float y=0, n=0;  
for (; (sqrt(n)-y)<=3.0; n++)  
€€€€€{ y=sqrt(n);  
€€€€€  printf("%f2.3\t", y);  
€€€€€}  
getch();  
}  
 
UWAGA:  
Sprawdź, czy nawias (sqrt(n)-y)<=3 można pominąć? Jaki jest  
priorytet operatorów w wyrażeniach:  
(sqrt(n)-y)<=3.0      i     sqrt(n)-y<=3.0 
Jaki będzie wynik? Dlaczego? 
 
Przykład:  
 
Instrukcja stanowiąca ciało pętli może być instrukcją pustą a  
wszystkie istotne czynności mogą zostać wykonane w ramach samego 
 
"szkieletu" for. Program przykładowy sprawdza ile kolejnych  
liczb całkowitych trzeba zsumować by uzyskać sumę nie mniejszą  
niż tysiąc.  
 
void main()  
{  
float SUMA=0, n=0;  
for (; SUMA < 1000; SUMA+=(++n));  
  printf("%f", n);  
getch(); 
}  
 

CZY NIE MOŻNA JAŚNIEJ ???  

________________________________________________________________ 
Można, ale po nabraniu wprawy takie skróty pozwolą Ci  
przyspieszyć tworzenie programów. Zmniejszenie wielkości pliku  
tekstowego jest w dzisiejszych czasach mniej istotne.  
Rozszyfrujmy zapis SUMA+=(++n). Preinkrementacja następuje PRZED 
 
użyciem zmiennej n, więc: 
1. Najpierw ++n, czyli n=n+1.  
2. Potem SUMA=SUMA+ (n+1).  
Dla wyjaśnienia przedstawiam dwie wersje (obie z pętlą for):  
 
void main() {            void main()  
float SUMA=0;            { float SUMA=0, n=0; 
int n;                   for (; SUMA < 1000; SUMA+=(++n)); } 
clrscr();                  
for (n=0; SUMA<=1000; n++)  
  { 
    SUMA=SUMA+n;  
  }  

113

background image

}   
________________________________________________________________ 
 
To jeszcze nie koniec pokazu elastyczności C/C++. W pętli for  
wolno nam umieścić więcej niż jedno wyrażenie inicjujące i  
więcej niż jedno wyrażenie kroku oddzielając je przecinkami.  
 
flat a, b, c;  
const float d=1.2345; 
void main()  
{  
for (a=5,b=3.14,c=10; c; ++a,b*=d,c--)  
printf("\n%f\t%f\t%f", a,b,c);  
getch();  
}  
 
Zwróć uwagę, że zapisy warunku: 
 
if (c)...;     i     if (c != 0)...;  
 
są w C++ równoważne. 
 
Przykład:   
 
Program będzie pisał kropki aż do naciśnięcia dowolnego  
klawisza, co wykryje funkcja kbhit(), będąca odpowiednikem  
KeyPressed w Pascalu. Zapis !kbhit() oznacza "NIE NACIŚNIĘTO  
KLAWISZA", czyli w buforze klawiatury nie oczekuje znak. Zwróć  
uwagę, że funkcja getch() może oczekiwać na klawisz w  
nieskończoność. Aby uniknąć kłopotliwych sytuacji, czasem  
znacznie wygodniej jest zastosować kbhit(), szczególnie, jeśli  
czekamy na DOWOLNY klawisz. 
 
void main() 
{  
  for (; !kbhit(); printf("."));  

 
Przykład:  
 
Wskaźnik w charakterze zmiennej roboczej w pętli typu for. Pętla 
 
powoduje wypisanie napisu. 
 
char *Ptr = "Jakis napis";  
void main() 
{  
 for (; (*Ptr) ;)  
 printf("%c",*Pt++);  
 getch();  

  
 

AUTOMATYCZNE GENEROWANIE TABLIC W PĘTLI for 

 
Na dyskietce znajdziesz jeszcze kilka przykładów FORxx.CPP  
użycia pętli. A teraz, zanim będziemy kontynuować naukę -  
przykładowy program do zabawy. Pętla for służy do wykrywania  
zgodności klawisza z elementami tablicy TABL[]. W tablicy D[]  

114

background image

umieszczone zostały częstotliwości kolejnych dźwięków, które  
program oblicza sam, wykorzystując przybliżony współczynnik. 
 

[P030.CPP]  

 
# include "conio.h"  
# include "dos.h"  
# include "math.h"  
# include "stdio.h"  
 
char TABL[27]={"zsxdcvgbhnjm,ZSXDCVGBHNJM<"};  
char k;  
float D[26];  
int i;  
  
void main()  
{  
clrscr();  
printf("[A]- KONIEC, dostepne klawisze: \n");  
printf(" ZSXDCVGBHNJM,i [Shift]");  
D[0]=200.0;  
for(i=1; i<26; i++) D[i]=D[i-1]*1.0577;  
for (;;)    //patrz przyklad {*} 
  {  
 k = getch();  
  for(i=0; i<27; i++)  
   { if (k==TABL[i])  
       { sound(D[i]); delay(100); nosound(); }  
   };  
       if (k=='a'|| k=='A') break;     //Wyjście z pętli.  
    k = '0';  
  };  
}  
  
Po uruchomieniu programu klawisze działają w sposób  
przypominający prosty klawiszowy instrument muzyczny. 
 
Automatyczne zainicjowanie tablicy wielowymiarowej możemy  
pozostawić C++. Wielkość tablicy może być znana na etapie  
kompilacji programu, lub określona w ruchu programu. 
 
C++ traktuje stałą (const) jako szczególny przypadek wyrażenia  
stałowartościowego (ang. true constant expression). Jeśli  
zadeklarowaliśmy zmienną wymiar jako stałą, możemy zastosować ją 
 
np. do zwymiarowania tablicy TAB[]. Przykład poniżej przedstawia 
 
takie właśnie zastosowanie stałych w C++.  
 

[P031.CPP]  

/* Inicjowanie tablicy przy pomocy stałej */ 
 
# include <iostream.h>  
  
main()  
{  
  const int wymiar = 7;                   //Deklaracja stałej 

115

background image

  char TAB[wymiar];                       //Deklaracja tablicy 
  
  cout << "\n Wielkosc tablicy TAB[] wynosi: " << sizeof TAB;  
  cout << "  bajtow."; 
  return 0; 
}  
  
Umożliwia to dynamiczne inicjowanie tablic pod warunkiem  
rygorystycznego przestrzegania zasady, że do zainicjowana stałej 
 
możemy zastosować wyłącznie wyrażenie stałowartościowe. . 
 
[S] sizeof - wielkość w bajtach.  
 

DANE PREDEFINIOWANE.  

 
Dla ułatwienia życia programiście producenci kompilatorów C++  
stosują stałe predefiniowane w plikach nagłówkowych, np.:  
 
_stklen - wielkość stosu,  
O_RDONLY - tryb otwarcia pliku "tylko do odczytu",  
GREEN - numer koloru w palecie, itp., itp.  
 
Predefiniowanych stałych możemy używać do deklarowania  
indeksów/rozmiarów tablic. 
 
 
 

PĘTLA TYPU while.  

 
Pętlę typu while stosuje się na ogół "do skutku", tj. do momentu 
 
spełnienia warunku, zwykle wtedy, gdy nie jesteśmy w stanie  
przewidzieć potrzebnej ilości cykli. Konstrukcja pętli while  
wygląda następująco:  
 
while (Wyrażenie_logiczne) Instrukcja;  
 
Jeśli Wyrażenie_logiczne ma wartość różną od zera, to zostanie  
wykonana Instrukcja. Sprawdzenie następuje PRZED wykonaniem  
Instrukcji, toteż Instrukcja może nie zostać wykonana ANI RAZU.  
Instrukcja może być INSTRUKCJĄ GRUPUJĄCĄ.  
 
Przykład  
 
Stosujemy pętlę while do programu piszącego kropki (patrz  
wyżej). 
 
void main() 
{  
while (!kbhit()) printf(".");  
}  
 
Przykład 
 
Stosujemy pętlę while w programie obliczającym sumę.  
 

116

background image

void main(){  
float SUMA=0, n=0;  
clrscr();  
  while (SUMA<1000) SUMA+=(++n);  
    printf("SUMA: %4.0f ostatnia liczba: %3.0f",  
    SUMA, n);  
getch();  

 

[P032.CPP]  

 
char *Pointer1="Koniec napisu to \0, *Pointer==0 ";  
char *Pointer2="Koniec napisu to \0, *Pointer==0 ";  
  
void main(){  
clrscr();  
while (*Pointer1)  
   printf("%c", *Pointer1++);  
printf("\nZobacz ten NUL na koncu lancucha znakow\n");  
while (*Pointer2)  
   printf("%c", *Pointer2++);  
printf("%d", *Pointer2);  
getch();  

 

PĘTLA do...while.  

 
Konstrukcja dwuczłonowa do...while tworzy pętlę, która:  
 
* jest wykonywana zawsze CO NAJMNIEJ JEDEN RAZ, ponieważ warunek 
 
jest sprawdzany nie na wejściu do pętli, a na wyjściu z pętli; 
* przerwanie pętli powodowane jest przez NIESPEŁNIENIE WARUNKU.  
 
Schemat pętli do...while jest następujący:  
 
do Instrukcja while (Wyrażenie_logiczne);  
 
Instrukcja może być instrukcją grupującą.  
 
Przykład:  
 
void main() 
{  
  do  
    {printf(".");}  
  while (!kbhit());  
  printf("Koniec petli....");  

 

INSTRUKCJA WARUNKOWA if, if...else i if...else...if..  

 
Instrukcja warunkowa ma postać:  
 
if (Wyrażenie) Instrukcja;  
if (Wyrażenie) Instrukcja1 else Instrukcja2;  

117

background image

 
Jeśli Wyrażenie ma wartość różną od zera (LOGICZNĄ bądź  
NUMERYCZNĄ !) to zostanie wykonana Instrukcja1, w przeciwnym  
razie wykonana zostanie Instrukcja2. Instrukcje mogą być  
instrukcjami grupującymi. Słowa kluczowe if i else mogą być  
stosowane wielokrotnie. Pozwala to tworzyć np. tzw. drzewa  
binarne. 
 
Przykład:   
 
void main()  
{  
float a;  
scanf("%f", &a);  
if (a<0) printf("Ujemna!");  
€€€€€else if (a==0) printf("Zero!");  
€€€€€€€€€€else printf("Dodatnia!");  
}  
 
Przykład:  
 
if (a>0) if (a<100) printf("Dwucyfrowa"); else printf("100+");  
 
inaczej:  
 
if(a>0) {if(a<100) printf("Dwucyfrowa"); else printf("100+");}  
 
Wyrażenie może zawierać operatory logiczne:  
 
if (a>0 && a<100) printf("Dwucyfrowa"); else printf("100+"); 
 
Zapis 100+ oznacza "sto i więcej". 
 
Przykład:  
 
C++ pozwala na krótszy zapis instrukcji warunkowej: 
 
(a>b)? MAX=a : MAX=b;  
 
inaczej:  
 
if (a>b) MAX=a; else MAX=b;  
 

INSTRUKCJE break i continue.  

 
Instrukcja break powoduje natychmiastowe bezwarunkowe  
opuszczenie pętli dowolnego typu i przejście do najbliższej  
instrukcji po zakończeniu pętli. Jeśli w pętli for opuścimy  
wyrażenie logiczne, to zostanie automatycznie przyjęte 1. Pętla  
będzie zatem wykonywana bezwarunkowo w nieskończoność. W  
przykładzie poniżej nieskończoną pętlę przerywa po podaniu z  
kalwiatury zera instrukcja break.  
 
Przykład: 
 
float a, sigma=0;  
void main(){  
for (;;)  
  {  

118

background image

    printf("\n Podaj liczbe do sumowania\n");  
    scanf("%f", &a);  
      if (a==0) break;  
    sigma+=a;  
    printf("\n SUMA: %f",sigma);  
  }  
printf("Nastapil BREAK");  
getch();  

 
 

Instrukcja continue.  

 
Instrukcja continue powoduje przedwczesne, bezwarunkowe  
zakończenie wykonania wewnętrznej instrukcji pętli i podjęcie  
próby realizacji następnego cyklu pętli. Próby, ponieważ  
najpierw zostanie sprawdzony warunek kontynuacji pętli. Program  
z przykładu poprzedniego zmodyfikujemy w taki sposób, by  
 
* jeśli liczba jest dodatnia - dodawał ją do sumy sigma;  
* jeśli liczba jest ujemna - nie robił nic, pomijał bieżącą  
pętlę przy pomocy rozkazu continue;  
(Ponieważ warunek wejściowy pętli jest zawsze spełniony, to  
pętlę zawsze uda się kontynuować.)  
* jeśli liczba równa się zero - przerywał pętlę instrukcją break 
 
 
Przykład:  
 
float a, sigma=0;  
void main() 
{  
 for (;;)  
  {  
  printf("\n Sumuje tylko liczby dodatnie\n");  
  scanf("%f", &a);  
    if (a<0) continue; 
        if (a==0) break;  
  sigma+=a;  
  printf("\n SUMA: %f",sigma);  
  }  
 printf("Nastapil BREAK");  
 getch();  

 

INSTRUKCJE switch i case.  

 
Instrukcja switch dokonuje WYBORU w zależności od stanu  
wyrażenia przełączającego (selector) jednego z możliwych  
przypadków - wariantów (case). Każdy wariant jest oznaczony przy 
 
pomocy stałej - tzw. ETYKIETY WYBORU. Wyrażenie przełączające  
może przyjmować wartości typu int. Ogólna postać istrukcji jest  
następująca:  
 
switch (selector)  
{  

119

background image

case STAŁA1: Ciąg_instrukcji-wariant 1;  
case STAŁA2: Ciąg_instrukcji-wariant 2;  
...............................  
case STAŁAn: Ciąg_instrukcji-wariant n;  
default    : Ostatni_ciąg_instrukcji;  
}  
 
Należy podkreślić, że po dokonaniu wyboru i skoku do etykiety  
wykonane zostaną również WSZYSTKIE INSTRUKCJE PONIŻEJ DANEJ  
ETYKIETY. Jeśli chcemy tego uniknąć, musimy dodać rozkaz break.  
 

[P033.CPP]  

 
# define pisz printf  //dla przypomnienia 
# include <stdio.h> 
void main()  
{  
int Numer_Dnia;  
  pisz("\nPodaj numer dnia tygodnia\n");  
  scanf("%d", &Numer_Dnia);  
  switch(Numer_Dnia)  
  {  
    case 1: pisz("PONIEDZIALEK.");  
    case 2: pisz("WTOREK");  
    case 3: pisz("SRODA.");  
    case 4: pisz("CZWARTEK.");  
    case 5: pisz("PIATEK.");  
    case 6: pisz("SOBOTA.");  
    case 7: pisz("NIEDZIELA.");  
  default: pisz("\n *********************");  
  }  
}  
 
Zwróć uwagę, że w przykładzie wariant default zostanie wykonany  
ZAWSZE, nawet jeśli podasz liczbę większą niż 7. 
 

[P034.CPP]  

 
# define pisz printf 
# include <stdio.h> 
 
void main()  
{  
int Numer_Dnia;  
  pisz("\nPodaj numer dnia tygodnia\n");  
  scanf("%d", &Numer_Dnia);  
  switch(Numer_Dnia)  
  {  
    case 1: pisz("PON."); break; 
    case 2: pisz("WTOR"); break; 
    case 3: pisz("SRO."); break; 
    case 4: pisz("CZW."); break; 
    case 5: pisz("PIO."); break; 
    case 6: pisz("SOB."); break; 
    case 7: pisz("NIEDZ."); break; 
  default: pisz("\n ?????");  
  }  

120

background image

}  
 
Instrukcja break przerywa wykonanie. Wariant default zostanie  
wykonany TYLKO w przypadku podania liczby większej niż 7.  
 

INSTRUKCJA POWROTU return.  

 
Służy do zakończenia wykonania zawierającej ją funkcji i może  
mieć postać:  
 
return;  
return stała;  
return Wyrażenie;  
return (wyrażenie);  
 
Przykład:  
 
Definiujemy funkcję _dodaj() zwracającą, poprzez instrukcję  
return wartość przekazanego jej w momencie wywołania argumentu  
powiększoną o 5.  
 
float _dodaj(float x)  
{  
  x+=5;  
  return x;  
}  
 
Funkcja _dodaj() zwraca wartość i nadaje tę wartość zmiennej  
wynik zadeklarowanej nazewnątrz funkcji i znanej w programie  
głównym. A oto program w całości.  
 

[P035.CPP] 

 
float funkcja_dodaj(float x)  
{  
 x += 5;  
 return x;  

float dana = 1, wynik = 0;  
 
void main()  
{  
  clrscr();  
  wynik = funkcja_dodaj(dana);  
  printf("%f", wynik);   
}  
 

INSTRUKCJA SKOKU BEZWARUNKOWEGO goto I ETYKIETY.  

 
Składnia instrukcji skoku goto jest następująca:  
 
goto Identyfikator_etykiety;  
 
UWAGA: Po każdej etykiecie musi wystąpić CO NAJMNIEJ JEDNA  
INSTRUKCJA. Jeśli etykieta oznacza koniec programu, to musi po  
niej wystąpić instrukcja pusta. Instrukcja goto nie cieszy się  

121

background image

powodzeniem ani dobrą sławą (niesłusznie!). Ostrożne i umiejętne 
 
jej stosowanie jeszcze nikomu nie zaszkodziło. Należy tu  
zaznaczyć, że etykieta nie wymaga deklaracji. 
 
Przykład:   
 
Program poniżej generuje dźwięki i "odlicza".  
 

[P036.CPP]  

 
#include <dos.h>  
#include <stdio.h>  
 
void main()  
{  
int czestotliwosc=5000, n=10, milisekundy=990;  
printf("\n");  
start: 
  {  
    sound(czestotliwosc);  
    delay(milisekundy);  
    nosound();  
    czestotliwosc/=1.2;  
    printf("%d\b", --n);  
       if (n) goto start;   //petle strukturalne zrob sam(a) 
  } 
 
koniec: ;  
}                    // Tu jest instrukcja pusta. 
 
[S!] DOS API function names - nazwy funkcji z interfejsu DOS 
________________________________________________________________ 
sound - dźwięk;  
delay - opóźnienie, zwłoka;  
nosound - bez dźwięku (wyłącz dźwięk); 
________________________________________________________________ 
 

Zadania  

________________________________________________________________ 
1. Biorąc pod uwagę, że iloraz częstotliwości kolejnych dźwięków 
 
jest stały tzn. Fcis/Fc=Ffis/Ff=....=const oraz, że oktawa to  
podwojenie częstotliwości, opracuj program i oblicz  
częstotliwości poszczególnych dźwięków.  
2. Spróbuj zastosować w programie przykładowym kolejno pętle 
for, while, do...while.  
3. Zastosuj we własnym programie doświadczalnym instrukcję  
switch.

LEKCJA 14. Jak tworzyć i stosować struktury.  

 

122

background image

________________________________________________________________ 
 
W trakcie tej lekcji poznasz pojęcia:  
* Klasy zmiennej.  
* Struktury.  
* Pola bitowego. 
* Unii. 
Dowiesz się także więcej o operacjach logicznych. 
________________________________________________________________ 
 
 

CO TO JEST KLASA ZMIENNEJ?  

 
W języku C i C++ programista ma większy wpływ na rozmieszczenie  
zmiennych w pamięci operacyjnej komputera i w rejestrach  
mikroprocesora. Może to mieć decydujący wpływ na dostępność  
danych z różnych miejsc programu i szybkość działania programu.  
Należy podkreślić, że TYP ZMIENNEJ (char, int, float itp.)  
decyduje o sposobie interpretacji przechowywanych w pamięci zer  
i jedynek, natomiast KLASA ZMIENNEJ decyduje o sposobie  
przechowywania zmiennej w pamięci. W C++ występują cztery klasy  
zmiennych. 
 

ZMIENNE STATYCZNE - static. 

 
Otrzymują stałą lokalizację w pamięci w momencie uruchamiania  
programu. Zachowują swoją wartość przez cały czas realizacji  
programu, chyba, że świadomie zażądamy zmiany tego stanu - np.  
instrukcją przypisania.  
Przykład deklaracji:   static float liczba; 
 
W większości kompilatorów C++ zmienne statyczne, które nie  
zostały jawnie zainicjowane w programie, otrzymują po  
zadeklarowaniu wartość ZERO.  
 

ZMIENNE AUTOMATYCZNE - auto.  

 
Otrzymują przydział miejsca w pamięci dynamicznie - na stosie  
procesora, w momencie rozpoczęcia wykonania tego bloku programu, 
 
w którym zmienne te zostały zadeklarowane. Przydzielenie pamięci 
 
nie zwalnia nas z obowiązku zainicjowania zmiennej (wcześniej  
wartość zmiennej jest przypadkowa). Zmienne automatyczne  
"znikają" po zakończeniu wykonywania bloku. Pamięć im  
przydzielona zostaje zwolniona. Przykład:   auto long suma; 
 

ZMIENNE REJESTROWE - register.  

 
Zmienne rejestrowe są także zmiennymi lokalnymi, widocznymi  
tylko wewnątrz tego bloku programu, w którym zostały  
zadeklarowane. C++ może wykorzystać dwa rejestry mikroprocesora  
- DI i SI do przechowywania zmiennych. Jeśli zadeklarujemy w  
programie więcej zmiennych jako zmienne rejestrowe - zostaną one 

123

background image

 
umieszczone na stosie. Znaczne przyspieszenie działania programu 
 
powoduje wykorzystanie rejestru do przechowywania np. licznika  
pętli.  
 
Przykład:  
 
register int i;  
.....  
for (i=1; i<1000; i++) {.....}  
 

ZMIENNE ZEWNĘTRZNE - extern. 

 
Jeśli zmienna została - raz i TYLKO RAZ - zadeklarowana w  
pojedynczym segmencie dużego programu, zostanie w tymże  
segmencie umieszczona w pamięci i potraktowana podobnie do  
zmiennych typu static. Po zastosowaniu w innych segmentach  
deklaracji extern zmienna ta może być dostępna w innym segmencie 
 
programu.  
 
Przykład:    extern int NUMER;  
 
 

STRUKTURY.  

 
Poznane wcześniej tablice mogą zawierać wiele danych, ale  
wszystkie te dane muszą być tego samego typu. Dla zgrupowania  
powiązanych ze sobą logicznie danych różnego typu C/C++ stosuje  
STRUKTURY, deklarowane przy pomocy słowa struct. Kolejne pola  
struktury są umieszczane w pamięci zgodnie z kolejnością ich  
deklarowania. Strukturę, podobnie jak zmienną, MUSIMY  
ZADEKLAROWAĆ. Struktura jest objektem bardziej złożonym niż  
pojedyncza zmienna, więc i deklaracja struktury jest bardziej  
skomplikowana. Deklaracja struktury składa się z następujących  
elementów: 
 
1. Słowo kluczowe struct (obowiązkowe).  
2. Nazwa (opcjonalna). Jeśli podamy nazwę, to nazwa ta będzie  
oznaczać dany typ struktury. 
3. Nawias klamrowy { 
4. Deklaracje kolejnych składników struktury.  
5. Nawias klamrowy }  
6. Lista nazw struktur określonego powyżej typu (może zostać  
zadeklarowana oddzielnie). 
 
Przykład. Deklaracja ogólnego typu struktury i określenie  
wewnętrznej postaci struktury. 
 
struct Ludzie   
{  
char Imiona[30];  
char Nazwisko[20];  
int wiek;  
char pokrewienstwo[10]  
};  

124

background image

 
Jeśli określimy już typ struktury - czyli rodzaj, wielkość i  
przeznaczenie poszczególnych pól struktury, możemy dalej tworzyć 
 
- deklarować i inicjować konkretne struktury danego typu. 
 
Przykład. Deklaracja zmiennych - struktur tego samego typu. 
 
struct Ludzie Moi, Twoi, Jego, Jej, Szwagra;  
 
Deklarację struktur można połączyć.  
 
Przykład. Połączona deklaracja struktur.  
 
struct Ludzie  
{ char pokrewienstwo[10]; 
char Imiona[30]; 
int wiek;  
} Moi, Twoi, Szwagra;  
 
Struktury statyczne  
 
* mają stałe miejsce w pamięci w trakcie całego programu;  
* są "widoczne" i dostępne w całym programie.  
 
Zadeklarujemy teraz typ struktury i zainicjujemy dwie struktury. 
 
 
Przykład. Zainicjowanie dwu struktur statycznych.  
 
struct Ludzie  
{ char pokrewienstwo[10];  
  char Imiona[30];  
  int wiek;  
};  
 
struct Ludzie Moi, Szwagra;  
static struct Ludzie Moi = { "Stryjek", "Walenty", 87 };  
static struct Ludzie Szwagra = { "ciotka", "Ala", 21 };  
 
Zapis  
 
static struct Ludzie Szwagra; 
 
oznacza:  
statyczna struktura typu "Ludzie" pod nazwą "Szwagra".  
 
Do struktury w całości możemy odwoływać się za pomocą jej nazwy  
a do poszczególnych elementów struktury poprzez nazwę struktury  
i nazwę pola struktury - ROZDZIELONE KROPKĄ ".". Zademonstrujmy  
to na przykładzie. Zwróć uwagę na różne sposoby przekazywania  
danych pomiędzy strukturami: 
 
C4.Wiek=Czlowiek2.Wiek; - przekazanie zawartości pojedynczego  
pola numerycznego;  
C4=Czlowiek3; - przekazanie zawartości całej struktury Czlowiek3 
 
do C4. 
 
Przykład. 

125

background image

Program manipulujący prostą strukturą.  

 

[P037.CPP]  

 
int main()  
{  
  
struct Ludzie 
{  
  char Imie[20];  
  int Wiek;  
  char Status[30];  
  char Tel_Nr[10]; 
};  
  
static struct Ludzie  
  Czlowiek1={"Ala", 7, "Ta, co ma Asa","?"},  
  Czlowiek2={"Patrycja", 13, "Corka", "8978987"},  
  Czlowiek3={"Krzysztof", 27, "Kolega z przedszkola", "23478"};  
  
struct Ludzie C4, C5;  
  
  C4=Czlowiek3;  
  C4.Wiek=Czlowiek2.Wiek;  
  C5=Czlowiek1;  
 
clrscr();  
 
printf("%s %d %s\n", C4.Imie, C4.Wiek, C4.Status);  
printf("%s  %s",C5.Imie, C5.Status);  
  
return 0;  
}  
 
Tablice mogą być elementami struktur, ale i odwrotnie - ze  
struktur, jak z cegiełek można tworzyć konstrukcje o wyższym  
stopniu złożoności - struktury struktur i tablice struktur.  
Jeśli tablica składa się z liczb typu int, to deklarujemy ją:  
 
int TABLICA[10];  
 
jeśli tablica składa się ze struktur, to deklarujemy ją:  
 
struct TABLICA[50];  
 
W przykładzie poniżej przedstawiono  
 
* deklarację jednowymiarowej tablicy LISTA[50],  
* elementami tablicy są struktury typu SCzlowiek,  
* jednym z elementów każdej struktury SCzlowiek jest struktura  
"niższego rzędu" typu Adres;  
 

[P038.CPP]  

 
int main()  
{  
 

126

background image

struct Adres 
{  
  char Ulica[30];  
  int Nr_Domu;  
  int Nr_Mieszk; 
};  
  
struct SCzlowiek 
{  
  char Imie[20];  
  int Wiek;  
  struct Adres Mieszkanie; 
};  
  
struct SCzlowiek LISTA[50];  
  
LISTA[1].Wiek=34;  
LISTA[1].Mieszkanie.Nr_Domu=29;  
printf("%d", LISTA[1].Mieszkanie.Nr_Domu);  
  
return 0;  
}  
  
Zapis  
 
printf("%d", LISTA[1].Mieszkanie.Nr_Domu  
 
oznacza:  
* wybierz element nr 1 z tablicy LISTA;  
(jak wynika z deklaracji tablicy, każdy jej element będzie miał  
wewnętrzną strukturę zorganizowaną tak, jak opisano w deklaracji 
 
struktury SCzlowiek);  
* wybierz ze struktury typu SCzlowiek pole Mieszkanie;  
(jak wynika z deklaracji, pole Mieszkanie będzie miało  
wewnętrzną organizację zgodną ze strukturą Adres);  
* ze struktury typu Adres wybierz pole Nr_Domu;  
* Wydrukuj zawartość pola pamięci interpretując ją jako liczbę  
typu int - w formacie %d.  
 
Słowo struktura tak doskonale pasuje, że chciałoby się  
powiedzieć:  
jeśli struktura struktur jest wielopoziomowa, to podobnie, jak  
przy wielowymiarowych tablicach, każdy poziom przy nadawaniu  
wartości musi zostać ujęty w dodatkową parę nawiasów klamrowych. 
 
 

A CO Z ŁAŃCUCHAMI ZNAKOWYMI ? 

________________________________________________________________ 
Język C++ oferuje do kopiowania łańcuchów znakowych specjalną  
funkcję strcpy(). Nazwa funkcji to skrót STRing CoPY (kopiuj  
łańcuch). Sposób wykorzystania tej funkcji:  
 
strcpy(Dokąd, Skąd); lub  
strcpy(Dokąd, "łańcuch znaków we własnej osobie");  
 
Szczegóły - patrz Lekcja o łańcuchach znakowych.  
________________________________________________________________ 
 

127

background image

 

STRUKTURY I WSKAŹNIKI.  

 
Wskaźniki mogą wskazywać strukturę w całości lub element  
struktury. Język C/C++ oferuje specjalny operator -> który  
pozwala na odwoływanie się do elementów struktury. W przykładzie 
 
poniżej przedstawiono różne sposoby odwołania się do elementów  
trzech identycznych struktur STA, STB, STC.  
 

[P039.CPP] 

 
int main()  
{  
  
struct  
{  
  char Tekst[20];  
  int Liczba1;  
  float Liczba2; 
} STA, STB, STC, *Pointer;  
  
  STA.Liczba1 = 1;  
  STA.Liczba2 = 2.2;  
  strcpy(STA.Tekst, "To jest tekst");  
  
  STB=STA;  
  
  Pointer = &STC;  
  Pointer->Liczba1 = 1;  
  Pointer->Liczba2 = 2.2;  
  strcpy(Pointer->Tekst, STA.Tekst);  
  
  printf("\nLiczba1-STA Liczba2-STB Tekst-STC\n\n");  
  printf("%d\t", STA.Liczba1);  
  printf("%f\t", STB.Liczba2);  
  printf("%s", Pointer->Tekst);  
 
return 0;  
}  
  
Rozszyfrujmy zapis:  
 
strcpy(Pointer->Tekst, STA.Tekst);  
 
Skopiuj łańcuch znaków z pola Tekst struktury STA do pola Tekst  
struktury wskazywanej przez pointer. Prawda, że to całkiem  
proste?  
 

CZY MUSIMY TO ROZDZIELAĆ ?  

________________________________________________________________ 
Jak zauważyłeś, liczby moglibyśmy zapisywać także jako łańcuchy  
znaków, ale wtedy nie moglibyśmy wykonywać na tych liczbach  
działań. Konwersję liczba - łańcuch znaków lub odwrotnie łańcuch 
 
znaków - liczba wykonują w C specjalne funkcje np.:  

128

background image

atoi() - Ascii TO Int.;  
itoa() - Int TO Ascii itp.  
Więcej informacji na ten temat i przykłady znajdziesz w dalszej  
części książki. 
________________________________________________________________ 
 
 
Elementami struktury mogą być zmienne dowolnego typu, łądznie z  
innymi strukturami.  
 

Ciekawostka:  

________________________________________________________________ 
Wskaźnik do deklarowanej struktury może być w języku C/C++ jak  
jeden z jej WŁASNYCH elementów. Jeśli wskaźnik wchodzący w skład 
 
struktury wskazuje na WŁASNĄ strukturę, to nazywa się to  
AUTOREFERENCJĄ STRUKTURY. 
________________________________________________________________ 
 

POLA BITOWE.  

 
Często zdarza się, że jakaś zmienna ma zawężony zakres wartości. 
 
Dla przykładu zmienne logiczne (tzw. flagi) to zawsze tylko 0  
lub 1. Wiek rzadko przekracza 255 lat a liczba dzieci zwykle nie 
 
jest większa niż 15. Nawet najbardziej niestali panowie nie  
zdążą ożenić się i rozwieść więcej niż 7 razy. Gdybyśmy zatem  
chcieli zapisać informacje  
 
* płeć 0 - mężczyzna, 1 - kobieta ( 1 bit );  
* wiek 0 - 255 lat (8 bitów);  
* ilość dzieci 0 - 15 (4 bity);  
* kolejny numer małżeństwa 0 - 7 (3 bity);  
 
to przecież wszystkie te informacje mogą nam się zmieścić w  
jednym szesnastobitowym rejestrze lub w dwu bajtach pamięci.  
Takie kilka bitów wydzielone i mające określone znaczenie to  
właśnie pole bitowe. C++ pozwala także na uwzględnianie znaku w  
polach bitowych. Pola bitowe mogą być typu int i unsigned int  
(czyli takie jak w przykładzie poniżej). Jeśli jakieś dane  
chcemy przechowywać w postaci pola bitowego, w deklaracji  
struktury sygnalizujemy to dwukropkiem. Stwarza to dwie istotne  
możliwości:  
* bardziej ekonomicznego wykorzystania pamięci;  
* łatwego dodatkowego zaszyfrowania danych.  
 

[P040.CPP]  

 
//Pamietaj o dolaczeniu plikow naglowkowych ! 
 
int main()  
{  
 
struct USC {  
  int Sex : 1;  

129

background image

  unsigned Wiek : 8;  
  unsigned Dzieci : 4;  
  unsigned Ktora  : 3; } Facet;  
 
  int bufor;  
  clrscr();  
  Facet.Sex = 0;  
  printf("\n Ile ma lat ?  : ");  
  scanf("%d", &bufor); Facet.Wiek = bufor;  
  printf("\n Ktore malzenstwo ?  : ");  
  scanf("%d", &bufor); Facet.Ktora = bufor;  
  printf("\n Ile dzieci ?  : ");  
  scanf("%d", &bufor); Facet.Dzieci = bufor;  
  printf("\n\n");  
   if (Facet.Ktora) printf("Facet ma %d zone", Facet.Ktora);  
  printf("\nPlec:   Dzieci:   Wiek (lat): \n\n");  
  printf("%d\t%d\t%d", Facet.Sex, Facet.Dzieci, Facet.Wiek);  
  getch();  
   
  return 0;  
}  
 
Uruchom program i sprawdź co się stanie, jeśli Facet będzie miał 
 
np. 257 lat lub 123 żonę. Przekroczenie zadeklarowanego zakresu  
powoduje obcięcie części bitów.  
 
Aby uzyskać "wyrównanie" pola bitowego do początku słowa należy  
przed interesującym naspolem bitowym zdefiniować tzw. pole  
puste:  
 
* pole bitowe bez nazwy;  
* długość pola pustego powinna wynosić 0.  
 
Poniżej przedstawiam przykład pola bitowego zajmującego trzy  
kolejne słowa 16 bitowe. Dodanie pola pustego wymusza  
rozpoczęcie pola pole_IV od początku trzeciego słowa maszynowego 
 
(zakładamy, że pracujemy z komputerem 16 bitowym).  
 
struct  
  {  
    unsigned pole_I:4;  
    unsigned pole_II:10;  
    unsigned pole_III:4;  
    unsigned :0;             /* to jest pole puste */  
    unsigned pole_IV:5;  
  } pole_przykladowe;  
 
Zwróć uwagę, że część bitów w drugim i trzecim słowie maszynowym 
 
nie zostanie wykorzystana. 
 

UNIE czyli ZMIENNE WARIANTOWE.  

 
Unie to specyficzne struktury, w których pola pamięci  
przeznaczone na objekty różnego typu nakładają się. Jeśli jakaś  
zmienna może być reprezentowana na kilka sposobów (wariantów) to 
 

130

background image

sensowne jest przydzielenie jej nie struktury a unii. W danej  
chwili pole pamięci należące do unii może zawierać TYLKO JEDEN  
WARIANT. W przykładzie - albo cyfrę (która znakowo jest widziana 
 
jako znak ASCII o kodzie 2,3,4 itd.) albo napis. Do  
zadeklarowania unii służy słowo kluczowe union. 
 

[P041.CPP]  

 
#include "string.h"  
#include "stdio.h"  
  
int BUFOR, i;  
  
int main()  
{   
  
union 
{   
  int Cyfra;   
  char Napis[20];  
} Unia;   
 
for (i=1; i<11; i++)   
  {   
  printf("\n Podaj liczbe jednocyfrowa:  ");   
  scanf("%d", &BUFOR);   
     if (BUFOR<0 || BUFOR>9)   
         strcpy(Unia.Napis, "TO NIE CYFRA !");   
     else   
         Unia.Cyfra = BUFOR;   
  printf("\n Pole jako Cyfra    Pole jako Napis \n");  
  
/* Tu wyswietlimy warianty: Pole jako cyfra i jako napis*/  
/* Petla pozwoli Ci przeanalizowac wszystkie cyfry 0...9 */  
  
  printf(" %d\t\t\t%s", Unia.Cyfra, Unia.Napis);  
  }   
return 0;   
}  
  
 
Pętla w przykładzie nie ma znaczenia. Służy tylko dla Twojej  
wygody - dzięki niej nie musisz uruchamiać programu  
przykładowego wielokrotnie. Podobnie zmienne BUFOR oraz i mają  
znaczenie pomocnicze. Zwróć uwagę, że nieprawidłowa  
interpretacja zawartości pola unii może spowodować wadliwe  
działanie programu.  
 

Zadania

________________________________________________________________ 
1. W programie przykładowym zamień unię na strukturę. Porównaj  
działanie.  
2 Przydziel na Wiek w strukturze Facet o jeden bit mniej. Ile  
lat może teraz mieć Facet ?  
3. Zmodyfikuj program przykładowy tak, by napis o liczbie  
mężów/żon zależał od płci - pola Sex.  

131

background image

4. Zamieniwszy unię na strukturę w programie, sprawdź, czy  
wpływa to na wielkość pliku *.EXE.  
________________________________________________________________ 
 

OPERACJE LOGICZNE.  

 
Zaczniemy od operacji logicznych na pojedynczych bitach liczb  
całkowitych. W C++ mamy do dyspozycji następujące operatory:  
 
~€€€€Zaprzeczenie (NOT)  ~0=1;  ~1=0;  
|€€€€Suma (OR)  0|0=0;  0|1=1;  1|0=1;  1|1=1;  
&€€€€Iloczyn (AND)  0&0=0;  0&1=0;  1&0=0;  1&1=1;  
^€€€€Alternatywa wyłączna ALBO...ALBO (XOR)  
€€€€€0^0=0;  0^1=1;  1^0=1;  1^1=0;  
<<€€€Przesunięcie bitów w lewo (Shift Left)  
€€€€€<< 00001000 = 00010000  dzieś. 8<<1=16 
>>€€€Przesunięcie bitów w prawo (Shift Right)  
€€€€€>> 00001000 = 00000100  dzieś. 8>>2=2 
 
Miło byłoby pooglądać to trochę dokładniej w przykładowych  
programach, ale potrzebne nam do tego będą funkcje. Zajmijmy się 
więc uważniej funkcjami.

LEKCJA 15. Jak posługiwać się funkcjami.  

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się więcej o:  
* funkcjach i prototypach funkcji;  
* przekazywaniu argumentów funkcji; 
* współpracy funkcji ze wskaźnikami. 
_______________________________________________________________  
 
Aby przedstawić działanie operatorów logicznych opracujemy 
własną funkcję Demo() i zastosujemy ją w programie przykładowym  
[najważniejszy fragment]. 

int Demo(int Liczba)  

int MaxNr=15; 
  for (; MaxNr>=0; MaxNr--) 
  { 
    if ((Liczba>>MaxNr)&1)  
        printf("1");  
    else  
        printf("0");  
  }  
return 0;   //Funkcja nie musi nic zwracac 
}  

Funkcja przesuwa liczbę o kolejno 15, 14, 13 itd. bitów w prawo  
i sprawdza, czy 16, 15, 14 bit jest jedynką, czy zerem. Iloczyn  
logiczny z jedynką ( 0000000000000001 ) gwarantuje nam, że wpływ 
na wynik operacji będzie miał tylko ten jeden bit (patrz wyżej - 
jak działają operatory logiczne).  

132

background image

 

[P042.CPP]  

 
# include <stdio.h> 
int Demo(int Liczba)  
{  
int MaxNr=15;  
for (; MaxNr>=0; MaxNr--)  
  if ((Liczba>>MaxNr)&1) printf("1");  
  else printf("0");  
return 0;  
}  
 
char odp;  
 
int main()  
{  
 int X, Y;  
 clrscr();  
 printf("\nPodaj dwie liczby calkowite od -32768 do +32767\n");  
 printf("\nLiczby X i Y rozdziel spacja");  
 printf("\nPo podaniu drugiej liczby nacisnij [Enter]");  
 printf("\nLiczby ujemne sa w kodzie dopelniajacym");  
 printf("\nskrajny lewy bit oznacza znak 0-Plus, 1-Minus");  
 for(;;)  
  { 
    printf("\n");  
    scanf("%d %d", &X, &Y);  
    printf("\nX:\t"); Demo(X);  
    printf("\nY:\t"); Demo(Y);  
    printf("\n~Y:\t"); Demo(~Y);  
    printf("\nX&Y:\t"); Demo(X&Y);  
    printf("\nX|Y:\t"); Demo(X|Y);  
    printf("\nX^Y:\t"); Demo(X^Y);  
    printf("\nY:\t"); Demo(Y);  
    printf("\nY>>1:\t"); Demo(Y>>1);  
    printf("\nY<<2:\t"); Demo(Y<<2);  
    printf("\n\n Jeszcze raz?  T/N");  
        odp=getch();  
        if (odp!='T'&& odp!='t') break; 
  }  
return 0;   
}  
 
Jeśli operacje mają być wykonywane nie na bitach a na logicznej  
wartości wyrażeń: 
 
|| oznacza sumę (LUB);  
&& oznacza iloczyn (I);  
!  oznacza negację (NIE).  
 
Przykłady:  
 
 (x==0 || x>5) -  x równa się 0 LUB x większy niż 5; 
 (a>5 && a!=11) -  a większe niż 5 I a nie równe 11; 
 (num>=5 && num!=6 || a>0) 
 
num nie mniejsze niż 5 I num nie równe 6 LUB a dodatnie;  
Wyrażenia logiczne sprawdzane instrukcją if MUSZĄ być ujęte w  

133

background image

nawiasy okrągłe.  
 
Do wytworzenia wartości logicznej wyrażenia może zostać użyty  
operator relacji: <  <=  ==  >=  >  != . Jeśli tak się nie  
stanie, za wartość logiczną wyrażenia przyjmowane jest:  
 
1, PRAWDA, TRUE, jeśli wartość numeryczna wyrażenia jest różna  
od zera.  
0, FAŁSZ, FALSE,  jeśli wartość numeryczna wyrażenia jest równa  
zero.  
 
Porównaj:  
 
if (a<=0) ...  
if (a) ...  
if (a+b) ...  
 

Konwersja - przykłady.  

 
C++ dysponuje wieloma funkcjami wykonującymi takie działania,  
np:  
 
itoa() - Integer TO Ascii - zamiana liczby typu int na łańcuch  
znaków ASCII;  
ltoa() - Long int TO Ascii - zamiana long int -> ASCII;  
atoi() - zamiana Ascii -> int;  
atol() - zamiana Asdii -> long int .  
 
Wszystkie wymienione funkcje przekształcając liczby na łańcuchy  
znaków potrzebują trzech parametrów:  
 
p1 - liczby do przekształcenia; 
p2 - bufora, w którym będą przechowywać wynik - łańcuch ASCII; 
p3 - podstawy (szesnastkowa, dziesiętna itp.). 
 
Jeśli chcemy korzystać z tych funkcji, powinniśmy dołączyć plik  
nagłówkowy z ich prototypami - stdlib.h (STandarD LIBrary -  
standardowa biblioteka). A oto przykład.  
 

[P043.CPP]  

 
# include "stdio.h"  
# include "stdlib.h"  
 
main()  
{  
int i; 
char B10[10], B2[20], B16[10];   //BUFORY 
for (i=1; i<17; i++)  
  printf("%s  %s  %s\n",  
                         itoa(i, B10[i], 10), 
                         itoa(i, B2[i], 2), 
                         itoa(i, B16[i], 16));  
return 0;  
}  
 
[Z]  

134

background image

________________________________________________________________ 
1. Opracuj program testujący działanie funkcji atoi().  
________________________________________________________________ 
 

KILKA SŁÓW O TYPACH DANYCH i KONWERSJI W C/C++ . 

 
Przed przystąpieniem do obszernego zagadnienia "funkcje w C"  
krótko zasygnalizujemy jeszcze jedno zjawisko. Wiesz z  
pewnością, że wykonywane na liczbach dwójkowych mnożenie może  
dać wynik o długości znacznie większej niż mnożna i mnożnik. W  
programach może się poza tym pojawić konieczność np. mnożenia  
liczb zmiennoprzecinkowych przez całkowite. Jak w takich  
przypadkach postępuje C++ ?  
 
Po pierwsze:  
 
C/C++ może sam dokonywać konwersji, czyli zmiany typów danych  
naogół zgodnie z zasadą nadawania zmiennej "mniej pojemnego"  
rodzaju typu zmiennej "bardziej pojemnego" rodzaju przed  
wykonaniem operacji;  
 
Po drugie:  
 
my sami możemy zmusić C++ do zmiany typu FORSUJĄC typ świadomie  
w programie.  
W przykładzie poniżej podając w nawiasach żądany typ zmiennej  
forsujemy zmianę typu int na typ float.  
 

[P044.CPP]  

 
# include "stdio.h" 
void main()  
{  
  int a=7;  
  printf("%f", (float) a);   

 
Konwersja typów nazywana bywa także "rzutowaniem" typów (ang.  
type casting). A oto kilka przykładów "forsowania typów":  
 
int a = 2;  
float x = 17.1, y = 8.95, z;  
char c;  
  
   c = (char)a + (char)x;  
   c = (char)(a + (int)x);  
   c = (char)(a + x);  
   c = a + x;  
  
   z = (float)((int)x * (int)y);  
   z = (float)((int)x * (int)y);  
   z = (float)((int)(x * y));  
   z = x * y;  
  
   c = char(a) + char(x);  
   c = char(a + int(x));  
   c = char(a + x);  

135

background image

   c = a + x;  
  
   z = float(int(x) * int(y));  
   z = float(int(x) * int(y));  
   z = float(int(x * y));  
   z = x * y;  
 

FUNKCJE BIBLIOTECZNE I WŁASNE W JĘZYKU C/C++ .  

 
Pojęcie funkcji obejmuje w C/C++ zarówno pascalowe procedury,  
jak i basicowe podprogramy. Funkcji zdefiniowanych w C++ przez  
prducenta jest bardzo dużo. Dla przykładu, funkcje arytmetyczne, 
 
które możesz wykorzystać do obliczeń numerycznych to np.:  
 
abs() - wartość bezwzględna,  
cos() - cosinus, sin() - sinus, tan() - tangens,  
asin(), atan(), acos(), - funkcje odwrotne ARCUS SINUS...  
funkcje hiperboliczne: sinh(), cosh(), tanh(),  
wykładnicze i logarytmiczne:  
exp() - e^x  
log() - logarytm naturalny,  
log10() - logarytm dziesiętny. 
  
Jeśli skorzystasz z systemu Help i zajrzysz do pliku math.h  
(Help | Index | math.h), znajdziesz tam jeszcze wiele  
przydatnych funkcji. 
 
Funkcja może, ale nie musi zwracać wartość do programu -  
dokładniej do funkcji wyższego poziomu, z której została  
wywołana. W ciele funkcji służy do tego instrukcja return.  
Użytkownik może w C++ definiować własne funkcje. Funkcja może  
być bezparametrowa. Oto przykład bezparametrowej funkcji,  
zwracającej zawsze liczbę całkowitą trzynaście:  
 
int F_Trzynascie()  
{  
return 13;  

 
Poprawne wywołanie naszej funkcji w programie głównym miałoby  
postać:  
 
int main()  
{  
......  
int X;  
........           // Funkcja typu int nie musi byc deklarowana. 
X = F_Trzynascie();  
......  
}  
 
Jeśli funkcja musi pobrać jakieś parametry od programu (funkcji  
wyższego poziomu, wywołującej)? Zwróć uwagę, że program główny w 
 
C/C++ to też funkcja - main(). Przykład następny pokazuje  
definicję funkcji obliczającej piątą potęgę pobranego argumentu  
i wywołanie tej funkcji w programie głównym.  
 

136

background image

Przykład: 
 
int F_XdoPiatej(int argument)  
{  
int robocza;   //automatyczna wewnetrzna zmienna funkcji 
robocza = argument * argument;  
robocza = robocza * robocza * argument;  
return (robocza);  
}  
  
int main()  
{  
int Podstawa, Wynik, a, b;  
  ...   /* Funkcja nie jest deklarowana przed uzyciem */ 
Wynik = F_XdoPiatej(Podstawa);  
 .....  
a = F_XdoPiatej(b); 
 ..... 
return 0; 

 
Zwróć uwagę, że definiując funkcję podajemy nazwę i typ  
ARGUMENTU FORMALNEGO funkcji - tu: argument. W momencie  
wywołania na jego miejsce podstawiany jest rzeczywisty bieżący  
argument funkcji.  
 
Aby zapewnić wysoką dokładność obliczeń wymienione wyżej funkcje 
 
biblioteczne sqrt(), sin() itp. "uprawiają" arytmetykę na  
długich liczbach typu double. Funkcję taką przed użyciem w swoim 
 
programie MUSISZ ZADEKLAROWAĆ. Przykład: 
 

[P045.CPP] 

 
main()  
{  
double a, b;  
double sqrt();     // tu skasuj deklaracje funkcji sqrt()  
                  // a otrzymasz bledny wynik !  
clrscr(); 
printf("Podaj liczbe\n");  
scanf("%lf", &a);  
b = sqrt(a);  
printf("\n %Lf", (long double) b);  
getch();  
return 0;  

 

PROTOTYPY FUNKCJI, czyli jeszcze o deklaracjach funkcji.  

 
Prototyp funkcji to taka deklaracja, która:  
 
* została umieszczona na początku programu poza funkcją main(),  
* zawiera deklarację zarówno typu funkcji, jak i typów  
argumentów.  
 

137

background image

Przykład prototypu (funkcja2.cpp):  
 
double FUNKCJA( double X, double Y);  
 
main()  
{  
double A=0, B=3.14;  
printf("Wynik działania funkcji: \n");  
printf("%lf", FUNKCJA(A,B));  
return 0; }  
 
double FUNKCJA(double X, double Y)  
{  
return ((1+X)*Y);  
}  
 
Prototyp mógłby równie dobrze wyglądać tak:  
 
double FUNKCJA(double, double);  
 
nazwy parametrów formalnych nie są istotne i można je pominąć.  
Jeśli prototyp funkcji wygląda tak:  
 
int Funkcja(int, char*, &float)  
 
oznacza to, że parametrami funkcji są wskaźniki do zmiennych,  
bądź referencje do zmiennych. Przy rozszyfrowywaniu takiej  
"abrakadabry" warto wiedzieć, że  
 
char*    oraz     char *  
int&     oraz     int &  
 
ma w tym przypadku identyczne znaczenie. 
 
W C++ wolno nie zwracać wartości funkcjom typu void. To dlatego  
właśnie często rozpoczynaliśmy programy od  
 
void main()  
 
Skutek praktyczny: Jeśli w ciele funkcji typu void występuje  
instrukcja return (nie musi wystąpić) to instrukcja ta nie może  
mieć argumentów.  
 
Oto przykład prototypu, definicji i wywołania funkcji typu void: 
 
 

[P046.CPP] 

 
#include <stdio.h> 
#include <conio.h>  
 
void RYSUJPROSTOKAT( int Wys, int Szer, char Wzorek);  
 
void main()  
{  
clrscr();  
RYSUJPROSTOKAT(5, 20, '€');  // klocek ASCII 176 - [Alt]-[176] 
getch();  
RYSUJPROSTOKAT(15, 15, '€'); //[Alt]-[177]  

138

background image

getch(); 

 
void RYSUJPROSTOKAT( int Wys, int Szer, char Wzorek) 

int i, j;         // automatyczne zmienne wewnętrzne funkcji  
for(i=1; i<=Wys; i++)  
    { 
     for(j=1; j<=Szer; j++) printf("%c",Wzorek);  
     printf("\n");  
    }  

 
Prototypy wszystkich funkcji standardowych znajdują się w  
plikach nagłówkowych *.H (ang. Header file).  
 
Skutek praktyczny:  
JEŚLI DOŁĄCZYSZ DO PROGRAMU STOSOWNE PLIKI NAGŁÓWKOWE *.h,możesz 
 
ZREZYGNOWAĆ Z DEKLARACJI FUNKCJI. Dodając do programu wiersz: 
 
#include <math.h>  
 
dołączający plik zawierający prototyp funkcji sqrt(), możesz  
napisać program tak:  
 
#include <stdio.h>  
#include <math.h>  
main()  
{  
  
  double a, b;  
  clrscr(); 
  printf("Podaj liczbe\n");  
  scanf("%lf", &a);  
  b = sqrt(a);  
  printf("\n %Lf", (long double) b);  
  getch();  
 
  return 0;  

 
 

PRZEKAZYWANIE PARAMETRÓW DO FUNKCJI.  

 
W C++ często przekazuje się parametry do funkcji przy pomocy  
wskaźników. Aby prześledzić co dzieje się wewnątrz funkcji wpisz 
 
i uruchom podany niżej program przykładowy. Najpierw  
skonstruujemy sam program a następnie zmodyfikujemy go w taki  
sposób, abyś mógł sobie popodglądać cały proces. Przy pomocy  
funkcji printf() każemy wydrukować kolejne stany zmiennych, stan 
 
programu i funkcji, a funkcja getch() pozwoli Ci obejrzeć to  
"krok po kroku". Mogłoby się wydawać, że program poniżej  
skonstruowany jest poprawnie...  
 
void FUNKCJA( int );   //Prototyp, deklaracja funkcji  
 

139

background image

void main()  
{  
  int Zmienna;    //Zmienna funkcji main, rzeczywisty argument  
  clrscr();  
  Zmienna = 7;  
 
  FUNKCJA( Zmienna);  //Wywolanie funkcji 
   
  printf("%d", Zmienna);   //Wydruk wyniku  
}  
 
void FUNKCJA( int Argument)    //Definicja funkcji 
{  
  Argument = 10 * Argument + Argument;  

 
FUNKCJA() jest jak widać trywialna. będzie zamieniać np. 2 na  
22, 3 na 33 itp. tylko w tym celu, by łatwo było stwierdzić, czy 
 
funkcja zadziałała czy nie. 
 
Rozbudujmy program tak by prześledzić kolejne stadia.  
 

[P047.CPP] 

  
void FUNKCJA( int );   //Prototyp   
 
int Zmienna;  
void main()  
{  
  clrscr();  
  printf("Stadium: \tZmienna  Argument");  
  printf("\nStadium 1\t%d\tnie istnieje\n", Zmienna);  
  Zmienna = 7;  
  printf("Stadium 2\t%d\tnie istnieje\n", Zmienna );  
    FUNKCJA( Zmienna);  
  printf("Stadium 3\t%d", Zmienna);  
// printf("%d", Argument);  
// taka proba sie NIE UDA !  
  getch();  
}  
  
void FUNKCJA( int Argument)    //Definicja funkcji  
{   
  printf("jestesmy wewnatrz funkcji\n");  
  printf("Nastapilo kopiowanie Zmienna -> Argument\n" );  
  printf("\t\t%d\t%d\n", Zmienna, Argument);  
  getch();  
  Argument = 10*Argument + Argument;  
  printf("\t\t%d\t%d\n", Zmienna, Argument);  
  getch();  
}  
  
 
Próba wydrukowania zmiennej Argument gdziekolwiek poza wnętrzem  
FUNKCJI() nie uda się i spowoduje komunikat o błędzie. Oznacza  
to, że POZA FUNKCJĄ zmienna Argument NIE ISTNIEJE. Jest tworzona 
 
na stosie jako zmienna automatyczna na wyłączny użytek funkcji,  

140

background image

w której została zadeklarowana i znika po wyjściu z funkcji.  
Przy takiej organizacji funkcji i programu funkcja otrzymuje  
kopię zmiennej, na niej wykonuje swoje działania, natomiast  
zmienna (zmienne) wewnętrzna funkcji znika po wyjściu z funkcji. 
 
Problem przekazania parametrów pomiędzy funkcjami wywołującymi  
("wyższego rzędu" - tu: main) i wywoływanymi (tu: FUNKCJA) można 
 
rozwiązać przy pomocy  
 
* instrukcji return (zwrot do programu jednej wartości) lub 
* wskaźników.  
 
Możemy przecież funkcji przekazać nie samą zmienną, a wskaźnik  
do zmiennej (robiliśmy to już w przypadku funkcji scanf() -  
dlatego, że samej zmiennej jeszcze nie było - miała zostać  
dopiero pobrana, ale istniało już przeznaczone na tą nową  
zmienną - zarezerwowane dla niej miejsce. Mogł zatem istnieć  
wskaźnik wskazujący to miejsce). wskaźnik należy oczywiście  
zadeklarować. Nasz program przybrałby zatem nową postać.  
Wskaźnik do zmiennej nazwiemy *Argument.  
 

[P048.CPP]  

 
//Pamietaj o plikach naglowkowych !  
void FUNKCJA( int *Argument);   //Prototyp  
int Zmienna;  
void main()  
{  
  clrscr();  
  printf("Stadium: \tZmienna  Argument");  
  printf("\nStadium 1\t%d\tnie istnieje\n", Zmienna);  
  Zmienna = 7;  
  printf("Stadium 2\t%d\tnie istnieje\n", Zmienna );  
  FUNKCJA( &Zmienna);   //Pobierz do funkcji ADRES Zmiennej  
  printf("Stadium 3\t%d", Zmienna);  
// printf("%d", Argument);  
// taka proba sie NIE UDA !  
  getch();  
}  
  
void FUNKCJA( int *Argument)    // Definicja funkcji  
{  
  printf("jestesmy wewnatrz funkcji\n");  
  printf("Nastapilo kopiowanie ADRESOW a nie zmiennej\n" );  
  printf("ADRESY:\t\t %X\t%X\n", &Zmienna, Argument);  
 getch();  
     *Argument = 10*  *Argument + *Argument;  /* DZIALANIE */ 
  printf("\t\t%d\t%d\n", Zmienna, *Argument);  
 getch();  
}  
  
 
W linii /* DZIALANIE */ mnożymy i dodajemy to, co wskazuje  
wskaźnik, czyli Zmienną. Funkcja działa zatem nie na własnej  
kopii zmiennej a bezpośrednio na zmiennej zewnętrznej. Zwróć  
uwagę na analogię w sposobie wywołania funkcji:  
 
FUNKCJA( &Zmienna );  

141

background image

scanf( "%d", &Zmienna );  
 
A jeśli argumentem funkcji ma być tablica? Rozważ przykładowy  
program. Program zawiera pewną nadmiarowość (ku większej  
jasności mechanizmów).   
 

[P049.CPP]  

  
# include <conio.h>   
# include <stdio.h>   
   
SUMA( int k, int Tablica[] )   
{   
 int i, SumTab=0;   
 for (i=0; i<k; i++)   
  {   
    SumTab = SumTab + Tablica[i];   
    printf("%d + ", Tablica[i]);   
  }   
 printf("\b\b=  %d", SumTab);   
 return SumTab;   
}   
  
int suma=0, N; char Odp;   
int TAB[10] = {1,2,3,4,5,6,7,8,9,10};   
   
main()   
{   
 clrscr();   
   do   
    {  
      printf("\n Ile wyrazow tablicy dodac ??? \n");   
      scanf("%d", &N);   
        if (N>10)   
        { printf("TO ZA DUZO ! - max. 10");   
         continue;  
        }   
      suma = SUMA( N,TAB );   
      printf("\nTO JEST suma z progr. glownego  %d", suma);   
      printf("\n Jeszcze raz ?   T/N");   
      Odp = getch();   
    }  
   while (Odp!='N' && Odp!='n');  
  return 0;   
}  
  
 

Kompilacja w C++ jest wieloprzebiegowa (PASS 1, PASS 2)

, więc definicja funkcji może być zarówno na początku jak i na końcu. 
 
A oto następny przykład. Operując adresem - wskaźnikiem do  
obiektu (tu wskaźnikami do dwu tablic) funkcja Wypelniacz()  
zapisuje pod wskazany adres ciąg identycznych znaków. Na końcu  
każdego łańcucha znaków zostaje dodany NUL - (\0) jako znak  
końca. Taki format zapisu łańcuchów znakowych nazywa się ASCIIZ. 
 
 

142

background image

[P050.CPP]  

 
void Wypelniacz(char *BUFOR, char Znak, int Dlugosc);  
  
char TAB2D[5][10]; // Tablica 5 X 10 = 50 elementow  
char TAB_1D[50];     // Tablica 1 X 50 = 50 elementow  
int k;  
  
main()  
{  
  clrscr();  
  Wypelniacz( TAB_1D, 'X', 41);  //Wypelnia X-ami 
  printf("%s\n\n", TAB_1D);  
    for (k=0; k<5; k++) Wypelniacz( TAB2D[k], 65+k, 9);  
//ASCII 65 to 'A';  66 to 'B' itd. 
 
    for (k=0; k<5; k++) printf("%s\n", TAB2D[k]);  
  getch();  
  return 0;  
}  
  
void Wypelniacz( char *BUFOR, char Znak, int Dlugosc )  
{  
int i;  
for ( i=0; i<=(Dlugosc-1); i++) *(BUFOR+i) = Znak;  
  *(BUFOR+Dlugosc) = '\0';  
}  
  
Zwróć uwagę, że:  
* NAZWA TABLICY (tu: TAB_1D i TAB2D) funkcjonuje jako wskaźnik  
PIERWSZEGO ELEMENTU TABLICY. 
 

FUNKCJE TYPU WSKAŹNIKOWEGO.  

 
Funkcje mogą zwracać do programu zarówno wartości typu int, czy  
float, jak i wartości typu ADRESU. Podobnie jak wskaźnik wymaga  
deklaracji i podania w deklaracji na jakiego typu obiekty będzie 
 
wskazywał, podobnie funkcja takiego typu wymaga w deklaracji  
określenia typu wskazywanych obiektów. Wiesz już, że zależy od  
tego tzw. krok wskaźnika. W przykładzie poniżej funkcja  
Minimum() poszukuje najmniejszego elementu tablicy i zwraca  
wskaźnik do tegoż elementu. Znając lokalizację najmniejszego  
elementu możemy utworzyć nową tablicę, ale już uporządkowaną  
według wielkości.  
 

[P051.CPP]  

 
int BALAGAN[10];  
int PORZADEK[10];  // Tablica koncowa - uporzadkowana  
int k, *pointer , MAX=10000 ;  
  
int *Minimum(int Ilosc, int *TABL);  
  
main()  
{  
  clrscr();  

143

background image

  printf("Podaj 10 liczb calkowitych od -10000 do 10000\n");  
  for (k=0; k<=9; k++) scanf("%d", &BALAGAN[k]);  
  printf("Po kolei: \n\n");  
  for ( k=0; k<=9; k++ )  
  {  
€€€€€pointer=Minimum(10, BALAGAN);  
€€€€€PORZADEK[k]=*pointer;  
€€€€€*pointer=MAX;  
  }  
  for(k=0; k<=9; k++) printf("%d  ", PORZADEK[k]);  
  
  getch();  
  return 0;  
}  
 
int *Minimum( int Ilosc, int *TABL )  
{  
 int *pMin;  int i;  
 pMin=TABL;  
 for (i=1; i<Ilosc; i++)  
  {  
    if (*(TABL+i) < *pMin) pMin=(TABL+i);  
  }  
 return (pMin);  
}  
 
 

WSKAŹNIKI DO FUNKCJI.  

 
W C++ możemy nie tylko podstawić daną w miejsce zmiennej (co  
jest trywialną i oczywistą operacją we wszystkich językach  
programowania), ale możemy także podstawiać na miejsce funkcji  
stosowanej w programie tę funkcję, która w danym momencie jest  
nam potrzebna. Aby wskazać funkcję zastosujemy, jak sama nazwa  
wskazuje - WSKAŹNIK DO FUNKCJI. Aby uniknąć deklarowania funkcji 
 
standardowych i być w zgodzie z dobrymi manierami nie zapomnimy  
o dołączeniu pliku z prototypami. Deklarację  
 
double ( *FUNKCJA ) (double);  
 
należy rozumieć:  
"Przy pomocy wskaźnika do funkcji *FUNKCJA wolno nam wskazać  
takie funkcje, które  
* pobierają jeden argument typu double float;  
* zwracają do programu wartość typu double float. " 
Dostępne są dla nas zatem wszystkie standardowe funkcje  
arytmetyczne z pliku MATH.H (MATH pochodzi od MATHematics -  
matematyka.) 
 

[P052.CPP]  

 
# include <conio.h>  
# include <math.h>  
double NASZA( double );  //Deklaracja zwyklej funkcji  
 
double (*Funkcja)(double ARG);   //pointer do funkcji  

144

background image

 
double Liczba, Wynik;  //Deklaracje zmiennych 
int WYBOR;  
 
main()  
{  
  clrscr();  
  printf("Podaj Liczbe \n");  
  scanf("%lf", &Liczba);  
  printf("CO MAM ZROBIC ?\n");  
  printf("1 - Sinus \n");  
  printf("2 - Pierwiastek\n");  
  printf("3 - Odwrotnosc 1/x\n");  
  scanf("%d", &WYBOR);  
    switch(WYBOR)  
    {  
    case 1: Funkcja=sin; break;  
    case 2: Funkcja=sqrt; break;  
    case 3: Funkcja=NASZA; break;  
    }  
  Wynik=Funkcja(Liczba);      // Wywolanie wybranej funkcji  
  printf("\n\nWYNIK = %lf", Wynik);  
  
  getch();  
  return 0;  
}  
  
double NASZA(double a)  
{  
  printf("\n A TO NASZA PRYWATNA FUNKCJA\n");  
  if (a!=0) a=1/a; else printf("???\n"); 
  return a;  
}  
 

main() - FUNKCJA SPECJALNA.  

 
Ta książka siłą rzeczy, ze względu na swoją skromną objętość i  
skalę zagadnienia o którym traktuje (autor jest zdania, że język 
 
C to cała filozofia nowoczesnej informatyki "w pigułce") pełna  
jest skrótów. Nie możemy jednak pozostawić bez, krótkiego  
choćby, opisu pomijanego dyskretnie do tej pory problemu  
PRZEKAZANIA PARAMETRÓW DO PROGRAMU.  
 
Konwencja funkcji w języku C/C++ wyraźnie rozgranicza dwa różne  
punkty widzenia. Funkcja pozwala na swego rodzaju separację  
świata wewnętrznego (lokalnego, własnego) funkcji od świata  
zewnętrznego. Nie zdziwi Cię więc zapewne, że i sposób widzenia  
parametrów przekazywanych programowi przez DOS i sposób widzenia 
 
"od wewnątrz" argumentów pobierabych przez funkcję main() jest  
diametralnie różny.  
 
To, co DOS widzi tak:  
 
PROGRAM PAR1 PAR2 PAR3 PAR4 PAR5 [...][Enter]  
 
funkcja main() widzi tak:  
 

145

background image

main(int argc, char **argv, char **env)  
 
lub tak:  
 
main(int argc, char *argv[], char *env[])  
 

CO TO JEST ???  

________________________________________________________________ 
Zapisane zgodnie z obyczajami stosowanymi w prototypach funkcji: 
 
 int argc - liczba całkowita (>=1, bo parametr Nr 1 to nazwa  
samego programu, za pośrednictwem której DOS wywołuje funkcję  
main). Liczba argumentów - parametrów może być zmienna.  
 
UWAGA: Język programowania wsadowego BPL przyjmuje nazwę  
programu za parametr %0 a C++ uznaje ją za parametr o numerze  
argv[0], tym niemniej, nawet jeśli nie ma żadnych parametrów  
argc = 1.  
 
argv - to tablica zawierająca wskaźniky do łańcuchów tekstowych  
reprezentowanych w kodzie ASCIIZ - nazw kolejnych paramentrów, z 
 
którymi został wywołany program.  
Pierszy element tej tablicy to nazwa programu. Ostatni element  
tej tablicy, o numerze argv - 1 to ostatni niezerowy parametr  
wywołania programu.  
 
env - to także tablica zawierająca wskaźniki do łańcuchów  
znakowych w kodzie ASCIIZ reprezentujących parametry środowiska  
(environment variables). Wskaźnik o wartości NUL sygnalizuje  
koniec tablicy. W Turbo C++ istnieje także predefiniowana  
zmienna globalna (::), przy pomocy której można uzyskać dostęp  
do środowiska operacyjnego - environ .  
________________________________________________________________ 
 

Przykłady poniżej przedstawiają sposób wykorzystania parametrów 
wejściowych programu.  

 

[P053.CPP]  

 
# include "stdio.h" 
# include "stdlib.h"  
 
main(int argc, char *argv[], char *env[])  

printf("Parametry srodowiska DOS: \n");  
  int i = 0;  
  do  
    {  
       printf("%s \n", env[i]);   
    i++;  
    }; 
  while (env[i] != NULL);  
printf("Lista parametrow programu: \n"); 
  for(i=1; i<= argc - 1; i++)  

146

background image

    printf("%s \n", argv[i]); 
printf("Nazwa programu: \n"); 
printf("%s", argv[0]); 
return 0; 
}  
 
Ponieważ C++ traktuje nazwę tablicy i wskaźnik do tablicy w  
specjalny sposób, następujące zapisy są równoważne:  
 
*argv[]  oraz  **argv  
*env[]   oraz  **env  
 
Nazwy argumentów argc, argv i env są zastrzeżone i muszą  
występować zawsze w tej samej kolejności. Argumenty nie muszą  
występować zawsze w komplecie. Dopuszczalne są zapisy:  
 
main(int argc, char **argv, char **env) 
main(int argc, char *argv[])  
main(int argc)  
main()  
 
ale niedopuszczalny jest zapis:  
 
main(char *env[])  
 
Nawet jeśli nie zamierzamy wykorzystać "wcześniejszych"  
parametrów - MUSIMY JE PODAĆ. 
 
 

Zadania

________________________________________________________________ 
1. Spróbuj tak zmodyfikować funkcję Demo(), by liczba w formie  
dwójkowej była pisana "od tyłu". Do cofania kursora w funkcji  
printf użyj sekwencji \b\b.  
 
2. Zinterpretuj zapis:  
if (MIANOWNIK) printf("%f", 1/MIANOWNIK); else exit(1);  
 
3 Spróbuj przeprowadzić rzutowanie typu we własnym programie. 
 
4 Przekaż wartość w programie przykładowym posługując się  
instrukcją:  
return (10*Argument + Argument);  
5 Rozszerz zestaw funkcji do wyboru w programie przykładowym.  

LEKCJA 16 - ASEMBLER TASM i BASM.  

________________________________________________________________ 
W trakcie tej lekcji:  
* dowiesz się , jak łączyć C++ z assemblerem  
* poznasz wewnętrzne formaty danych  
________________________________________________________________ 
 

147

background image

WEWNĘTRZNY FORMAT DANYCH I WSPÓŁPRACA Z ASSEMBLEREM.  

 
W zależności od wybranej wersji kompilatora C++ zasady  
współpracy z asemblerem mogą się trochę różnić. Generalnie,  
kompilatory współpracują z tzw. asemblerami in-line (np. BASM),  
lub asemblerami zewnętrznymi (stand alone assembler np. MASM,  
TASM). Wstawki w programie napisane w assemblerze powinny zostać 
 
poprzedzone słowem asm (BORLAND/Turbo C++), bądź _asm (Microsoft 
 
C++). Przy kompilacji należy zatem stosownie do wybranego  
kompilatora przestrzegać specyficznych zasad współpracy. Np. dla 
 
BORLAND/Turbo C++ można stosować do kompilacji BCC.EXE/TCC.EXE  
przy zachowaniu warunku, że TASM.EXE jest dostępny na dysku w  
bieżącym katalogu.  
 
Typowymi sposobami wykorzystania assemblera z poziomu C++ są:  
 
* umieszczenie ciągu instrukcji assemblera bezpośrednio w  
  źródłowym tekście programu napisanym w języku C/C++,  
* dołączeniu do programu zewnętrznych modułów (np. funkcji)  
  napisanych w assemblerze. 
 
W C++ w tekście źródłowym programu blok napisany w asemblerze  
powinien zostać poprzedzony słowem kluczowym asm (lub _asm):  
 
# pragma inline  
 
void main()   
{   
        asm mov dl, 81  
        asm mov ah, 2   
        asm int 33   
}   
  
Program będzie drukował na ekranie literę "Q" (ASCII 81).  
 

JAK POSŁUGIWAĆ SIĘ DANYMI W ASEMBLERZE. 

 
Napiszemy w asemblerze program drukujący na ekranie napis "tekst 
 
- test". Rozpczynamy od zadeklarowania łańcucha znaków: 
 
void main()  
{  
   char *NAPIS = "tekst - test$";     /* $ - ozn. koniec */ 
 
Umieściliśmy w pamięci łańcuch, będący w istocie tablicą  
składającą się z elementów typu char. Wskaźnik do łańcucha może  
zostać zastąpiony nazwą-identyfikatorem tablicy. Zwróć uwagę, że 
 
po łańcuchu znakowym dodaliśmy znak '$'. Dzięki temu możemy  
skorzystać z DOS'owskiej funkcji nr 9 (string-printing DOS  
service 9). Możemy utworzyć kod w asemblerze:  
 
asm mov dx, NAPIS  
asm mov ah, 9  

148

background image

asm int 33  
 
Cały program będzie wyglądał tak:  
 

[P054.CPP]  

 
# pragma inline  
void main()  
{  
  char *NAPIS = "\n tekst - test $";  
  
  asm {  
       MOV DX, NAPIS  
       MOV AH, 9  
       INT 33 
      }  

  
Zmienna NAPIS jest pointerem i wskazuje adres w pamięci, od  
którego rozpoczyna się łańcuch znaków. Możemy przesłać zmienną  
NAPIS bezpośrednio do rejestru i przekazać wprost przerywaniu  
Int 33. Program assemblerowski (tu: TASM) mógłby wyglądać np.  
tak:  
 

[P055.ASM]  

 
      .MODEL SMALL      ;To zwylke robi TCC 
      .STACK 100H       ;TCC dodaje standardowo 4K 
      .DATA  
NAPIS DB     'tekst - test','$'  
      .CODE  
START:        
       MOV AX, @DATA  
       MOV DS, AX       ;Ustawienie segmentu danych 
ASM:  
       MOV DX, OFFSET NAPIS  
       MOV AH, 9  
       INT 21H          ;Drukowanie 
KONIEC:  
       MOV AH, 4CH  
       INT 21H          ;Zakończenie programu 
       END START  
 
Inne typy danych możemy stosować podobnie. Wygodną taktyką jest  
deklarowanie danych w tej części programu, która została  
napisana w C++, aby inne fragmenty programu mogły się do tych  
danych odwoływać. Możemy we wstawce asemblerowskiej odwoływać  
się do tych danych w taki sposób, jakgdyby zostały zadeklarowane 
 
przy użyciu dyrektyw DB, bądź DW.  
 

WEWNĘTRZNE FORMATY DANYCH W C++.  

 

149

background image

LICZBY CAŁKOWITE typów char, short int i long int.  

 
Liczba całkowita typu short int stanowi 16-bitowe słowo i może  
zostać zastosowana np. w taki sposób:  
 

[P056.CPP]  

 
#pragma inline 
void main()  
{  
  char *napis = "\nRazem warzyw:  $";  
  int marchewki = 2, pietruszki = 5;  
  asm {  
        MOV     DX, napis  
        MOV     AH, 9  
        INT     33  
        MOV     DX, marchewki  
        ADD     DX, pietruszki  
        ADD     DX, '0'  
        MOV     AH, 2  
        INT     33  
      }  
}  
 
Zdefiniowaliśmy dwie liczby całkowite i łańcuch znaków - napis.  
Ponieważ obie zmienne (łańcuch znków jest stałą) mają długość  
jednego słowa maszynowego, to efekt jest taki sam, jakgdyby  
zmienne zostały zadeklarowane przy pomocy dyrektywy asemblera DW 
 
(define word). Możemy pobrać wartość zmiennej marchewki do  
rejestru instrukcją  
 
MOV DX, marchewki         ;marchewki -> DX 
 
W rejestrze DX dokonujemy dodawania obu zmiennych i wyprowadzamy 
 
na ekran sumę, posługując się funkcją 2 przerywania DOS 33  
(21H).  
 
W wyniku działania tego programu otrzymamy na ekranie napis:  
 
Razem warzyw:  7 
 
Jeczsze jeden szczegół techniczny. Ponieważ stosowana funkcja  
DOS pracuje w trybie znakowym i wydrukuje nam znak o kodzie  
ASCII przechowywanym w rejestrze, potrzebna jest manipulacja:  
 
ADD DX, '0'   ;Dodaj kod ASCII "zera" do rejestru  
 
Możesz sam sprawdzić, że po przekroczeniu wartości 9 przez sumę  
wszystko się trochę skomplikuje (kod ASCII zera - 48). Z równym  
skutkiem możnaby zastosować rozkaz  
 
ADD DX, 48 
 
Jeśli prawidłowo dobierzemy format danych, fragment programu  
napisany w asemblerze może korzystać z danych dokładnie tak  
samo, jak każdy inny fragment programu napisany w C/C++. Możemy  

150

background image

zastosować dane o jednobajtowej długości (jeśli drugi, pusty  
bajt nie jest nam potrzebny). Zwróć uwagę, że posługujemy się w  
tym przypadku tylko "połówką" rejestru DL (L - Low - młodszy).  
 

[P057.CPP] 

 
#pragma inline 
void main()  
{  
    const char *napis = "\nRazem warzyw:  $";  
    char marchewki = 2, pietruszki = 5;  
    asm {  
        MOV     DX, napis  
        MOV     AH, 9  
        INT     33  
        MOV     DL, marchewki 
        ADD     DL, pietruszki 
        ADD     DL, '0'  
        MOV     AH, 2  
        INT     33  
      } 
}  
 
W tej wersji zadeklarowaliśmy zmienne marchewki i pietruszki  
jako zmienne typu char, co jest równoznaczne zadeklarowaniu ich  
przy pomocy dyrektywy DB.  
 
Zajmijmy się teraz maszynową reprezentacją liczb typu unsigned  
long int (długie całkowite bez znaku). Ze względu na specyfikę  
zapisu danych do pamięci przez mikroprocesory rodziny Intel  
80x86 długie liczby całkowite (podwójne słowo - double word) np. 
 
12345678(hex) są przechowywane w pamięci w odwróconym szyku.  
Zamieniony miejscami zostaje starszy bajt z młodszym jak również 
 
starsze słowo z młodszym słowem. Liczba 12345678(hex) zostanie  
zapisana w pamięci komputera IBM PC jako 78 56 34 12.  
 
Gdy inicjujemy w programie zmienną   
 
long int x = 2;  
 
zostaje ona umieszczona w pamięci tak:   02 00 00 00 (hex). 
Młodsze słowo (02 00) jest umieszczone jako pierwsze. To właśnie 
 
słowo zawiera interesującą nas informację, możemy wczytać to  
słowo do rejestru rozkazem  
 
MOV DX, X  
 
Jeśli będzie nam potrzebna druga połówka zmiennej - starsze  
słowo (umieszczone w pamięci jako następne), możemy zastosować  
pointer (czyli podać adres następnego słowa pamięci).  
 

[P058.CPP]  

 
# pragma inline 

151

background image

void main()  
{  
    unsigned long marchewki = 2, pietruszki = 5; 
    const char *napis = "\nRazem warzyw:  $";  
    asm  
      {  
        MOV     DX, napis  
        MOV     AH, 9  
        INT     33  
        MOV     DX, marchewki 
        ADD     DX, pietruszki 
        ADD     DX, '0'  
        MOV     AH, 2  
        INT     33  
      }  
}  
 
W przypadku liczb całkowitych ujemnych C++ stosuje zapis w  
kodzie komplementarnym. Aby móc manipulować takimi danymi każdy  
szanujący się komputer powinien mieć możliwość stosowania liczb  
ujemnych.  
 
Najstarszy bit w słowie, bądź bajcie (pierwszy z lewej) może  
spełniać rolę bitu znakowego. O tym, czy liczba jest ze znakiem, 
 
czy też bez decyduje wyłącznie to, czy zwracamy uwagę na ten  
bit. W liczbach bez znaku, obojętnie, czy o długości słowa, czy  
bajtu, ten bit również jest (i był tam zawsze!), ale  
traktowaliśmy go, jako najstarszy bit nie przydając mu poza tym  
żadnego szczególnego znaczenia. Aby liczba stała się liczbą ze  
znakiem - to my musimy zacząć ją traktować jako liczbę ze  
znakiem, czyli zacząć zwracać uwagę na ten pierwszy bit.  
Pierwszy, najstarszy bit liczby ustawiony do stanu 1 będzie  
oznaczać, że liczba jest ujemna - jeśli zechcemy ją potraktować  
jako liczbę ze znakiem.  
 
Filozofia postępowania z liczbami ujemnymi opiera się na  
banalnym fakcie: 
 
  (-1) + 1 = 0  
 
Twój PC "rozumuje" tak: -1 to taka liczba, która po dodaniu 1  
stanie się 0. Czy można jednakże wyobrazić sobie np.  
jednobajtową liczbę dwójkową, która po dodaniu 1 da nam w  
rezultacie 0 ? Wydawałoby się, że w dowolnym przypadku wynik  
powinien być conajmniej równy 1.  
 
A jednak. Jeśli ograniczymy swoje rozważania do ośmiu bitów  
jednego bajtu, może wystąpić taka, absurdalna tylko z pozoru  
sytuacja. Jeśli np. dodamy 255 + 1 (dwójkowo 255 = 11111111):  
 
              1111 1111    hex    FF    dec    255 
                 +    1          + 1          +  1 
            ___________         _____        _____ 
            1 0000 0000          100           256 
  
 
otrzymamy 1 0000 0000 (hex 100). Dla Twojego PC oznacza to, że w 
 
ośmiobitowym rejestrze pozostanie 0000 0000 , czyli po prostu 0. 

152

background image

 
Nastąpi natomiast przeniesienie (carry) do dziewiątego (nie  
zawsze istniejącego sprzętowo bitu). 
 
Wystąpienie przeniesienia powoduje ustawienie flagi CARRY w  
rejestrze FLAGS. Jeśli zignorujemy flagę i będziemy brać pod  
uwagę tylko te osiem bitów w rejestrze, okaże się, że  
otrzymaliśmy wynik 0000 0000. Krótko mówiąc FF = (-1), ponieważ  
FF + 1 = 0.  
 
Aby odwrócić wszystkie bity bajtu, bądź słowa możemy w  
asemblerze zastosować instrukcję NOT. Jeśli zawartość rejestru  
AX wynosiła np. 0000 1111 0101 0101 (hex 0F55), to instrukcja  
NOT AX zmieni ją na 1111 0000 1010 1010 (hex F0AA). Dokładnie  
tak samo działa operator bitowy ~_AX w C/C++. W zestawie  
rozkazów mikroprocesorów rodziny Intel 80x86 jest także  
instrukcja NEG, powodująca zamianę znaku liczby (dokonując  
konwersji liczby na kod komplementarny). Instrukcja NEG robi to  
samo, co NOT, ale po odwróceniu bitów dodaje jeszcze jedynkę.  
Jeśli rejestr BX zawierał 0000 0000 0000 0001 (hex 0001), to po  
operacji NEG AX zawartość rejestru wyniesie 1111 1111 1111 1111  
(hex FFFF).  
 

Zastosujmy praktycznie uzupełnienia dwójkowe przy współdziałaniu asemblera 
z C++:  

 

[P059.CPP]  

 
#pragma inline 
void main()  
{  
  const char *napis = "\nRazem warzyw:  $";  
  int marchewki = -2, pietruszki = 5;  
  asm {  
        MOV     DX, napis  
        MOV     AH, 9  
        INT     33  
        MOV     DX, marchewki   
        NEG     DX 
        ADD     DX, pietruszki   
        ADD     DX, '0'  
        MOV     AH, 2  
        INT     33  
      }  
}  
 
Dzięki zamianie (-2) na 2 przy pomocy instrukcji NEG DX  
otrzymamy wynik, jak poprzednio równy 7.  
 

Przypomnijmy prezentację działania operatorów bitowych C++.  

Wykorzystaj program przykładowy do przeglądu bitowej  
reprezentacji liczb typu int (ze znakiem i bez).  
 

153

background image

[P060.CPP]  

 
/* Program prezentuje format liczb i operatory bitowe */  
  
# include "iostream.h"  
# pragma inline  
  
void demo(int liczba)          //Definicja funkcji 
{   
  int n = 15;   
  for (; n >= 0; n--)   
      if ((liczba >> n) & 1)   
   cout << "1";   
      else   
   cout << "0";   
}   
  
char odp;   
char *p = "\nLiczby rozdziel spacja $";   
  
int main()   
{   
 int x, y;   
  
 cout ˙<< "\nPodaj dwie liczby calkowite od -32768 do +32767\n"; 
 
 asm {  
      mov dx, p  
      mov ah, 9  
      int 33  
      }  
 cout << "\nPo podaniu drugiej liczby nacisnij [Enter]";   
 cout << "\nLiczby ujemne sa w kodzie dopelniajacym";   
 cout << "\nSkrajny lewy bit oznacza znak 0-Plus, 1-Minus";  
  
for(;;)  
  {  
    cout << "\n";   
    cin >> x >> y;   
    cout << "\nX:      "; demo(x);   
    cout << "\t\tY:    "; demo(y);   
    cout << "\n~X:     "; demo(~x);   
    cout << "\t\t~Y:   "; demo(~y);  
    cout << "\nX & Y:  "; demo(x & y);   
    cout << "\nX | Y:  "; demo(x | y);   
    cout << "\nX ^ Y:  "; demo(x ^ y);   
    cout << "\n Y:     "; demo(y);   
    cout << "\nY >> 1: "; demo(y >> 1);   
    cout << "\nY << 2: "; demo(y << 2);   
  
    cout << "\n\n Jeszcze raz?  T/N: ";   
    cin >> odp;   
    if (odp!='T'&& odp!='t') break;  
  }   
}   
  
Wstawka asemblerowa nie jest w programie niezbędna, ale w tym  
miejscu wydaje się być "a propos". Przy pomocy programu  
przykładowego możesz zobaczyć "na własne oczy" jak wygląda  
reprezentacja bitowa liczb całkowitych i ich kody  

154

background image

komplementarne.  
 
Praca bezpośrednio ze zmiennymi jest jednym ze sposobów  
komunikowania się z programem napisanym w C++. Mogą jednak  
wystąpić sytuacje bardziej skomplikowane, kiedy to nie będziemy  
znać nazwy zmiennej, przekazywanej do funkcji. Jeśli napiszemy w 
 
asemblerze funkcję w celu zastąpienia jakiejś funkcji  
bibliotecznej C++ , program wywołując funkcję przekaże jej  
parametry i będzie oczekiwał, iż funkcja pobierze sobie te  
parametry ze stosu. Rozważmy się to zagadnienie dokładniej.  
Typową sytuacją jest pisanie w asemblerze tylko kilku funkcji  
(zwykle takich, które powinny działać szczególnie szybko). Aby  
to zrobić, musimy nauczyć się odczytywać parametry, które  
program przekazuje do funkcji w momencie jej wywołania.  
Zaczynamy od trywialnej funkcji, która nie pobiera w momencie  
wywołania żadnych parametrów. W programie może to wyglądać np.  
tak:  
  

[P061.CPP]   

 
//*TEKST to znany funkcji zewnętrzny wskaźnik 
 
#pragma inline  
 
char *TEKST = "\ntekst - test$";  
  
void drukuj(void);         //Prototyp funkcji  
  
void main()   
{   
      drukuj();            //Wywołanie funkcji drukuj()  
}  
  
void drukuj(void)          //Definicja funkcji  
{  
  asm MOV     DX, TEKST  
  asm MOV     AH, 9   
  asm INT     33   
}   
  
Funkcja może oczywiście nie tylko zgłosić się napisem, ale także 
 
zrobić dla nas coś pożytecznego. W kolejnym programie  
przykładowym czyścimy bufor klawiatury (flush), co czasami się  
przydaje, szczególnie na starcie programów. 
 

[P062.CPP]  

  
# pragma inline   
  
char *TEKST = "\nBufor klawiatury PUSTY. $";   
   
void czysc_bufor();          //Też prototyp funkcji  
  
void main()    
{    

155

background image

  czysc_bufor();       //Czyszczenie bufora klawiatury  
}   
   
void czysc_bufor(void)       //Definicja funkcji   
{   
START:  
  asm MOV AH, 11   
  asm INT 33   
  asm OR AL, AL   
  asm JZ KOMUNIKAT   
  asm MOV AH, 7   
  asm INT 33   
  asm JMP START   
KOMUNIKAT:   
  asm MOV DX, TEKST  
  asm MOV AH, 9  
  asm INT 33  
}  
  
Póki nie wystąpi problem przekazania parametrów, napisanie dla  
C++ funkcji w asemblerze jest banalnie proste. Zwróć uwagę, że  
zmienne wskazywane w programach przez pointer *TEKST zostały  
zadeklarowane poza funkcją main() - jako zmienne globalne.  
Dzięki temu nasze funkcje drukuj() i czysc_bufor() mają dostęp  
do tych zmiennych.  
 
Spróbujemy przekazać funkcji parametr. Nazwiemy naszą funkcję  
wyswietl() i będziemy ją wywoływać przekazując jej jako argument 
 
znak ASCII przeznaczony do wydrukowania na ekranie:  
wyswietl('A'); . Pojawia się zatem problem - gdzie program  
"pozostawia" argumenty przeznaczone dla funkcji przed jej  
wywołaniem? W Tabeli poniżej przedstawiono w skrócie "konwencję  
wywoływania funkcji" (ang. Function Calling Convention) języka  
C++.  
 

Konwencje wywołania funkcji.  

________________________________________________________________ 
 
Język    Argumenty na stos  Postać         Typ wart. zwrac.  
________________________________________________________________ 
 
BASIC    Kolejno            offset adresu    Return n  
C++      Odwrotnie          wartości         Return 
Pascal   Kolejno            wartości         Return n  
________________________________________________________________ 
 
Return n oznacza liczbę bajtów zajmowanych łącznie przez  
wszystkie odłożone na stos parametry.  
 
W C++ parametry są odkładane na stos w odwróconej kolejności.  
Jeśli chcemy, by parametry zostały odłożone na stos kolejno,  
powinniśmy zadeklarować funkcję jako "funkcję z Pascalowskimi  
manierami" - np.:  
 
pascal void nazwa_funkcji(void); 
 
Dodatkowo, w C++ argumenty są przekazywane poprzez swoją  
wartość, a nie przez wskazanie adresu parametru, jak ma to  

156

background image

miejsce np. w BASICU. Istnieje tu kilka wyjątków przy  
przekazywaniu do funkcji struktur i tablic - bardziej  
szczegółowo zajmiemy się tym w dalszej części książki.  
 
Rozbudujemy nasz przykładowy program w taki sposób, by do  
funkcji były przekazywane dwa parametry - litery 'A' i 'B'  
przeznaczone do wydrukowania na ekranie przez funkcję: 
 
# pragma inline 
void wyswietl(char, char);        //Prototyp funkcji 
 
void main()  
{  
  wyswietl('A', 'B');             //Wywolanie funkcji 

 
void wyswietl(char x, char y)    //Definicja (implementacja) 
{  
.... 
 
Parametry zostaną odłożone na stos: 
 
PUSH 'B' 
PUSH 'A'  
 
Każdy parametr (mimo typu char) zajmie na stosie pełne słowo.  
C++ nie potrafi niestety układać na stosie bajt po bajcie.  
Funkcja wyswietl() musi uzyskać dostęp do przekazanych jej  
argumentówów. Odwołamy się do zmiennych C++ w taki sposób, jak  
robiłaby to każda inna funkcja w C++:  
 

[P063.CPP]  

  
# pragma inline  
void wyswietl(char, char);        //Prototyp funkcji  
void main()   
{   
 _AH = 2;                        //BEEEEE ! 
 wyswietl('A', 'B');             //Wywolanie funkcji  
}  
  
void wyswietl(char x, char y)    //Definicja (implementacja)  
{  
 _DH = 0;        // To C++ nie TASM, to samo, co asm MOV DH, 0  
 _DL = x;        //             asm MOV DL, x    
 asm INT 33   
 _DH = 0;        //             asm MOV DH, 0  
 _DL = y;        //             asm MOV DL, y     
 asm INT 33   

 
Aby pokazać jak dalece BORLAND C++ jest elastyczny wymieszaliśmy 
 
tu w jednaj funkcji instrukcje C++ (wykorzystując pseudozmienne) 
 
i instrukcje assemblera. Może tylko przesadziliśmy trochę  
ustawiając rejestr AH - numer funkcji DOS dla przerywania int 33 
 
przed wywołaniem funkcji wyswietl() w programie głównym. To  

157

background image

brzydka praktyka (ozn. //BEEEE), której autor nie zaleca.  
Jak widzisz, przekazanie parametrów jest proste.  

LEKCJA 17: TROCHĘ SZCZEGÓLÓW TECHNICZNYCH.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się więcej o szczegółach działania  
komputera widzianych z poziomu assemblera.  
________________________________________________________________ 
 

LICZBY ZMIENNOPRZECINKOWE TYPU float.  

 
To, że C++ przy wywołaniu funkcji jest "przyzwyczajony" do  
odkładania argumentów na stos zawsze po dwa bajty może nam  
sprawić trochę kłopotów, gdy zechcemy zastosować argument typu  
float, double, bądź long - znacznie przekraczający długością  
dwubajtowe słowo maszynowe. 
 
# include <....  
.... 
# pragma inline 
void main()  
{  
     float liczba = 3.5;  
....  
 
Jeżeli zajrzymy do pamięci naszego PC, okaże się, że liczba 3.5  
została tam "zaszyfrowana" jako 00 00 60 40. Dlaczego? Format  
liczb zmiennoprzecinkowych jest znacznie bardziej skomplikowany. 
 
Liczba dziesiętna w rodzaju 123.4 to  
 
1*102 + 2*101 + 3*100  + 4*10-1  
 
{* !UWAGA SKLAD tu cyfry potegi wyzej *} 
 
Ale PC może posługiwać się wyłącznie zerami i jedynkami, i  
liczyć wyłącznie w systemie dwójkowym. Liczbę dziesiętną 3.5  
możnaby przedstawić dwójkowo np. tak:  
 
1*21 + 1*20 + 1*2-1 = 2 + 1 + 1/2    {* !UWAGA SKLAD: potegi *} 
 
czyli 0000 0000 0000 0011.1000 0000 0000 0000  
 
Kropka oznacza przecinek oddzielający część całkowitą od części  

158

background image

ułamkowaj - "przecinek dwójkowy" (a nie dziesiętny!). Każdą  
liczbę dziesiętna można zamienić na liczbę dwójkową.  
Przykładowodzieiętne 7.75 można zamienić na  
 
4 + 2 + 1 + 1/2 + 1/4 = 0000 0000 0000 0111.1100  (dwójkowo)  
 
Pozostaje jednak pewien problem. Komputer nie ma możliwości  
zaznaczenia przecinka, dlatego też przecinek musi być ustawiany  
zawsze w tej samej pozycji - blisko początku liczby. 
 
Liczby zmiennoprzecinkowe są poddawane "normalizacji" (ang.  
noralized). Nasza liczba 0000 0000 0000 0011.1000 po  
normalizacji będzie wyglądać tak: 1.110 0000 0000... * 2^1.  
Odbywa się to zupełnie tak samo, jak normalizacja liczb  
dziesiętnych. Przesunięcie przecinka powoduje, że 12345.67 =  
1.234567 * 10^4. Aby wróciła do swojej starej "zwykłej" postaci  
(jest to tzw. "rozwinięcie" liczby - ang. expand) należy  
przesunąć przecinek o jedno miejsce w prawo - otrzymamy znowu  
11.1 . W liczbach dziesiętnych pierwsza cyfra może być różna  
(tylko nie zero), a w dowolnej poddanej normalizacji  
zmiennoprzecinkowej liczbie dwójkowej pierwszą cyfrą jest zawsze 
 
1. Skoro w formacie liczb zmiennoprzecinkowych pierwsza jedynka  
jest przyjmowana "z definicji" (ang. implicit), więc można ją  
pominąć. Zostanie nam zatem zamiast 1.11 tylko 11 i ta  
przechowywana część liczby jest nazywana jej częścią znaczącą  
(ang. significant). To jeszcze nie wszystko - powinien tam być  
wykładnik potęgi. Wystarczy zapamiętać wykładnik, bo podstawa  
jest zawsze ta sama - 2. Niestety wykładniki są przechowywane  
nie w sposób naturalny, a po dodaniu do nich tzw. przesunięcia  
(ang. offset lub bias). Pozwala to uniknąć kłopotów z  
określaniem znaku wykładnika potęgi. 
 
Dla liczb typu float offset wykładnika wynosi +127 a dla liczb  
double float +1023. Wrócmy do naszej przykładowej liczby. Jeśli  
nasza liczba 3.5 = 11.1(B) ma być zapisana w postaci  
zmiennoprzecinkowej - float, zapisany w pamięci wykładnik potęgi 
 
wyniesie:  
 
1 + 127 = 128 = 80 (hex)  
 
A teraz znak liczby. Pierwszy bit każdej liczby  
zmiennoprzecinkowej określa znak liczby (ang. sign bit). Liczby  
zmiennoprzecinkowe nie są przechowywane w postaci dwójkowych  
uzupełnień. Jeśli pierwszy bit - bit znaku równy jest 1 - liczba 
 
jest ujemna. natomiast jeżeli 0, liczba jest dodatnia. Jest to  
jedyna różnica pomiędzy dodatnimi a ujemnymi liczbami  
zmiennoprzecinkowymi. Nasza liczba 3.5 = 11.1 zostanie  
zakodowana jako:  
 
znak liczby           - 0  
wykładnik potęgi      - 1000 0000  
cyfry znaczące liczby - 110000000....  
 
Ponieważ wiemy, że mamy do dyspozycji dla liczb float 4 bajty  
(możesz to sprawdzić sizeof(float x=3.5)), uzupełnijmy brakujące 
 
do 32 bity zerami:  

159

background image

 
3.5 = 0100 0000 0110 0000 0000 0000 0000 0000 = 40 60 00 00  
 
zapis 40600000 to oczywiście szesnastkowa postać naszej liczby.  
Jeśli teraz weźmiemy pod uwagę, że nasz PC zamieni miejscami  
starsze słowo z młodszym 00 00 40 60 a następnie w obrębie  
każdego słowa dodatkowo starszy bit z młodszym, to zrozumiemy,  
dlaczego nasza liczba "siedziała" w pamięci w zaszyfrowanej  
postaci 00 00 60 40. 
 
Rozpatrzmy szkielet programu wykorzystującego funkcję z "długim" 
 
argumentem. Aby zapanować nad zapisem liczby zmiennoprzecinkowej 
 
do pamięci naszego PC możemy na poziomie assemblera postąpić np. 
 
tak: 
 
 
# include <..... 
# pragma inline  
void funkcja(long int)             //Prototyp funkcji 
main()  
{  
    long liczba = 0xABCDCDEF;      //Deklaracja argumentu 
..... 
    funkcja(liczba);               //Wywołanie w programie 
....  
}  
 
void funkcja(long int x)           //Implementacja funkcji  
{ ..... }                          // x - argument formalny 
  
Argument przekazywany funkcji() jest zmienną 4 - bajtową typu  
long int. Możemy ją zamienić na dwa słowa, zanim przekażemy ją  
do wykorzystania w asemblerowskiej części programu. 
 
funkcja(long int x)  
{  
  int x1starsze, x2mlodsze;   //Wewnętrzne zmienne pomocnicze 
    x2mlodsze = (int) x;                  
    x >> 16; 
    x1starsze = (int) x;           
    _DX = x1starsze;  
    _BX = x2mlodsze; 
    asm {  
       ......                 //Tu funkcja już może działać 
 
Forsując konwersję typu na (int), spowodujemy, że młodsze słowo  
zostanie przypisane zwyczajnej krótkiej zmiennej x2mlodsze.  
Następnie zawartość długiej zmiennej zostanie przesunięta o 16  
bitów w prawo (starsze słowo zostanie przesunięte na miejsce  
młodszego). Powtórzenie operacji przypisania spowoduje  
przypisanie zmiennej x1starsze starszej połówki słowa. Od tej  
chwili możemy odwołać się do tych zmiennych w naszym fragmencie  
napisanym w asemblerze. Postępujemy tak, by to C++ martwił się o 
 
szczegóły techniczne i sam manipulował stosem i jednocześnie  
pilnował poprawności konwersji danych.  
 

160

background image

ZWROT WARTOŚCI PRZEZ FUNKCJĘ.  

 
A teraz kilka słów o tym, co się dzieje, gdy funkcja zapragnie  
zwrócić jakąś wartość do programu. 
  
Wykorzystanie przez funkcje rejestrów do zwrotu wartości.  
________________________________________________________________ 
 
Typ wartości             Funkcja używa rejestru (lub pary) 
________________________________________________________________ 
 signed char / unsigned char AL 
 short                       AX  
 int                         AX  
 enum                        AX 
 long                     para DX:AX (starsze słowo DX, młodsze  
                             AX)  
 float                   AX = Adres  (jeśli far to DX:AX)  
 double                  AX = Adres  (jeśli far to DX:AX)  
 struct                  AX = Adres  (jeśli far to DX:AX)  
 near pointer            AX  
 far pointer             DX:AX  
________________________________________________________________ 
 
 
Zależnie od typu wartości zwracanej przez funkcję (określonej w  
prototypie funkcji), C++ odczytuje zawartość odpowiedniego  
rejestru: AL, AX lub DX:AX. Jeśli funkcja ma np. zwrócić wartość 
 
o długości jednego bajtu, to przed wyjściem z funkcji należy ją  
"zostwić" w rejestrze AL. Jeśli wywołując funkcję C++ oczekuje  
zwrotu wartości jednobajtowej, to po powrocie z funkcji  
automatycznie pobierze bajt z rejestru AL. Krótkie wartości  
(typu short int) są "pozostawiane" przez funkcję w AX, a długie  
w parze rejestrów: DX - starsze, AX - młodsze słowo.  
 
Zastosujmy to w programie. Funkcja będzie odejmować dwie liczby  
całkowite. Pobierze dwa argumenty typu int, wykona odejmowanie i 
 
zwróci wynik typu int (return (_AX)). Dla modelu pamięci small  
będzie to wyglądać tak:  
 

[P064.CPP] 

  
# include <iostream.h>  
# pragma inline  
int funkcja(int, int);            //Prototyp funkcji  
  
void main()   
{   
  cout << "\nWynik  7 - 8 = " << funkcja(7, 8);   
}   
  
int funkcja(int x, int y)          //Implementacja funkcji  
{   
  asm { 
       MOV   AX, x   
       SUB   AX, y   
      } 

161

background image

  return (_AX);          //Zwróć zawartość rejestru AX 
}   
 
Zwróć uwagę, że po return(_AX); stawiamy średnik, natomiast po  
instrukcjach assemblera nie:  
 
asm MOV  AX, DX  
 
chyba, że chcemy umieścić kilka instrukcji assemblera w jednej  
linii (patrz niżej). 
 
C++ i assembler są równoprawnymi partnerami. C++ może odwoływać  
się do zmiennych i funkcji assemblera, jeśli zostały  
zadeklarowane, jako publiczne (public) oraz zewnętrzne  
(EXTeRNal) i vice versa. C++ oczekuje, że zewnętrzne  
identyfikatory będą się rozpoczynać od znaku podkreślenia "_".  
Jeśli w programie pisanym w BORLAND C++ zastosujemy zewnętrzne  
zmienne i funkcje, C++ sam automatycznie doda do identyfikatorów 
 
znak podkreślenia. Turbo Assembler nie robi tego automatycznie i 
 
musimy zadbać o to "ręcznie". Przykładowo, współpraca pomiędzy  
programem P  .CPP i modułem MODUL.ASM będzie przebiegać  
poprawnie:  
 

[P065.CPP]  

 
extern int UstawFlage(void);     //Prototyp funkcji  
int Flaga;  
void main()  
{  
  UstawFlage();  
}  
 
[MODUL.ASM]  
 
               .MODEL   SMALL  
               .DATA  
                EXTRN   _Flaga:WORD  
               .CODE  
                PUBLIC  _UstawFlage  
_UstawFlage   PROC  
                CMP     [_Flaga], 0  
                JNZ     SKASUJ_Flage  
                MOV     [_Flaga], 1 
                JMP     SHORT  KONIEC  
SKASUJ_Flage:   MOV     [_Flaga], 0  
KONIEC:  
                RET  
_UstawFlage     ENDP  
                END  
 
Kompilacja może przebiegać oddzielnie wg schematu:  
 
PROGRAM.CPP --> PROGRAM.OBJ  
MODUL.ASM   --> MODUL.OBJ  
TLINK PROGRAM.OBJ MODUL.OBJ --> PROGRAM.EXE  
 
Lub możemy powierzyć tę pracę kompilatorowi, który sam wywoła  

162

background image

TASM i TLINK: 
 
TCC PROGRAM.CPP MODUL.ASM  
 
W BORLAND C++ 3.1 mamy do dyspozycji zintegrowany assembler  
(ang. in-line) - BASM. Ma on jednak w stosunku do  
"wolnostojącego" Turbo Assemblera pewne ograniczenia:  
 
* ma zawężony w stosunku do TASM zestaw dyrektyw (tylko DB, DD,  
DW, EXTRN); 
* nie pozwala na stosowanie składni typowej dla trybu "Ideal  
mode";  
* nie pozwala na zastosowanie makra; 
* nie pozwala stosować instrukcji charakterystycznych dla 386  
ani 486.  
 
Możesz stosować kilka rozkazów assemblera w jednej linii, ale  
powinieneś rozdzielać je wewnątrz linii średnikami:  
 
asm {  
      POP AX;  POP DX;  POP DS  
      IRET  
    }  
 
Komentarz we wstawce assemblerowskiej musi zostać poprzedzony  
typowym dla C - /* (sam średnik, jak w TASM jest  
niedopuszczalny):  
 
asm {  
      MOV  DX, 1          ;TAK NIE MOŻNA W BASM !  
...  
asm {  
     ADD  AX, BX;         /* Taki komentarz może być */  
 

KŁOPOTY Z REJESTRAMI ?  

________________________________________________________________ 
Jeśli zastosujesz rejestry DI i SI we wstawce assemblerowaj,  
kompilator C++ nie będzie miał gdzie umieścić zmiennych klasy  
register z programu głónego. Zastanów się - co się bardziej  
opłaca.  
________________________________________________________________ 
 

O WEKTORACH PRZERYWAŃ DOS 

 
Mikroprocesory Intel 80X86 rezerwują w pamięci naszych PC  
początkowe 1024 Bajty (adresy fizyczne 00000...00400 hex) na 256 
 
wektorów przerywań (każdy wektor składa się z dwu słów i może  
być traktowany jako DW, bądź far pointer). Następne 256 bajtów  
(00400...00500 hex) zajmuje BIOS, a kolejne 256 (00500...00600  
hex) wykorzystuje DOS i Basic.  
 
Wektor to w samej rzeczy pełny adres początku procedury  
obsługującej przerywanie o danym numerze  
 
UWAGA:  
Wektor zapisywany jest w pamięci w odwrotnej kolejności:  

163

background image

 
Adres pamięci:          0000:0000   [OFFSET Wekt. int 0]  
                        0000:0002   [SEGMENT      int 0]  
                        0000:0004   [OFFSET Wekt. int 1]  
                        0000:0006   [SEGMENT      int 1]  
                        0000:0008   [OFFSET       int 2]  
                           ....          ....  
Procesory 80X86 zamieniają jeszcze dodatkowo starszy bajt z  
młodszym.  
 
Posługując się systemowym debuggerem DEBUG możesz łatwo  
przejrzeć tablicę wektorów przerywań własnego komputera. Jeśli  
wydasz rozkaz:  
 
C:\DOS\DEBUG  
-D 0:0  
 
zobaczysz zawartość pierwszych 32 wektorów int #0...int#31,  
czyli pierwsze 128 bajtów pamięci:  
 
-d 0:0  
  
0000:0000  FB 91 32 00 F4 06 70 00-78 F8 00 F0 F4 06 70 00  
0000:0010  F4 06 70 00 54 FF 00 F0-53 FF 00 F0 53 FF 00 F0  
0000:0020  A5 FE 00 F0 87 E9 00 F0-23 FF 00 F0 23 FF 00 F0  
0000:0030  23 FF 00 F0 CE 02 00 C8-57 EF 00 F0 F4 06 70 00  
0000:0040  D1 0C BD 1B 4D F8 00 F0-41 F8 00 F0 74 07 70 00  
0000:0050  39 E7 00 F0 4A 08 70 00-2E E8 00 F0 D2 EF 00 F0  
0000:0060  00 00 FF FF FB 07 70 00-5D 0C 00 CA 9F 01 BD 1B  
0000:0070  53 FF 00 F0 A0 7C 00 C0-22 05 00 00 2F 58 00 C0  
 
Po zdeszyfrowaniu okaże się, że pierwszy wektor (przerywanie 0)  
wskazuje na adres startowy: 0032:91FB (adres absolutny 0951B).  
Generalnie możliwe są cztery sytuacje. Wektor może wskazywać:  
 
* adres startowy procedur ROM-BIOS: blok F - Fxxx:xxxx,  
* adres funkcji DOS,  
* adres funkcji działającego właśnie debuggera (DEBUG przejmuje  
obsługę niektórych przerywań), lub innego programu rezydującego  
w pamięci - np. NC.EXE, 
* wektor może być pusty - 00 00:00 00 jeśli dane przerywanie nie 
 
jest obsługiwane.  
 
Jeśli zechcesz sprawdzić, jak obsługiwane jest dane przerywanie  
możesz znów zastosować debugger, wydając mu rozkaz  
zdezasamblowania zawartości pamięci począwszy od wskazanego  
adresu:  
 
-u 32:91FB  
  
0032:91FB BE6B47        MOV SI,476B                              
0032:91FE 2E            CS:                                     
0032:91FF 8B1E7E47      MOV BX,[477E]                            
0032:9203 2E            CS:                                     
0032:9204 8E16D73D      MOV SS,[3DD7]                            
0032:9208 BCA007        MOV SP,07A0                              
0032:920B ˙˙˙˙˙˙˙˙˙˙˙E80200        ˙˙˙˙˙˙˙˙˙˙CALL ˙˙˙˙˙˙˙˙˙˙9210 
 
0032:920E EBDA          JMP 91EA                                 

164

background image

0032:9210 ˙˙˙˙˙˙˙˙˙˙˙16            ˙˙˙˙˙˙˙˙˙˙˙PUSH ˙˙˙˙˙˙˙˙˙˙˙SS 
 
0032:9211 07            POP ES                                   
0032:9212 ˙˙˙˙˙˙˙˙˙˙˙16            ˙˙˙˙˙˙˙˙˙˙˙PUSH ˙˙˙˙˙˙˙˙˙˙˙SS 
 
0032:9213 1F            POP DS                                   
0032:9214 C606940308    MOV BYTE PTR [0394],08                   
0032:9219 C606920316    MOV BYTE PTR [0392],16                   
 
Z poziomu assemblera do wektora i odpowiednio do funkcji  
obsługującej przerywanie możesz odwołać się instrukcją INT  
numer.  
 
Zmienna numer może tu przyjmować wartości od 00 do FF. Jeśli  
wydasz taki rozkaz, komputer zapisze na stos (żeby sobie nie  
zapomnieć) zawartość rejestrów CS - bież. segment rozkazu, IP -  
bieżący offset rozkazu i FLAGS. Następnie wykona daleki (far  
jump) skok do adresu wskazanego przez wektor.  
 
Jeśli jednak część przerywań jest "niewykorzystana", lub w Twoim 
 
programie trzeba je obsługiwać inaczej - niestandardowo ? W  
BORLAND C++ masz do dyspozycji specjalny typ funkcji: interrupt. 
 
Aby Twoja funkcja mogła stać się "handlerem" przerywania, możesz 
 
zadeklarować ją tak:  
 
void interrupt MojaFunkcja(bp, di, si, ds .....)  
 
Do funkcji klasy interrupt przekazywane są jako argumenty  
rejestry, nie musisz zatem stosować pseudozmiennych _AX, _FLAGS  
itp.. Jeśli zadeklarujesz funkcję jako handler przy pomocy słowa 
 
"interrupt", funkcja automatycznie zapamiętuje stan rejestrów:  
AX, BX, CX, DX, SI, DI, BP, ES i DS.  
Po powrocie z funkcji rejestry zostaną automatycznie odtworzone. 
 
Przykładem funkcji obsługującej przerywanie może być piszczek()  
posługujący się wbudowanym głośniczkiem i portem:  
 
# define us unsigned 
# include <iostram.h>  
# include <dos.h>  
 
void InstalujWektor(void interrupt (*adres)(), int numer_wekt);  
 
void interrupt Piszczek(us bp, us di, us si, us ds, us es, 
                        us ax, us bx, us cx, us dx);  
 
void main()  
{  
 .....  

  
....  
 
Po zadeklarowaniu prototypów dwu funkcji:  
Piszczek() - nasz handler przerywania;  
InstalujWektor() - funkcja instalująca nasz handler;  

165

background image

możemy przystąpić do zdefiniowania oby funkcji. Posłużymy się  
zmiennymi  
nowe_bity, stare_bity. Wydawanie dźwięku polega na włączaniu i  
wyłączaniu głośniczka. Pusta pętla posłuży nam do zwłoki w  
czasie.  
 
void interrupt Piszczek(us bp, us di, us si, us ds, us es, 
                        us ax, us bx, us cx, us dx)  
{  
  char nowe_bity, stare_bity, i;  
  int n; 
  unsigned char licznik = ax >> 8;  
 
  stare_bity = inportb(0x61);  
 
    for(nowe_bity = stare_bity, n = 0; n <= licznik; n++)  
       {  
         outportb(0x61, 0xFC & nowe_bity);     //Wylacz 
         for(i = 1; i < 255; i++) ;            //Czekaj  
         outportb(0x61, nowe_bity / 2);        //WLACZ 
         for(i = 1; i < 255; i++) ;            //Czekaj  
        }  
  outportb(0x61, stare_bity);                  //Stan poczatkowy 
 
}  
 
Funkcja instalująca handler korzysta z bibliotecznej funkcji C++ 
 
setvect() (ustaw wektor przerywania) i potrzebuje dwu  
argumentów:  
 
* numeru wektora przerywania (numer * 4 = adres),  
* adresu funkcji - handlera - *faddr.  
 
void InstalujWektor(void interrupt (*adres)(), int  
numer_wektora)  
{  
  cout << "\nInstaluje wektor" << numer_wektora << "\n"; 
  setvect(numer_wektora, adres);  
}  
 
Pozostało nam wygenerować przerywanie. Załatwimy to funkcją  
Start():  
 
void Start(unsigned char licznik, int numer_wektora)  
{  
  _AH = licznik;  
  geninterrupt(numer_wektora);        //generuj przerywanie 
}  
 
Nasz główny program będzie zatem wyglądać tak:  
 
# include <...  
...  
void main()  
{  
  Instaluj(Piszczek, 10);  
  Start(5, 10);  
}  
 

166

background image

Należy do dobrych manier odtworzyć po wykorzystaniu oryginalną  
zawartość wektora przerywania, który "unowocześniliśmy". W  
bibliotece BORLAND C++ masz do dyspozycji m. in. funkcje  
 
getvect() - pobierz wektor (ten stary) i  
setvect() - ustaw wektor (ten nasz - nowocześniejszy). 
 
Jeśli zechcemy korzystać z rejestrów 386/486?  
 
Jeśli mamy komputer z 32 bitowymi rejestrami, to wypadałoby z  
tego korzystać. Na poziomie assemblera masz do dyspozycji  
dyrektywy:  
 
.386, .386P i .386C  
(P oznacza pełny zestaw instrukcji wraz z trybem  
uprzywilejowanym - 386 privileged instruction set).  
 
Mikroprocesor Intel 80386 może obsługiwać pamięć zgodnie z  
tradycyjnym podziałem na 64 kilobajtowe segmenty (tryb USE16),  
lub podzieloną na ciągłe segmenty po 4 GB (tryb USE32).  
 
Rejestry ogólnego przeznaczenia rozrosły się z 16 do 32 bitów i  
zyskały w nazwie dodatkową literę E (Extended - rozszerzony).  
"Stare" rejestry stały się młodszą połówką nowych. I tak:  
 
EAX = 0...15 to stary AX, 16...31 to rozbudowa do EAX 
(dokładniej: 0..7 = AL, 8..15 = AH, 0...15 = AX, 0...31 = EAX)  
BX -> 0...31 EBX:  0...7 BL, 8...15 BH, 0...15 BX 
CX -> 0...31 ECX  
DX -> 0...31 EDX  
wszystkie z dodatkowym podziałem na połówki H i L (np.  
DX = DH:DL).  
SI -> 0...31 ESI   w tym (SI = 0..15) 
DI -> 0...31 EDI   w tym (DI = 0..15) 
BP -> 0...31 EBP   w tym (BP = 0..15) 
SP -> 0...31 ESP   w tym (SP = 0..15) 
IP -> 0...31 EIP   w tym (IP = 0..15) 
FLAGS -> 0...31 EFLAGS   w tym (FLAGS = 0..15) 
 
Wszystkie "stare" połówki dostępne pod starą nazwą.  
Rejestry segmentowe pozostały 16 bitowe, ale jest ich o dwa  
więcej: CS, DS, ES, SS oraz nowe FS i GS.  
 
Nowe 32 bitowe rejestry działają według tych samych zasad:  
 
.386 
... 
MOV  EAX, 1     ;zapisz 1 do rejestru EAX  
SUB  EBX, EBX   ;wyzeruj rejestr EBX  
ADD  EBX, EAX   ;dodaj (EAX)+(EBX) --> EBX 
 
Dostęp do starszej połowy rejestru można uzyskać np. poprzez  
przesuwanie (rotation):  
 
.386 
... 
MOV  AX, Liczba_16_bitowa  
ROR  EDX, 16  
MOV  AX, DX  
ROR  EDX, 16  

167

background image

... itp.  
 
W assemblerze możesz stosować wobec procesora 386 nowe  
instrukcje (testowania nie istniejących wcześniej bitów,  
przenoszenia krótkich liczb do 32 bitowych rejestrów z  
uwzględnieniem zaku i uzupełnieniem zerami itp.):  
 
BSF, BSR, BTR, BTS, LFS, LGS, MOVZX, SETxx,  
BT,  BTC, CDQ, CWDE, LSS, MOVSX, SHLD i SHRD.  
 
Przy pomocji instrukcji MOV w trybie uprzywilejowanym (tzw.  
most-privileged level 0 - tylko w trybie .386P) możesz dodatkowo 
 
uzyskać dostęp do specjalnych rejestrów mikroprocesora 80386. 
 
CR0, CR2, CR3,  
DR0, DR1, DR2, DR3, DR6, DR7  
TR6, TR7  
 
Występuje tu typ danych - FWORD - 48 bitów (6 bajtów). Obok  
znanych dyrektyw DB i DW pojawia się zatem nowa DF, a oprócz  
znajomych wskaźników BYTE PTR, WORD PTR pojawia się nowy FWORD  
PTR. Przy pomocy dyrektywy .387 możesz skorzystać z koprocesora. 
 
Jak wynika z zestawu dodatkowych insrukcji:  
 
FCOS, FSINCOS, FUCOMP, FPREM1, FUCOM, FUCOMPP, FSIN  
 
warto dysponować koprocesorem, jeśli często korzystasz z  
grafiki, animacji i funkcji trygonometrycznych (kompilacji nie  
przyspieszy to niestety ani o 10% - tam odbywają się operacje  
stałoprzecinkowe). 
 
Zwróć uwagę, że procesory 386 i wcześniejsze wymagały instalacji 
 
dodatkowego układu 387 zawierającego koprocesor  
zmiennoprzecinkowy. Procesory 486 jeśli mają rozszerzenie DX -  
zawierają już koprocesor wewnątrz układu scalonego. 
_______________________________________________________________ 

LEKCJA 18 - O ŁAŃCUCHACH TEKSTOWYCH  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się,  
* jak manipulować łańcuchami tekstowymi i poznasz kilka  
specjalnych funkcji, które służą w C++ właśnie do takich celów;  
* jak wykonują się operacje plikowo-dyskowe. 
________________________________________________________________ 
 

OPERACJE NA ŁAŃCUCHACH TEKSTOWYCH.  

 
String, czyli łańcuch - to gupa znaków "pisarskich" (liter, cyfr 
 
i znaków specjalnych typu ?, !, _ itp.). Ponieważ C++ nie ma  

168

background image

odzielnego typu danych "string" - łańcuchy znaków to tablice  
złożone z pojedynczych znaków (typowe elementy typu char).  
Techniką obiektową można utworzyć klasę - nowy typ danych  
"string". W bibliotekach Microsoft C++ istnieje predefiniowana  
klasa CString, ale zanim przejdziemy do programowania  
obiektowego i zdarzeniowego - rozważmy manipulowanie tekstami w  
sposób najprostszy.  
 
Maksymalną możliwą długość napisu należy podać wtedy, gdy w  
programie deklaruje się zmienną tekstową: 
  
  char tekst1[40]; 
 
Jest to poprawna deklaracja zmiennej tekstowej o nazwie  
(identyfikator) tekst1. Maksymalna długość tekstu, który można  
umieścić w tej zmiennej tekstowej to - 40 znaków (liter, cyfr,  
itp.). A jeśli chcę zastosować tylko pojedynczy znak zamiast  
całego napisu? To proste: 
 
char napis[1];  
 
Skoro długość łańcucha wynosi 1, to przecież nie jest żaden  
łańcuch! Informacja o długości (size - wielkość) wpisywana w  
nawiasy jest zbędna. Uproszczona wersja utworzenia zmiennej  
jednoznakowej i nadania zmiennej nazwy wygląda w tak:  
 
char znak;  
 
Nie jest to już jednak deklaracja zmiennej łańcuchowej - lecz  
deklaracja zmiennej znakowej. Łańcuch znaków (string) to grupa  
znaków (dokł. tablica znakowa) zakończona zwykle przez tzw.  
"wartownika" - znak NULL (zero). A pojedynczy znak to tylko  
pojedynczy znak. Nie ma tu miejsca (i sensu) dodawanie po  
pojedynczym znaku "wartownika" końca tekstu - zera.  
 
Gdy w deklaracjach zmiennych tekstowych rezerwujesz miejsce w  
pamięci dla napisów - zawsze możesz zażądać od kompilatora C++  
zarezerwowania większej ilości miejsca - na zapas. Zawsze lepiej 
 
mieć zbyt dużo miejsca, niż zbyt mało.  
 

LEPIEJ MIEĆ NIŻ NIE MIEĆ. 

________________________________________________________________ 
 
Upewnij się, czy kompilator zarezerwował (a Ty zadeklarowałeś)  
wystarczająco dużo miejsca dla Twoich tekstów. C++ niestety nie  
sprawdza tego w trakcie działania programu. Jeśli będziesz  
próbował umieścić w pamięci tekst o zbyt dużej długości (dłuższy 
 
niż zadeklarowałeś w programie), C++ posłusznie zapisze go do  
pamięci, ale może to spowodować nieprawidłowe działanie, bądź  
nawet "zawieszenie" programu.  
________________________________________________________________ 
 
Inną przydatną w praktyce programowania cechą języka C++ jest  
możliwość zadeklarowania zawartości zmiennej tekstowej w  
momencie zadeklarowania samej zmiennej. Takie nadanie  
początkowej wartości nazywa się zdefiniowaniem, bądź  
zainicjowaniem zmiennej. W programie zapisuje się to tak:  

169

background image

 
  char napis[] = "To jest jakis napis";  
 
Powoduje to przypisanie zmiennej tekstowej "napis" konkretnego  
łańcucha tekstowego "To jest jakiś napis". Zwróć uwagę, że w  
nawiasach nie podajemy ilości znaków, z których składa się  
tekst. Kompilator sam policzy sobie ilość znaków (tu 19) i  
zarezerwuje miejsce w pamięci dla napisu. Jeśli wolisz sam  
zadecydować, możesz zapisać deklarację tak:  
 
  char napis[35] = "To jest jakis napis";  
 
Jeśli to zrobisz, kompilator C++ zarezerwuje w pamięci miejsce  
dla 35 znaków, a nie dla 19.  
 
W programach często inicjuje się teksty posługując się nie  
tablicą znakową - lesz wskaźnikiem do tekstu. Deklaracja i  
zainicjowanie wskaźnika (wskaźnik wskazuje pierwszy element  
łańcucha znakowego) wygląda wtedy tak:  
 
char *p = "Jakis tam napis";  
 
Rzućmy okiem na kilka gotowych funkcji, które do manipulowania  
tekstami oferuje C++.  
 

ŁĄCZENIE TEKSTÓW.  

  
[S] String Concatenation - łączenie łańcuchów tekstowych. 
Zlepek/skrót. Słowo strcat w języku C++ znaczy sklej.  
 
W praktycznych programach zapewne często pojawi się dwa lub  
więcej tekstów, które trzeba będzie połączyć w jeden napis.  
Wyobraźmy sobie, że imię i nazwisko użytkownika mamy zapisane  
jako dwa oddzielne łańcuchy tekstowe. Aby połączyć te dwa teksty 
 
w jeden trzeba przeprowadzić tzw. sklejanie (ang. concatenation) 
 
tekstów. W języku C++ mamy w tym celu do dyspozycji specjalną  
funkcję:  
 
strcat() - STRing conCATenation - sklejanie łańcuchów.  
 
Aby połączyć dwa łańcuchy tekstowe napis1 i napis2 w jeden  
należy zastosować tę funkcję w taki sposób:  
 
  strcat(napis1, napis2);  
 
Funkcja strcat() zadziała w taki sposób, że łańcuch znaków  
napis2 zostanie dołączony do końca łańcucha napis1. Po  
zakończeniu działania funkcji zmienna napis1 zawiera "swój  
własny" napis i dołączony na końcu napis zawarty uprzednio w  
zmiennej napis2.  
 
Program poniżej przedstawia praktyczny przykład zastosowania  
funkcji strcat().  
 

170

background image

[P066.CPP] 

 
#include <conio.h>  
#include <iostream.h> 
#include <string.h>     //W tym pliku jest prototyp strcat() 
  
int main(void)  
{  
char imie[50], nazwisko[30];  
 
clrscr();  
   cout << "Podaj imie: ";  
   cin >> imie;  
  
   cout << "Podaj nazwisko: ";  
   cin >> nazwisko;  
  
   strcat(imie, " ");     
   strcat(imie, nazwisko);    
  
   cout << "\nNazywasz sie: " << imie << '\n';  
   cout << "Naciśnij dowolny klawisz";  
getch();  
return 0; 
}  
  
 
Program zapyta najpierw o imię a następnie o nazwisko. Po  
wpisaniu przez Ciebie odpowiedzi program doda do siebie oba  
teksty i wypisze na ekranie Twoje imię i nazwisko w całości.  
Interesująxe w programie jest połączenie przy pomocy funkcji C++ 
 
strcat() dwu łańcuchów tekstowych w jeden łańcuch z dodaniem  
spacji rozdzielającej łańcuchy znaków. Najistotniejszy fragment  
programu wraz z komentarzem - poniżej. 
 
   strcat(imie, " "); <-- dodaj do końca tekstu spację 
   strcat(imie, nazwisko);    <-- po dołączonej spacji dodaj  
                               drugi tekst - nazwisko 
 
Ponieważ prototyp funkcji strcat() znajduje się w pliku STRING.H 
 
- należy dołączyć ten plik nagłówkowy dyrektywą #include.  
 

DŁUGOŚĆ ŁAŃCUCHA TEKSTOWEGO.  

 
Każdy tekst ma swoją długość: liczbę znaków, z których się  
składa. Dla przykładu łańcuch znaków:  
 
"Przychodzi katecheta do lekarza i płacze, a lekarz na to: Bóg  
dał - Bóg wziął..."  
 
ma dla długość 71, ponieważ składa się z 71 znaków (odstęp -  
spacja to też znak). Łańcuch znaków  
 
"Ile diabłów mieści się w łebku od szpilki?"  
 
ma długość 42. Teoretycznie długość łańcuchów znakowych może  

171

background image

wynosić od 0 do nieskończoności, ale w Borland/Turbo C++  
występuje ograniczenie: łańcuch znaków może mieć długość zawartą 
 
w przedziale od 0 do 65536 znaków. Taki np. łańcuch znaków jest  
całkiem do przyjęcia: 
 
"Nie ważne, czy Polska będzie bogata, czy biedna - ważne, żeby  
była katolicka (czyli nasza), bo nasze będą wtedy pieniądze,  
urzędy i nasza władza. Lepiej być pół-Bogiem wśród nędzarzy  
(oczywiście za ich pieniądze, z ich podatków), niż zarabiać na  
chleb własną pracą." 
 

Łańcuch zerowy - Null string

________________________________________________________________ 
 
Łańcuch zerowy (dokładniej: łańcuch tekstowy o zerowej długości) 
 
to taki łańcuch, który zawiera 0 (zero) znaków. Jak to możliwe,  
by łańcuch tekstowy zawierał zero znaków? W C++ łańcuchy znaków  
zawierają na końcu znak '\0' (zero) jako "wartownika" końca  
tekstu. Jeśli pierwszy element tablicy znakowej będzie zerem -  
powstanie właśnie łańcuch znakowy o zerowej długości. Można to  
zrobić np. tak:    
 
char napis[0] = 0;  
char *p = "";  
char napis[50] = ""; 
________________________________________________________________ 
 
 
Kiedy C++ wyznacza długość łańcucha znaków - zlicza kolejne  
znaki, aż dojdzie do zera. W przykładzie już pierwszy znak jest  
zerem, więc C++ uzna, że długość takiego łańcucha wynosi zero.  
Czasem w praktyce programowania zainicjowanie takiego pustego  
łańcucha pozwala mieć pewność, że tablica nie zawiera jakichś  
starych, zbędnych danych.  
 
Możliwość sprawdzenia, jaką długość ma łańcuch tekstowy może się 
 
to przydać np. do rozmieszczenia napisów na ekranie. Dla  
przykładu, pozycja na ekranie, od której rozpocznie się  
wyświetlanie napisu zależy od długości tekstu, który został  
wyświetlony wcześniej. Do określania długości tekstu masz w C++  
do dyspozycji gotową funkcję:  
 
strlen() - STRing LENgth - długość łańcucha znakowego.  
 
Funkcję strlen() stosuje się w następujący sposób:  
 
unsigned int dlugosc;  
char tekst[...]; 
 ...  
   dlugosc = strlen(tekst);  
 
Funkcja ma jeden argument - napis, którego długość należy  
określić (tu: zmienna nazywa się tekst). Funkcja strlen() w  
wyniku swojego działania ZWRACA długość łańcucha tekstowego jako 
 
liczbę całkowitą bez znaku (nieujemną). Liczba zwrócona jako  

172

background image

wynik przez funkcję strlen() może zostać użyta w dowolny sposób  
- jak każda inna wartość numeryczna.  
 
Funkcja strlen() nie podaje w odpowiedzi na wywołanie (mądrze  
nazywa się to "zwraca do programu wartość") długości łańcucha  
tekstowego, która została zadeklarowana (maksymalnej  
teoretycznej), lecz FAKTYCZNĄ DŁUGOŚĆ tekstu. Jeśli, dla  
przykładu, zadeklarujemy zmienną tekstową tak:  
 
  char string1[30] = "Lubie C++ ";  
 
zadeklarowana maksymalna długość łańcucha znakowego wynosi 30,  
natomiast faktyczna długość łańcucha znakowego wynosi 10 znaków. 
 
Jeśli wywołamy strlen() i każemy jej określić długość łańcucha  
znakowego string1:  
 
  unsigned int dlugosc = strlen(string1);  
 
funkcja przypisze zmiennej dlugosc wartość 10 a nie 30.  
 
Jeśli wpisałeś poprzedni program program przykładowy do okienka  
edycyjnego - wystarczy dodać dwa nowe wiersze.  
 

[P067.CPP] 

 
#include <conio.h>  
#include <iostream.h> 
#include <string.h>  
  
main()  
{  
   char imie[50], nazwisko[20];  
   int dlugosc;  
 
clrscr();  
   cout << "Podaj imie: ";  
   cin >> imie;  
   cout << "Podaj nazwisko: ";  
   cin >> nazwisko;  
   strcat(imie, " ");  
   strcat(imie, nazwisko);  
   cout << "\nNazywasz sie: " << imie << '\n';  
   dlugosc = strlen(imie); 
   cout<<"Imie i nazwisko sklada sie z: "<<dlugosc<<"znakow\n"; 
   cout << "Nacisnij dowolny klawisz"; 
getch();  
return 0; 
}  
  
W programie z Listingu 5.2 nie musisz stosować dodatkowej  
zmiennej dlugosc. Taki sam efekt uzyskasz pisząc zamiast dwu  
wierszy jeden: 
 
  cout << "Wszystkich znakow bylo: " << strlen(imie) << '\n';  
 

173

background image

POBIERANIE I WYSZUKIWANIE WYCINKA TEKSTU - substring.  

 
Podobnie łatwo do łączenia łańcuchów możesz dokonać podziału  
większych tekstów na mniejsze fragmenty. "Duże" pierwone  
łańcuchy nazywają się "string", a te mniejsze fragmenty -  
"substring". Do podziału łańcuchów na "podłańcuchy" język C++  
dysponuje specjalnymi funkcjami:  
 
strncpy() i strcpy() - STRiNg CoPY - kopiuj string.  
 

Część składowa większego łańcucha znaków - Substring

________________________________________________________________ 
Substring to mniejszy łańcuch znaków stanowiący część większego  
łańcucha znaków. Np. substring BAB jest częścią większego  
łańcucha BABCIA.  
source - źródło (miejsce pochodzenia);  
destination - miejsce przeznaczenia. 
________________________________________________________________ 
 
 
Funkcja strncpy() kopiuje we wskazane miejsce tylko pewną -  
zadaną liczbę początkowych znaków łańcucha. Funkcję strncpy()  
możesz zastosować w swoich programach w następujący sposób:  
 
  char tab_A[80] = "BABCIA"; 
  char tab_B[80] = ""; 
 
  strncpy(tab_B, tab_A, 3);     /* kopiuj 3 pierwsze znaki */  
 
W tym przykładzie wywołujemy funkcję strncpy() przekazując jej  
przy wywołaniu trzy argumenty:  
 
tab_B - destination string - wynikowy łańcuch tekstowy (ten  
          nowy, który powstanie);  
tabn_A - source string - łańcuch źródłowy (ten, z którego  
           będziemy "obcinać" kawałek);  
3 - maksymalna liczba znaków, którą należy obciąć . Obcięte  
      znaki utworzą "substring" - "BAB".  
 
Pobieranie i "wycinanie" znaków rozpocznie się od pierwszego  
znaku łańcucha źródłowego tab_A[80], więc funkcja wywołana w  
taki sposób:  
 
  strncpy(string1, string2, 3);  
 
spowoduje pobranie pierwszych 3 znaków z łańcucha string2 i  
skopiowanie ich do łańcucha string1.  
 
Funkcja strcpy() (Uwaga! bez "n") powoduje skopiowanie całego  
łańcucha znaków. Sposób zastosowania funkcji jest podobny do  
przykładu z strncpy(), z tym, że nie trzeba podawać liczby  
całkowitej określającej ilość znaków do kopiowania. Jak  
wszystkie, to wszystkie (jak mawiała babcia), zatem wywołanie  
funkcji:  
 
  strcpy(string1, string2);  
 
spowoduje skopiowanie całego łańcucha znaków zawartego w  

174

background image

zmiennej string2 do zmiennej string1. Jeśli, dla przykładu,  
zmiennej string2 przypiszemy łańcuch tekstowy  
 
  string2 = "BABCIA";  
 
to po zadziałaniu funkcji strcpy(string1, string2) zmiennej  
string1 zostanie przypisany dokładnie taki sam łańcuch. 
 
Rozważmy program przykładowy. Po uruchomieniu program poprosi o  
wpisanie łańcucha tekstowego. Wpisz dowolny tekst. Tekst  
powinien zawierać więcej niż 3 znaki. Po pobraniu  
wyjściowego/źródłowego tekstu od użytkownika, program pobierze z 
 
tego tekstu kilka mniejszych łańcuchów tekstowych typu  
"substring" i wyświetli je na ekranie.  
 

[P068.CPP] 

 
#include <conio.h>  
#include <iostream.h> 
#include <string.h> 
#include <stdio.h> 
  
main()  
{  
   char napis1[80] = "";    
   char napis2[80] = "";   
   char napis3[80] = "";   
  
   clrscr(); 
   cout << "Wpisz jakis tekst: ";  
   gets(napis1);   
  
   strcpy(napis2, napis1);  
   strncpy(napis3, napis1, 3);  
  
   cout << "\nKopia tekstu: "; 
   cout << '*' << napis2 << "*\n";  
   cout << "Pierwsze 3 znaki tekstu: "; 
   cout << '\'' <<  napis3 << '\'' << '\n';  
  
   cout << "\n\n...dowolny klawisz...";  
   getch(); 
   return 0; 
}  
 

A jeśli zabraknie znaków? 

________________________________________________________________ 
 
Spróbuj uruchomić program podając mu łańcuch tekstowy krótszy  
niż 5 znaków. Jest to próba oszukania funkcji, która oczekuje,  
że kopiowane 3 znaki powinny istnieć, mało tego, powinny być  
zaledwie częścią większego łańcucha. 
Jak widzisz, program nie "zawiesza się". W języku C++ funkcje  
opracowane są zwykle w taki sposób, że nawet otrzymując  
bezsensowne parametry potrafią jakoś tam wybrnąć z sytuacji. Tym 
 

175

background image

niemniej, nawet jeśli program się nie zawiesza, nie oznacza to,  
że wyniki działania przy bezsensownych danych wejściowych będą  
mieć jakikolwiek sens. Jako programista powinieneś wystrzegać  
się takich błędów (dane z poza zakresu, dane bez sensu  
merytorycznego) nie licząc na to, że C++ jakoś z tego wybrnie.  
________________________________________________________________ 
 
Najważniejszy fragment tekstu programu wraz z komentarzem:  
 
   char napis1[80] = "";   <-- deklaracje zmiennych tekstowych  
   char napis2[80] = "";   <-- i nadanie im zerowej zawartości 
   char napis3[80] = "";   <-- długość pustego napisu - zero. 
 ...  
   gets(napis1);         <-- GET String - pobierz string 
   strcpy(napis2, napis1); <-- kopiowanie całego tekstu 
   strncpy(napis3, napis1, 3);   <-- kopiowanie części tekstu 
 ... 
 
Zwróć uwagę, że program do pobrania danych (tekstu) od  
użytkownika posługuje się funkcją gets() (ang. GET String -  
pobierz łańcuch znaków). Obiekt cin jest bardzo wygodnym  
środkiem służącyn do wczytywania danych, ale nie pozwala  
wprowadzać napisów zawierających spacje. Jeśli zastosowalibyśmy  
w programie  
 
cin >> string1;  
 
i wpisali tekst zawierający spacje, np.:  
 
To nie ważne, czy Polska... 
 
wczytane zostałyby tylko znaki To (do pierwszej spacji). Z kolei 
 
funkcja gets() pozwala wczytać wiersz tekstu zawierający dowolne 
 
znaki uznając za koniec znak CRLF (powrót karetki, zmiana  
wiersza) generowany po naciśnięciu [Entera]. Przeciwną,  
symetryczną funkcją do gets() jest funkcja puts() (ang. PUT  
String - wyprowadź wiersz tekstu). Prototypy funkcji gets() i  
puts() znajdują się w pliku nagłówkowym STDIO.H. Dlatego ten  
plik nagłówkowy został dołączony na początku dyrektywą #include. 
 
 

WYSZUKIWANIE TEKSTÓW.  

 
Wyobraźmy sobie, że mamy listę imion i chcemy na tej liście  
odszukać znajome imię np. Alfons. Specjalnie do takich celów C++ 
 
dysponuje funkcją:  
 
strstr() - STRing's subSTRing - część łańcucha tekstowego  
 
Aby wyszukać w większym tekście mniejszy fragment, powinniśmy  
wywołując funkcję przekazać jej dwie informacje:  
 
GDZIE SZUKAĆ - wskazać łańcuch tekstowy do przeszukiwania;  
i  
CZEGO SZUKAĆ - podać ten tekst, który nas interesuje i który  
funkcja powinna dla nas odnaleść.  

176

background image

 
Funkcja strstr(), powinna zatem mieć dwa argumenty:  
   
  char Lista[] = "Adam, Buba, Adolf, Magda";  
  ...  
  gdzie = strstr(Lista, "Adolf");  
 
Funkcja strstr() wyszukuje pierwsze wystąpienie danego tekstu.  
Po wyszukaniu, funkcja powinna nam w jakiś sposób wskazać, gdzie 
 
znajduje się interesujący nas tekst. Jak wiesz, do wskazywania  
różnych interesujących rzeczy służą w C++ WSKAŹNIKI (pointer). W 
 
przykładzie powyżej funkcja strstr() w wyniku swojego działania  
zwraca wskaźnik do szukanego tekstu "Alfons". Aby wskaźnik nam  
nie przepadł, trzeba go zapamiętać. Funkcja zatem przypisuje  
wskaźnik zmiennej "gdzie". W miejscu przeznaczonym dla tej  
zmiennej w pamięci będzie odtąd przechowywany wskaźnik,  
wskazujący nam - gdzie w pamięci kmputera znajduje się  
interesujący nas tekst "Alfons\0".  
 
Aby komputer zarezerwował miejsce w pamięci dla wskaźnika,  
trzeba go o to "poprosić" na początku programu, deklarując, że w 
 
programie zamierzamy posługiwać się wskaźnikiem. Deklaracja  
wskaźnika do zmiennej tekstowej wygląda tak:  
 
  char *wskaznik;  
 
Przykładowy program pniżej demonstruje sposób zadeklarowania  
wskaźnika i wyszukiwanie tekstu. Program nie oczekuje żadnej  
informacji wejściowej od użytkownika. Uruchom program i  
przeanalizuj wydruk na ekranie porównując go z tekstem programu. 
 
 

[P069.CPP] 

 
#include <conio.h>  
#include <iostream.h> 
#include <string.h> 
  
main()   
{   
char string1[] = "Ala, Magda, Adam, Alfons, Jasiek, Alfons, As"; 
 
char *pointer;  
 
   clrscr(); 
   cout << "Lista:\n" << string1;  
 
   pointer = strstr(string1, "Alfons");  
 
   cout << "Tekst 'Alfons' wystapil po raz pierwszy:\n"; 
   cout << "  " << pointer << '\n';  
  
   pointer = strstr(ptr, "Jasiek");  
   cout << "Tekst 'Jasiek' wystapil po raz pierwszy:\n"; 
   cout << "  " << pointer << '\n';  
  

177

background image

   pointer = strstr(pointer, "As");  
   cout << "Tekst 'As' wystapil:\n"; 
   cout << "  " << ptr << '\n' << "\n\nNacisnij cokolwiek"; 
 
   getch(); 
   return 0; 
}  
 
Inną metodą zastosowania funkcji manipulujących łańcuchami  
tekstowymi może być "obróbka" tekstu wprowadzonego przez  
użytkownika. Następny program przykładowy pozwala użytkownikowi  
wprowadzić tekst do przeszukiwania (odpowiednik listy) i tekst  
do wyszukania (odpowiednik imienia). W wyniku wyszukania  
wskazanego łańcucha program wyświetla listę począwszy od  
wyszukanego pierwszego wystąpienia zadanego łańcucha znaków.  
 

[P070.CPP] 

 
#include <conio.h>  
#include <iostream.h>  
#include <string.h> 
#include <stdio.h>  
  
main()  
{  
   char str1[80], str2[80];  
   char *ptr;             
  
   clrscr(); 
   cout << "Wpisz tekst do przeszukania:\n ";  
   gets(str1);  
  
   cout << "Co mam wyszukac?\n--> ";  
   gets(str2);  
  
   ptr = strstr(str1, str2);    <-- wyszukiwanie tekstu 
   cout << "Znalazlem: " << '\'' << str1 << '\'' << " w "; 
   cout << '\'' << str2 << '\'' << '\n'; 
   cout << ptr;  
   cout << "\n\n ...Nacisnij klawisz..."; 
   getch(); 
   return 0; 
}  
 
 

DUŻE I MAŁE LITERY. 

 
Litery mogą być małe i duże. Duże litery nazywają się  
"capitals". Od słowa CAPitalS pochodzi skrót na klawiszu [Caps  
Lock]. Innym, używanym do określenia tego samego słowem jest  
"upper case" (duże litery) lub "lower case" (małe litery).  
Czasami pojawia się potrzeba zaminy dużych liter na małe, bądź  
odwrotnie. W C++ służą tego celu funkcje:  
 
strupr() - STRing to UPpeR case - zamień litery włańcuchu  
             tekstowym na duże.  
strlwr() -  STRing to LoWeR case - zamień litery w łańcuchu na  

178

background image

             małe. 
 
Program przykładowy poniżej demonstruje działanie tych funkcji. 
 

[P071.CPP] 

 
#include <conio.h>  
#include <iostream.h>  
#include <string.h> 
#include <stdio.h>  
  
main()  
{  
   char string1[80];  
 
   clrscr(); 
   cout << "Wpisz tekst do zamiany:\n";  
   gets(string1);  
  
   cout << "\nNormalnie: " << string1 << '\n';  
   cout << "TYLKO DUZE: " << strupr(string1) << '\n';  
   cout << "tylko male: " << strlwr(string1) << '\n';  
   cout << "\n\n...Nacisnij klawisz...";  
   getch(); 
   return 0; 
}  
  
 

DLA DOCIEKLIWYCH.  

________________________________________________________________ 
 
* Argumenty funkcji - zawsze w tej samej kolejności!  
Kiedy wywołujesz gotową funkcję - np. strstr(), argumenty  
funkcji muszą być podane zawsze w tej samej kolejności (tak, jak 
 
funkcja "się spodziewa"). Wywołanie funkcji:  
 
  pointer = strstr(string, substring, 3);  
 
powiedzie się i funkcja zadziała zgodnie z oczekiwaniami.  
Natomiast wywołanie funkcji tak:  
 
  pointer = strstr(3, substring, string);  
 
spowoduje błąd przy kompilacji programu.  
 
* Przy manipulacji stringami kłopoty mogą sprawiać spacje, bądź  
ich brak. Dla przykładu przy sklejaniu dwóch łańcuchów  
tekstowych warto dla czytelności dodać spację, by nie uzyskiwać  
napisów typu: WaldekKowalski. Łatwo można przegapić i inne  
ograniczniki (ang. delimiter). 
 
* Ocena długości tekstu.  
Szczególnie przewidujący i ostrożny musi być programista wtedy,  
gdy łańcuch będzie wprowadzany przez użytkownika programu.  
________________________________________________________________ 

179

background image

LEKCJA 19: KILKA INNYCH PRZYDATNYCH FUNKCJI.  

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się, jak zapisać teksty na dysku i  
jak jeszcze można nimi manipulować przy pomocy gotowych funkcji  
Borland C++.  
________________________________________________________________ 
 
 

Program poniżej demonstruje zastosowanie trzech przydatnych funkcji:  

 

[P072.CPP]  

 
#include <conio.h>  
  
int main(void)  
{  
   int i, x = 0, y = 0;  
   clrscr();  
   for (i = 1; i < 10; i++)  
   {  
y = i;  
x = 5*i;  
textbackground(16-i);  
textcolor(i);  
gotoxy(x, y);  
cprintf("Wspolrzedne: x=%d  y=%d", x, y);  
     getch();  
   }  
   return 0;  
}  
  
textbackground() - ustaw kolor tła pod tekstem  
texcolor() - ustaw kolor tekstu  
gotoxy() - rozpocznij drukowanie tekstu od punktu o  
współrzędnych ekranowych  
x - numer kolumny (w normalnym trybie: 1-80)  
y - numer wiersza (w normalnym trybie: 1-25) 
 

Zadania

________________________________________________________________ 
1. Rozmieść na ekranie napisy i znaki semigraficzne tworzące  
rysunek tabelki.  
2. Opracuj program, w którym pojedyncze znaki, bądź napisy będą  
poruszać się po ekranie.  
3. Spróbuj przyspieszyć działanie swojego programu z  
poprzedniego zadania poprzez wstawkę w assemblerze.  
________________________________________________________________ 
 

180

background image

OPERACJE PLIKOWE - NIEOBIEKTOWO. 

 
W systemia DOS dane i programy są zgrupowane w pliki. Pliki  
(ang. file) mogą być TEKSTOWE i BINARNE. Najczęstszymi  
operacjami na plikach są:  
 
* Utworzenie nowego pliku (ang. CREATE); 
* Odczyt z pliku (ang. READ);  
* Zapis do pliku (WRITE);  
* Otwarcie pliku (OPEN);  
* Zamknięcie pliku (CLOSE);  
* Wyszukanie danej w pliku (SEEK);  
 
W kontaktach z urządzeniami - np. z dyskiem pośredniczą DOS i  
BIOS. To system DOS wie, gdzie na dysku szukać pliku (katalogu)  
o podanej nazwie i w których sektorach dysku znajdują się  
fizycznie dane należące do danego pliku. Operacje z plikami  
opierają się o odwoływanie do systemu operacyjnego za  
pośrednictwem tzw. Deskryptora pliku (File Descriptor - numer  
identyfikacyjny pliku).  
 
Zestaw "narzędzi" potrzebnych nam do pracy to:  
 
IO.H - prototypy funkcji obsługi WEjścia/WYjścia (ang.  
Input/Output=IO);  
 
FCNTL.H - plik zawierający definicje wymienionych poniżej  
stałych:  
O_BINARY - otwarcie pliku w trybie binarnym;  
O_TEXT - otwarcie pliku w trybie tekstowym;  
O_RDONLY (Open for Read Only) - otwórz tylko do odczytu;  
O_WRONLY (...Write Only) - tylko dla zapisu;  
O_RDWR (Reading and Writing) dozwolony zapis i odczyt;  
 
STAT.H - zawiera definicje stałych  
S_IREAD - plik tylko do odczytu (przydatne dla funkcji creat);  
S_IWRITE - tylko zapis (przydatne dla funkcji creat);  
 
FUNKCJE: 
int open(p1, p2, p3) - trójparametrowa funkcja otwierająca plik; 
 
(parametry patrz przykład) zwraca do programu Wynik = -1   
(operacja zakończona niepowodzeniem - np. nie ma pliku)  
lub Wynik = File Descriptor - numer pliku przekazany przez DOS.  
int creat(p1, p2) - funkcja tworząca nowy plik;  
int read(...) - funkcja czytająca z pliku;  
int write(...) - funkcja zapisu do pliku;  
imt close(...) - zamknięcie pliku.  
 
Po uruchomieniu program otwiera automatycznie trzy standardowe  
pliki, związane z urządzeniami:  
0 - stdin - standardowy plik wejściowy (norm. klawiatura  
konsoli);  
1 - stdout - standardowy plik wyjściowy (norm. monitor);  
2 - stderr - standardowy plik wyjściowy - diagnostyczny  
(komunikaty o błędach).  
 
[S] STD...  
STandarD INput - standardowe wejście.  
STD      OUTput - standardowe wyjście.  

181

background image

STD      ERRors - plik diagnostyczny.  
 
//[P072-2.CPP]  
  
# include <stdio.h>   
# include <conio.h>   
# include <SYS\STAT.H>   //Duze litery tylko dla podkreslenia  
# include <FCNTL.H>   
# include <IO.H>   
   
char *POINTER;   
int IL_znakow, DLUG_pliku, TRYB_dostepu, Wynik, i;   
int Plik_1, Plik_2;   
char BUFOR[20] = {"TEKST DO PLIKU"};   
char STOS[3], ZNAK='X';   
   
main()  
{   
POINTER = &BUFOR[0];   
   
printf("Wloz dyskietke do A: i nacisnij cos...\n");  
  
Plik_1 = creat( "a:\\plik1.dat", S_IWRITE);  
  
if (Plik_1 == -1)   
  printf("\n Nie udalo sie zalozyc plik1.dat...");   
  
Plik_2 = creat( "a:\\plik_2.dat", S_IWRITE);   
  if (Plik_2 == -1)  
    printf("\n Klops przy Plik2.dat");  
  
_fmode = O_BINARY;  //Bedziemy otwierac w trybie binarnym   
  
Wynik = open( "a:\\plik1.dat", O_WRONLY );   
  if (Wynik == -1)   
    printf("\n Nie udalo sie otworzyc pliku...");  
  
IL_znakow = 15;    //Ilosc znakow do zapisu   
  
Wynik =write( Plik_1, POINTER, IL_znakow );   
  
printf("Zapisalem %d znakow do pliku.", Wynik);   
  
close( Plik_1 );   
  
Plik_1 = open("a:\\Plik1.dat", O_RDONLY );   
Plik_2 = open("a:\\Plik_2.dat", O_WRONLY );   
  
POINTER = &STOS[0];   
  
 for (i=1; ZNAK; i++)     //Kopiuje plik + spacje  
  {  
    STOS[1] = ZNAK;  
    write( Plik_2, POINTER, 2);  
    read( Plik_1, &ZNAK, 1);  
  }  
  
close(Plik_1); close(Plik_2);   
  
getch();   

182

background image

return 0;  
}   
  
 

Przykładowy program wykonuje następujące czynności:  

 
1. Tworzy plik a:\plik1.dat (potrzebny dostęp do dyskietki a:).  
2. Tworzy plik a:\plik_2.dat.  
3. Otwiera plik a:\plik1.dat w trybie binarnym tylko do zapisu. 
(ZWRÓĆ UWAGĘ, że tryb binarny nie przeszkadza zapisać tekstu.) 
4. Dokonuje zapisu do pliku. 
5. Zamyka plik a:\plik1.dat. 
6. Otwiera plik1.dat w trybie binarnym tylko do odczytu. 
7. Otwiera plik_2.dat tylko do zapisu.  
8. Kopiuje plik1.dat do plik_2.dat dodając spacje.  
 
Zwróć uwagę na konstrukcję:  
 
for(i=1; ZNAK; i++)  
 
Wyjaśnienie. Póki jest znak wykonuj kopiowanie. Przypominam, że  
koniec to NUL - '\0'. 
  
 
Jeśli czytamy i piszemy po kolei - wszystko jest proste. Jeżeli  
natomiast chcemy wyszukać w pliku określone miejsce, to będzie  
nam jeszcze dodatkowo potrzebny mechanizm do określenia pozycji  
w pliku - tzw. WSKAŹNIK PLIKOWY. Pozycję można określać względem 
 
początku pliku:  
 
SEEK_SET - stała określająca pozycjonowanie względem początku  
pliku;  
SEEK_CUR - względem położenia bieżącego (ang. Current -  
bieżący);  
SEEK_END - określenie pozycji względem końca pliku;  
EOF - End Of File - znak końca pliku.  
 
Funkcja lseek(): 
WSK_PLK = long int lseek( plik,  o_ile,  kierunek);  
służy do pozycjonowania w pliku.  
Liczba typu long int określająca pozycję w pliku nazywana jest  
WSKAŹNIKIEM PLIKOWYM ( w programie przykładowym została  
oznaczona long int WSK_PLK).  
 

W programie przykładowym wykonywane jest kolejno:  

* utworzenie na dysku pliku PROBA.DAT;  
* zapis do pliku wprowadzonych z klawiatury liczb całkowitych  
typu int;  
* zamknięcie pliku;  
* otwarcie pliku do odczytu;  
* ustawienie wskaźnika na końcu pliku;  
* odczyt z pliku od końca;  
* wyprowadzenie odczytanych z pliku danych na ekran.  
 

183

background image

[P073.CPP]  

 
# include "sys\stat.h"  
# include "conio.h"  
# include "stdio.h"  
# include "io.h"  
# include "fcntl.h"  
# define Cofnij_o_Zero 0  
# define dwa_bajty 2  
 
int Numer = 0;  
int Plik, L, M, i;  
long int Dlug_Pliku;  
  
main()  
{  
clrscr();  
creat("A:\PROBA.DAT", S_IWRITE);  
printf("\nPodaj liczbe rozna od zera,  zero - KONIEC");  
_fmode=O_BINARY;  
Plik=open("A:\PROBA.DAT", O_WRONLY);  
do  
  {  
    printf("\n Nr liczby \t%d\t\t", Numer++);  
    scanf("%d", &L);  
    if (L) write(Plik, &L, 2);  
  }  
while (L != 0);  
 
close(Plik);  
getch();  
 
printf("\n Teraz odczytam te liczby z pliku \n");  
Plik=open("A:\PROBA.DAT", O_RDONLY);  
Dlug_Pliku=lseek(Plik, 0, SEEK_END);  
for (i=Dlug_Pliku-dwa_bajty; i>=0; i-=2)  
  {   
    lseek(Plik, i, SEEK_SET);  
    read(Plik, &M, dwa_bajty);  
    printf("%d,  ", M);  
  }  
close(Plik);  
getch();  
 
return 0;  
}  
 

Zadania  

________________________________________________________________ 
Opracuj program wykonujący operacje na tekstach opisane  
wcześniej na łańcuchach tekstowych pobieranych z zewnętrznych  
plików dyskowych i umieszczanych w wynikowych plikach  
tekstowych. 
________________________________________________________________ 

184

background image

LEKCJA 20 - JEŚLI PROGRAM POWINIEN URUCHOMIĆ INNY 
PROGRAM...  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak w C++ można programować  
* procesy potomne  
* pisać programy rezydujące w pamięci (TSR)  
________________________________________________________________ 
 

O programach rezydentnych (TSR) i procesach potomnych.  

 
Warunek zewnętrznej zgodności z poprzednimi wersjami DOS  
wyraźnie hamuje ewolucję systemu MS DOS w kierunku "poważnych"  
systemów operacyjnych umożliwjających pracę wieloprogramową w  
trybie "multiuser", "multitasking" i "time sharing". Pewną  
namiastkę pracy wieloprocesowej dają nam już DOS 5/6 i Windows  
3.1. Można już otwierać wiele okien programów jednocześnie,  
można np. drukować "w tle", można wreszcie pisać rezydujące  
stale w pamięci programy klasy TSR (ang. Terminated and Stay  
Resident) uaktywniające się "od czasu do czasu".  
 

O bloku PSP. 

 
System DOS przydziela programom blok - "nagłówek" wstępny  
nazywany PSP (ang. Program Segment Prefix). Blok ten zawiera  
informacje o stanie systemu DOS w momencie uruchamiania programu 
 
(nazywanego tu inaczej procesem). Znajdują się tam informacje o  
bieżącym stanie zmiennych otoczenia systemowego (ang.  
environment variables) i parametrach uruchomieniowych. Blok PSP  
zajmuje 256 bajtów na początku kodu programu w zakresie adresów: 
 
 
CS:0000 ... CS:0100  (hex)  
 
Właściwy kod programu zaczyna się zatem od adresu CS:0100.  
Interpreter rozkazów systemu DOS ładuje programy do pamięci  
posługując się funkcją systemową nr 75 (4B hex). Wszystko jest  
proste dopóki mamy do czynienia z programem "krótkim" typu  
*.COM. Jeśli jednakże program uruchamiany jest w wersji  
"długiej" - *.EXE, dowolna może być nie tylko długość pliku, ale 
 
także początkowa zawartość rejestrów CS, SS, SP i IP. W plikach  
typu *.EXE początek bloku PSP wskazują rejestry DS (DS:0000) i  
ES. W Borland C++ masz do dyspozycji specjalną funkcję getpsp()  
przy pomocy której możesz uzyskać dostęp do bloku PSP programu.  
Krótki przykład zastosowania tej funkcji poniżej:  
 
/* Przykład zastosowania funkcji getpsp(): */  
 
# include <stdio.h>  
# include <dos.h>  
  
main()  
{  
   static char TAB[128];  
   char far *ptr;  

185

background image

   int dlugosc, i;  
  
   printf("Blok PSP: %u \n", getpsp());  
  
   ptr = MK_FP(_psp, 0x80);  
   dlugosc = *ptr;  
  
   for (i = 0; i < dlugosc; i++)  
      TAB[i] = ptr[i+1];  
  
   printf("Parametry uruchomieniowe: %s\n", TAB);  
  
}  
  
W normalnych warunkach po wykonaniu "swojej roboty" program  
zostaje usunięty z pamięci operacyjnej (czym zajmuje się funkcja 
 
systemowa nr 76 - 4C (hex)). Aby tak się nie stało, program  
może:  
 
* uruchomić swój proces (program) potomny;  
* wyjść "na chwilę" do systemu DOS - tj. uruchomić jako swój  
proces potomny interpreter COMMAND.COM;  
* przekazać sterowanie programowi COMMAND.COM pozostając w  
pamięci w postaci "uśpionej" oczekując na uaktywninie.  
 
Poniżej kilka prostych przykładów uruchamiania jednych procesów  
przez inne w Borland C++:  
 
/* Funkcja execv(): uruchomienie programu "potomnego"*/  
 
# include <process.h>  
# include <stdio.h>  
# include <errno.h>  
  
void main(int argc, char *argv[])  
{  
   int i;  
  
   printf("Parametry uruchomieniowe:");  
   for (i=0; i<argc; i++)  
      printf("\n%d)  %s", i, argv[i]);  
  
printf("Przekazuje parametry do procesu 2 par_1, par_2...\n");  
   execv("CHILD.EXE", argv);  
.... 
exit (2);  
}  
 

[P074.CPP] 

 
/* Funkcja system() - na chwilę do DOS */  
 
# include <stdlib.h>  
# include <stdio.h>  
  
void main()  
{  
   printf("Wyjscie do DOS i wykonanie jednego rozkazu:\n");  

186

background image

   system("dir > c:\plik.dir");  
}  
  
 
/* Funkcje grupy spawn...() : spawnl() */  
  
# include <process.h>  
# include <stdio.h>  
# include <conio.h>  
  
void main()  
{  
   int rezultat;  
   rezultat = spawnl(P_WAIT, "program.exe", NULL);  
   if (rezultat == -1)  
   {  
      perror(" Fiasko !");  
      exit(1);  
   }  
}  
  
  
/* Funkcja spawnle() */  
  
# include <process.h>  
# include <stdio.h>  
# include <conio.h>  
  
void main()  
{  
   int rezultat;  
  
   rezultat = spawnle(P_WAIT, "program.exe", NULL, NULL);  
   if (rezultat == -1)  
   {  
      perror("Fiasko !");  
      exit(1);  
   }  
}  
  
Zagadnienie uruchamiania programów potomnych (ang. child  
process) przez programy macieżyste (ang. parent process) jest  
rozpracowane w C++ dość dokładnie i zarazem obszernie. Istnieje  
wiele gotowych funkcji bibliotecznych, z usług których możesz tu 
 
skorzystać. Wszystko to nie jest jednak "prawdziwym" programem  
TSR. Przyjrzyjmy się zatem dokładniej dopuszcalnym przez system  
DOS sposobom zakończenia programu nie powodującym usunięcia  
programu z pamięci.  
 
Jeśli program rezydentny jest niewielki (kod < 64 K), możemy  
zakończyć program posługując się przerywaniem INT 39 (27 hex).  
Jeśli natomiast zamierzamy posługiwać się dłuższymi programami,  
mamy do dyspozycji funkcję systemową nr 49 (31 hex). Należy tu  
zwrócić uwagę, że zakończenie programu w taki sposób (z  
pozostawieniem w pamięci) nie spowoduje automatycznego  
zamknięcia plików, a jedynie opróżnienie buforów. Programy  
rezydentne dzieli się umownie na trzy kategorie:  
 
[BP] - background process - procesy działające "w tle";  

187

background image

[SV] - services - programy usługowe - np. PRINT;  
[PP] - pop up programs - uaktywniane przez określoną kombinację  
       klawiszy;  
 
System DOS dysponuje tzw. przerywaniem multipleksowym  
(naprzemiennym) wykorzystywanym często przez programy  
rezydentne. Jest to przerywanie nr INT 47 (2F hex). MS DOS  
załatwia takie problemy funkcjami nr 37 (25 hex) - zapisanie  
wektora przerywania i 53 (35 hex) - odczytanie wektora  
przerywania. 
 

Z jakich funkcji C++ można skorzystać? 

 
W C++ masz do dyspozycji parę funkcji getvect() i setvect()  
(ang. GET/SET VECTor - pobierz/ustaw wektor przerywania).  
Poniżej krótkie przykłady zastosowań tych funkcji.  
 
/* Opcja:  Options | Compiler | Code generation | Test Stack  
Overflow  powinna zostać wyłączona [ ] (off)  */  
 
# include "stdio.h"  
# include "dos.h"  
# include "conio.h"  
  
/* INT 28 (1C hex) - Przerywanie zegarowe */  
  
void interrupt ( *oldhandler)(void);  
int licznik = 0;  
  
void interrupt handler(void)  
{  
/* Inkrementacja globalnej zmiennej licznik */  
   licznik++;  
  
/* Wywolujemy stary "handler" zegara */  
   oldhandler();  
}  
  
void main()  
{  
/* Zapamiętaj poprzedni wektor przerywania 28 */  
   oldhandler = getvect(28);  
  
/* Zainstaluj nową funkcje obslugi przerywania */  
   setvect(28, handler);  
  
/* Inkrementuj licznik */  
   for (; licznik < 10; ) printf("licznik: %d\n",licznik);  
  
//odtworz stara funkcje obslugi przerywania: interrupt handler  
 
   setvect(28, oldhandler);  
}  
 
 
# include <stdio.h>  
# include <dos.h>  
  
void interrupt nowa_funkcja(); // prototyp funkcji - handlera 

188

background image

  
void interrupt (*oldfunc)(); /* interrupt function pointer */  
 
int warunek = 1;  
  
main()  
{  
  printf("\n [Shift]+[Print Screen] = Quit \n");  
  printf("Zapamietaj, i nacisnij cosik....");  
  while(!kbhit()); 
 
  /* zapamietaj stary wektor */  
  oldfunc  = getvect(5);  
  /* INT 5 to przerywanie Sys Rq, albo Print Screen */ 
 
  /* zainstaluj nowa funkcje obslugi: interrupt handler */  
  setvect(5, nowa_funkcja);  
  
  while (warunek) printf(".");  
  
   /* Odtworz stary wektor przerywania */  
   setvect(5, oldfunc);  
  
  printf("\n Udalo sie... nacisnij cosik...");  
  while(!kbhit()); 
}  
 
/* Definicja nowego handlera */ 
void interrupt nowa_funkcja()  
{  
  warunek = 0;    
/* jesli warunek == 0, petla zostanie przerwana*/  
}  
  
Jeśli nasz program zamierza korzystać z przerywania  
multipleksowego INT 47 (2F hex), należy pamiętać, że przerywanie 
 
to wykorzystują także inne programy systemowe. Rozróżniać te  
programy można przy pomocy identyfikatorów (podaję dziesiętnie): 
 
01  - PRINT.EXE  
06  - ASSIGN.COM  
16  - SHARE.EXE  (10 hex)  
26  - ANSI.SYS  
67  - HIMEM.SYS  
72  - DOSKEY.COM  
75  - TASK SWITCHER  
173 - KEYB.COM  
174 - APPEND.EXE  
176 - GRAFTABL.COM  
183 - APPEND.EXE  
 
Identyfikator programu TSR jest przekazywany za pośrednictwem  
rejestru AH.  
 
System DOS jest na razie systemem w zasadzie jednozadaniowym i  
jednoużytkownikowym, w którym zasoby są przydzielane procesom  
kolejno (ang. serially reusable resources). Aby uchronić się  
przed potencjalnym konfliktem, powinniśmy upewnić się, czy DOS  
"nic nie robi". Często stosowaną "sztuczką techniczną" jest  

189

background image

zastosowanie flag ErrorMode i InDos systemu oraz wykorzystanie  
mechanizmów przerywań nr 36 i 40 (24 i 28 hex). Przydatną  
informacją jest także identyfikator programu - PID. Na taką  
ewntualność Borland C++ dysponuje makrem getpid zdefiniowanym w  
pliku nagłówkowym <PROCESS.H>:  
 
# define  getpid()   (_psp)  
 
Inną przydatną funkcją może okazać się keep() (ang. keep  
resident - pozostań rezydentny). Oto krótki przykład  
zastosowania tej funkcji - znów z wykorzystaniem przerywań  
zegarowych.  
 
# include <dos.h>  
 
# define INTR 0x1C       /* przerywanie INT 28 */ 
# define ATTR 0x7900  
 
/* ograniczenie wielkości sterty (heap length) i stosu (stack  
length):                                                      */ 
extern unsigned _heaplen = 1024;  
extern unsigned _stklen  = 512;  
  
void interrupt ( *oldhandler)(void);  
  
void interrupt handler(void)  
{  
   unsigned int (far *ekran)[80];  
   static int licznik;  
  
// Adres pamieci dla monitora barwnego:  B800:0000.  
// Dla monitora monochromatycznego:  B000:0000.  
 
   ekran = MK_FP(0xB800,0);  
  
// piloksztaltna zmiana licznika w przedziale 0 ... 9  
 
   licznik++;  
   licznik %= 10;  
 
   ekran[0][79] = licznik + '0' + ATTR;  
  
// wywołaj stara funkcje obslugi - old interrupt handler: 
   oldhandler();  
}  
  
void main()  
{  
  
oldhandler = getvect(INTR);  
  
// zainstaluj nowa funkcje interrupt handler  
setvect(INTR, handler);  
  
/* _psp - to adres początku programu, SS:SP to adres stosu,  
czyli koniec programu. Biorac pod uwage przesuniecie  
SEGMENT/OFFSET o jedna tetrade: SS:SP = SS + SP/16; */ 
 
keep(0, (_SS + (_SP/16) - _psp));  
}  

190

background image

  

Kilka istotnych drobiazgów technicznych.  

 
W Borland C++ masz do dyspozycji predefiniowane struktury  
BYTEREGS (rejestry jednobajtowe - "połówki") i WORDREGS  
(rejestry dwubajtowe). Możesz po tych strukturach dziedziczyć i  
np. taką metodą wbudować je do swoich własnych klas. Nic nie  
stoi na przeszkodzie, by utworzyć np. klasę  
 
class REJESTRY : public WORDREGS  
{  
 ...  
};  
 
czy też własną strukturę:  
 
struct REJESTRY : WORDREGS { ... }; 
 
Definicje tych struktur w Borland C++ wyglądają następująco:  
 
struct BYTEREGS  
{  
  unsigned int al, ah, bl, bh, cl, ch, dl, dh;  
};  
 
struct WORDREGS  
{  
  unsigned int ax, bx, cx, dx, si, di, cflag, flags;  
};  
 
Rejestry segmentowe mają własną strukturę:  
 
struct SREGS  
{  
  unsigned int es, cs, ss, ds;  
}; 
 
Pole WORDREGS::cflag odpowiada stanowi flagi przeniesienia (ang. 
 
Carry Flag) rejestru flags mikroprocesora, a pole  
WORDREGS::flags odpowiada stanowi całości rejestru (w wersji 16  
- bitowej). Ponieważ rejestry mogą być widziane alternatywnie  
jako podzielone na miezależne połówki - lub jako całość, to  
właśnie "albo - albo" wyraża w C++ unia. W Borland C++ taka  
predefiniowana unia nazywa się REGS:  
 
union REGS  
{  
  struct WORDREGS x;  
  struct BYTEREGS h;  
};  
 
Z tych predefiniowanych struktur danych korzystają m. in.  
funkcje int86() intdosx() i int86x() ("x" pochodzi od eXtended - 
 
rozszerzony). Oto krótkie przykłady zastosowania tych funkcji.  
 
# include <stdio.h>  
# include <conio.h>  

191

background image

# include <dos.h>  
  
# define INT_NR 0x10  // 10 hex == 16 (Nr przerywania) VIDEO 
  
void UstawKursor(int x, int y)  
{  
   union REGS regs;  
  
   regs.h.ah = 2;  // ustaw kursor   
   regs.h.dh = y;  // Wspolrzedne kursora na ekranie 
   regs.h.dl = x;  
   regs.h.bh = 0;  // Aktywna stronica ekranu --> video page 0  
   int86(INT_NR, &regs, &regs);  
}  
  
void main()  
{  
   clrscr();  
   UstawKursor(30, 12);  
   printf("Tekst - Test");  
   while(!kbhit()); 
}  
  
# include <dos.h>  
# include <process.h>  
# include <stdio.h>  
  
void main()  
{  
   char nazwapliku[40];  
   union REGS inregs, outregs;  
   struct SREGS segregs;  
  
   printf("\nPodaj nazwe pliku: ");  
   gets(nazwapliku);             // gets() == GET String 
   inregs.h.ah = 0x43;  
   inregs.h.al = 0x21;  
   inregs.x.dx = FP_OFF(nazwapliku);  
   segregs.ds = FP_SEG(nazwapliku);  
   int86x(0x21, &inregs, &outregs, &segregs);  
   printf("\n Atrybuty pliku: %X\n", outregs.x.cx);  
}  
  
# include <stdio.h>  
# include <dos.h>  
 int SkasujPlik(char far*) // Prototyp 
 
void main()  
{  
   int error;  
   err = SkasujPlik("PLIK.DAT");  
   if (!error) printf("\nSkasowalem plik PLIK.DAT");  
   else  
      printf("\nNie moge skasowac pliku PLIK.DAT");  
}  
  
int SkasujPlik(char far *nazwapliku)  
{  
   union REGS regs; struct SREGS sregs;  
   int wynik;  

192

background image

   regs.h.ah = 0x41;          // Funkcja kasowania pliku 
   regs.x.dx = FP_OFF(nazwapliku);  
   sregs.ds = FP_SEG(nazwapliku);  
   wynik = intdosx(&regs, &regs, &sregs);  
   return(regs.x.cflag ? wynik : 0);  
                // Jesli CF == 1, nastapilo fiasko operacji 
}  
 
I wreszcie na zakończenie szczegóły techniczne działania funkcji 
 
systemowej nr 49 (31 hex) odpowiedzialnej za obsługę programów  
rezydujących w pamięci (załadowanie procesu z pozostawieniem w  
pamięci).  
 
1. Wywołanie funkcji:  
AL = kod powrotu (ang. return code);  
AH = 0031 (hex) - nr funkcji;  
DX = długość programu TSR w paragrafach - Size/16 [Bajtów];  
2. Działanie:  
* funkcja nie zamyka plików, lecz opróżnia bufory;  
* funkcja odtwarza wektory przerywań nr 34, 35, 36 (hex 21, 22,  
23);  
* proces macieżysty może uzyskać kod powrotu przy pomocy funkcji 
 
nr 77 (4D hex).  
 
Wykorzystanie struktury SDA (ang. Swappable Data Area - obszar  
wymiennych danych) nie jest praktyką zalecaną.  
 
Tworząc programy rezydentne bądź bardzo ostrożny i pamiętaj o  
jednej z podstawowych zasad - NIE JESTEŚ (tzn Twój program nie  
jest) SAM.  
_______________________________________________________________ 

LEKCJA 21: KILKA PROCESÓW JEDNOCZEŚNIE. 

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się, jak to zrobić, by Twój PC mógł 
wykonywać kilka rzeczy jednocześnie. 
________________________________________________________________ 
 
 

Procesy współbieżne.  

 
Sprzęt, czyli PC ma możliwości zdecydowanie pozwalające na  
techniczną realizację pracy wielozadaniowej. Nie ma też żadnych  
przeciwskazań, by zamiast koprocesora umożliwić w PC instalację  
drugiego (trzeciego) równoległego procesora i uprawiać na PC  
poważne programowanie współbieżne. Po co? To proste. Wyobraź  
sobie Czytelniku, że masz procesor pracujący z częstotliwością  
25 MHz (to 25 MILIONÓW elementarnych operacji na sekundę!).  
Nawet, jeśli wziąć pod uwagę, że niektóre operacje (dodawanie,  
mnożenie, itp.) wymagają wielu cykli - i tak można w  
uproszczeniu przyjąć, że Twój procesor mógłby wykonać od  
kilkuset tysięcy do kilku milionów operacji w ciągu sekundy.  

193

background image

Jeśli pracujesz np. z edytorem tekstu i piszesz jakiś tekst -  
znacznie ponad 99% czasu Twój procesor czeka KOMPLETNIE  
BEZCZYNNIE (!) na naciśnięcie klawisza. Przecież Twój komputer  
mogłby w tym samym czasie np. i formatować dyskietkę (dyskietka  
też jest powolna), i przeprowadzać kompilację programu, i  
drukować dokumenty, i przeprowadzić defragmentację drugiego  
dysku logicznego, itp. itd..  
 
Nawet taka pseudowspółbieżność realizowana przez DOS, Windows,  
czy sieć jest ofertą dostatecznie atrakcyjną, by warto było  
przyjrzeć się mechanizmom PSEUDO-współbieżności w C i C++.  
Współbieżność procesów, może być realizowana na poziomie  
 
* sprzętowym (architektura wieloprocesorowa),  
* systemowym (np. Unix, OS/2),  
* nakładki (np. sieciowej - time sharing, token passing)  
* aplikacji (podział czasu procesora pomiędzy różne  
   funkcje/moduły tego samego pojedynczego programu).  
 
My zajmiemy się tu współbieżnością widzianą z poziomu aplikacji. 
 
Funkcje setjmp() (ang. SET JuMP buffer - ustaw bufor  
umożliwiający skok do innego procesu) i longjmp() (ang. LONG  
JuMP - długi skok - poza moduł) wchodzą w skład standardu C i w  
związku z tym zostały "przeniesine" do wszystkich kompilatorów  
C++ (nie tylko Borlanada).  
 

Porozmawiajmy o narzędziach.  

 
Zaczniemy od klasycznego zestawu narzędzi oferowanego przez  
Borlanda. Aby zapamiętać stan przerwanego procesu stosowana jest 
 
w C/C++ struktura PSS (ang. Program Status Structure) o nazwie  
jmp_buf (JuMP BUFfer - bufor skoku). W przypadku współbieżności  
wielu procesów (więcej niż dwa) stosuje się tablicę złożoną ze  
struktur typu  
 
  struct jmp_buf TablicaBuforow[n]; 
 
Struktura służy do przechowywania informacji o stanie procesu  
(rejestrach procesora w danym momencie) i jest predefiniowana w  
pliku SETJMP.H:  
 
  typedef struct  
               {  
                 unsigned j_sp, j_ss, j_flag, j_cs;  
                 unsigned j_ip, j_bp, j_di, j_es;  
                 unsigned j_si, j_ds;  
               } jmb_buf[1];  
 
Prototypy funkcji:  
 
  int setjmp(jmp_buf bufor);  
  void longjmp(jmp_buf bufor, int liczba);  
 
W obu przypadkach jmp_buf bufor oznacza ten sam typ bufora  
(niekoniecznie ten sam bufor - może ich być wiele), natomiast  
int liczba oznacza tzw. return value - wartość zwracaną po  
powrocie z danego procesu. Liczba ta może zawierać informację, z 

194

background image

 
którego procesu nastąpił powrót (lub inną przydatną w  
programie), ale nie może być ZEREM. Jeśli funkcja longjmp()  
otrzyma argument int liczba == 0 - zwróci do programu wartość 1. 
 
 
Wartość całkowita zwracana przez funkcję setjmp() przy pierwszym 
 
wywołaniu jest zawsze ZERO a przy następnych wywołaniach (po  
powrocie z procesu) jest równa parametrowi "int liczba"  
przekazanemu do ostatnio wywołanej funkcji longjmp().  
 
Przyjrzyjmy się temu mechanizmowi w praktyce. Wyobraźmy sobie,  
że chcemy realizować współbieżnie dwa procesy - proces1 i  
proces2. Proces pierwszy będzie naśladował w uproszczeniu  
wymieniony wyżej edytor tekstu - pozwoli na wprowadzanie tekstu, 
 
który będzie powtarzany na ekranie. Proces drugi będzie  
przesuwał w dolnej części ekranu swój numerek - cyferkę 2 (tylko 
 
po to, by było widać, że działa). Program główny wywołujący oba  
procesy powinien wyglądać tak:  
 
 ... 
void proces1(void);  
void proces2(void);  
 
int main(void)  
{  
  clrscr();  
  proces1();  
  proces2();  
return 0;  
}  
 
Ależ tu nie ma żadnej współbieżności! Oczywiście. Aby  
zrealizować współbieżność musimy zadeklarować bufor na bieżący  
stan rejestrów i zastosować funkcje setjmp():  
 
#include <setjmp.h>  
 
void proces1(void);  
void proces2(void);  
  
jmp_buf bufor1;  
 
int main(void)  
{  
clrscr();  
if(setjmp(bufor1) != 0) proces1();    //Powrót z procesu2 był? 
proces2();  
return 0;  
}  
 
Po wywołaniu funkcji setjmp() zostanie utworzony bufor1, w  
którym zostanie zapamiętany stan programu. Funkcja, jak zawsze  
przy pierwszym wywołaniu zwróci wartość ZERO, więc warunek  
 
  if(setjmp(bufor1) != 0) ... 
 

195

background image

nie będzie spełniony i proces1() nie zostanie wywołany. Program  
pójdzie sobie dalej i uruchomi proces2():  
 
void proces2(void)  
{  
 for(;;)  
 {  
   gotoxy(10,20);  
   printf("PROCES 2: ");  
   for(int i = 1; i<40; i++)  
   {  
   printf(".2\b");  
   delay(5);            //UWAGA: delay() tylko dla DOS! 
   }  
   longjmp(bufor1, 1);         <--- wróć 
 }                 ____________ tę jedynkę zwróci setjmp() 
}  
  
Proces 2 będzie drukował "biegającą dwójkę" (zwolnioną przez  
opóźnienie delay(5); o pięć milisekund), poczym funkcja  
longjmp() każe wrócić z procesu do programu głównego w to  
miejsce:  
 
int main(void)  
{  
clrscr();  
if(setjmp(bufor1)) proces1();           <--- tu powrót 
proces2();  
return 0;  
}  
 
Zmieni się tylko tyle, że powtórnie wywołana funkcja setjmp()  
zwróci tym razem wartość 1, zatem warunek będzie spełniony i  
rozpocznie się proces1():  
 
void proces1(void)  
{  
   while(kbhit())  
      {  
        gotoxy(1,1);  
        printf("PROCES1, Pisz tekst:       [Kropka - Koniec]");  
        gotoxy(pozycja,2);  
          znak = getch();  
          printf("%c", znak);  
        pozycja++;  
      }  
   if(znak == '.') exit (0);  
}  
 
Proces 1 sprawdzi przy pomocy funkcji kbhit() czy w buforze  
klawiatury oczekuje znak (czy coś napisałeś). Jeśli tak -  
wydrukuje znak, jeśli nie - zakończy się i program przejdzie do  
procesu drugiego. A oto program w całości:  
 

[P075.CPP] 

 
#include <stdio.h>  
#include <process.h>  
#include <setjmp.h>  

196

background image

#include <conio.h>  
#include <dos.h>  
  
void proces1(void);  
void proces2(void);  
  
jmp_buf bufor1, bufor2;  
  
char znak;  
int pozycja = 1;  
  
int main(void)  
{  
  clrscr();  
  if(setjmp(bufor1)) proces1();  
  proces2();  
  return 0;  
}  
  
void proces1(void)  
{  
   while(kbhit())  
      {  
        gotoxy(1,1);  
        printf("PROCES1, Pisz tekst:       [Kropka - Koniec]");  
        gotoxy(pozycja,2);  
        znak = getch();  
        printf("%c", znak);  
        pozycja++;  
      }  
   if(znak == '.') exit (0);  
}  
  
void proces2(void)  
{  
 for(;;)  
 {  
   gotoxy(10,20);  
   printf("PROCES 2: ");  
   for(int i = 1; i<40; i++)  
   {  
     printf(".1\b");  
     delay(5);  
   }  
   longjmp(bufor1,1);  
 }  
}  
  
[!!!] UWAGA 
________________________________________________________________ 
Funkcja delay() użyta dla opóżnienia i zwolnienia procesów  
będzie funkcjonować tylko w środowisku DOS. Przy uruchamianiu  
prykładowego programu pod Windows przy pomocy BCW należy tę  
funkcję poprzedzić znakiem komentzrza // .  
________________________________________________________________ 
 
 
 

197

background image

Wyobrażmy sobie, że mamy trzy procesy. Przykład współbieżności trzech 
procesów oparty na tej samej zasadzie

 zawiera program  
poniżej  
 

[P076.CPP] 

 
#include <stdio.h>  
#include <process.h>  
#include <setjmp.h>  
#include <conio.h>  
#include <dos.h>  
  
void proces1(void);  
void proces2(void);  
void proces3(void);  
  
jmp_buf bufor1, bufor2;  
  
char znak;  
int pozycja = 1;  
  
int main(void)  
{  
  clrscr();  
  if(setjmp(bufor1)) proces1();  
  if(setjmp(bufor2)) proces2();  
  proces3();  
  return 0;  
}  
  
void proces1(void)  
{  
  while(kbhit())  
   {  
     gotoxy(1,1);  
     printf("PROCES1, Pisz tekst: [Kropka - Koniec]");  
     gotoxy(pozycja,2);  
     znak = getch();  
     printf("%c", znak);  
     pozycja++;  
   }  
   if(znak == '.') exit (0);  
}  
  
void proces2(void)  
{  
 for(;;)  
 {  
   gotoxy(10,20);  
   printf("PROCES 2: ");  
   for(int i = 1; i<40; i++)  
   {  
     printf(".2\b");  
     delay(5);  
   }  
   longjmp(bufor1, 1);  
  

198

background image

 }  
}  
  
  
void proces3(void)  
{  
 for(;;)  
 {  
   gotoxy(10,23);  
   printf("PROCES 3: ");  
   for(int i = 1; i<40; i++)  
   {  
     printf(".3\b");  
     delay(2);  
   }  
  
   longjmp(bufor2,2);  
 }  
}  
  
Procesy odbywają się z różną prędkością. Kolejność uruchamiania  
procesów będzie:  
 
- proces3()  
- proces2()  
- proces1()  
 
Po uruchomieniu programu zauważysz, że proces pierwszy (pisania) 
 
został spowolniony. Można jednak temu zaradzić przez ustawienie  
flag i priorytetów. Jeśli dla przykładu uważamy, że pisanie jest 
 
ważniejsze, możemy wykrywać zdarzenie - naciśnięcie klawisza w  
każdym z mniej ważnych procesów i przerywać wtedy procesy mniej  
ważne. Wprowadzanie tekstu w przykładzie poniżej nie będzie  
spowolnione przez pozostałe procesy.  
 

[P077.CPP] 

 
#include <stdio.h>  
#include <process.h>  
#include <setjmp.h>  
#include <conio.h>  
#include <dos.h>  
  
void proces1(void);  
void proces2(void);  
void proces3(void);  
  
jmp_buf BuforStanu_1, BuforStanu_2;  
  
char znak;  
int pozycja = 1;  
  
int main(void)  
{  
  clrscr();  
  if(setjmp(BuforStanu_1)) proces1();  
  if(setjmp(BuforStanu_2)) proces2();  

199

background image

  proces3();  
  return 0;  
}  
  
void proces1(void)  
{  
   while(kbhit())  
    {  
      gotoxy(1,1);  
      printf("PROCES1, Pisz tekst: [Kropka - Koniec]");  
      gotoxy(pozycja,2);  
      znak = getch();  
      printf("%c", znak);  
      pozycja++;  
    }  
   if(znak == '.') exit (0);  
  
}  
  
void proces2(void)  
{  
 for(;;)  
 {  
   gotoxy(10,20);  
   printf("PROCES 2: ");  
   for(int i = 1; i<40; i++)  
   {  
     if(kbhit()) break;  
     printf(".2\b");  
     delay(5);  
   }  
   longjmp(BuforStanu_1, 1);  
  
 }  
}  
  
  
void proces3(void)  
{  
 for(;;)  
 {  
   gotoxy(10,23);  
   printf("PROCES 3: ");  
   for(int i = 1; i<40; i++)  
   {  
     if(kbhit()) break;  
     printf(".3\b");  
     delay(2);  
   }  
  
   longjmp(BuforStanu_2,2);  
 }  
}  
  

UWAGA 

________________________________________________________________ 
W pierwszych dwu przykładach trzymanie stale wciśniętego  
klawisza spowoduje tylko automatyczną repetycję wprowadzanego  
znaku. W przykładzie trzecim spowoduje to przerwanie procesów 2  

200

background image

i 3, co będzie wyraźnie widoczne na monitorze (DOS).  
Zwróć uwagę, że kbhit() nie zmienia stanu bufora klawiatury. 
________________________________________________________________ 
 
 
W bardziej rozbudowanych programach można w oparciu o drugi  
parametr funkcji longjmp() zwracany przez funkcję setjmp(buf) po 
 
powrocie z procesu identyfikować - z którego procesu nastąpił  
powrót i podejmować stosowną decyzję np. przy pomocy instrukcji  
switch:  
 
     switch(setjmp(bufor))  
        {  
          case 1 : proces2();  
          case 2 : proces3();  
          .....  
          default : proces0();  
        }  
 

UWAGA 

________________________________________________________________ 
* Zmienne sterujące przełączaniem procesów powinny być zmiennymi 
 
globalnymi, bądź statycznymi. Także dane, które nie mogą ulec  
nadpisaniu bezpieczniej potraktować jako globalne.  
________________________________________________________________ 
 
W przypadku wielu procesów celowe jest utworzenie listy, bądź  
kolejki procesów. Przydatny do tego celu bywa mechanizm tzw.  
"łańcuchowej referencji". W obiektach klasy PozycjaListy należy  
umieścić pole danych - strukturę i pointer do następnego  
procesu, któremu (zgodnie z ustalonym priorytetem) należy  
przekazać sterowanie:  
 
  static jmp_buf Bufor[m];           <-- m - ilość procesów 
   ...  
 
   class PozycjaListy  
     {  
     public: 
        jmp_buf Bufor[n];            <-- n - Nr procesu 
        PozycjaListy *nastepna;  
     }  
 
Wyobrażmy sobie sytuację odrobinę różną od powyższych przykładów 
 
(w której zresztą para setjmp() - longjmp() równie często  
występuje.  
 
#include <setjmp.h>  
 
jmp_buf BuforStanu;  
int Nr_Bledu;  
 
int main(void)  
{  
  Nr_Bledu = setjmp(BuforStanu)         <-- tu nastąpi powrót  
  if(Nr_Bledu == 0)                <-- za pierwszym razem ZERO 

201

background image

    {  
      /* PRZED powrotem z procesu (ów) */  
       ....  
      Proces();                      <-- Wywołanie procesu 
    }  
  else  
    {  
      /* PO powrocie z procesu (ów)  */  
      ErrorHandler();                <-- obsługa błędów 
    }  
 ....  
  return 0;  
}  
 
Taka struktura zapewnia działanie następujące:  
 
- Był powrót z procesu?  
  NIE: Wywołujemy proces!  
  TAK: Obsługa błędów, które wystąpiły w trakcie procesu.  
 
Jeśli teraz proces zaprojektujemy tak:  
 
void Proces()  
{  
  int Flaga_Error = 0;  
 ...  
/* Jeśli nastąpiły błędy, flaga w trakcie pracy procesu jest  
    ustawiana na wartość różną do zera */  
 
  if(Error) Flaga_Error++;  
 ...  
  if(Fllaga_Error != 0) longjmp(BuforStanu, Flaga_Error);  
 ...  
}  
 
proces przekaże sterowanie do programu w przypadku wystąpienia  
błędów (jednocześnie z informacją o ilości/rodzaju błędów).  
 

Zadania

________________________________________________________________ 
1. Napisz samodzielnie program realizujący 2, 3, 4 procesy  
współbieżne. Jeśli chcesz, by jednym z procesów stał się  
całkowivie odrębny program - skorzystaj z funkcji grupy  
spawn...() umożliwiających w C++ uruchamianie procesów  
potomnych.  
_______________________________________________________________ 

LEKCJA 22. NA ZDROWY CHŁOPSKI ROZUM PROGRAMISTY.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się:  
* jak przyspieszać działanie programów w C++  
* jakie dodatkowe narzędzia zyskujesz "przesiadając się" na  
nowoczesny kompilator C++  
________________________________________________________________ 
 

202

background image

 

UNIKAJMY PĘTLI, które nie są NIEZBĘDNE !  

 
Unikanie zbędnych pętli nazywa się fachowo "rozwinięciem pętli"  
(ang. loop unrolling). Zwróć uwagę, że zastępując pętlę jej  
rozwinięciem (ang. in-line code):  
 
* zmniejszamy ilość obliczeń, 
* zmniejszamy ilość zmiennych. 
 
Wyobraźmy sobie pętlę:  
 
for (i = 0; i < max; i++)  
    T[i] = i;  
 
Jeśli "unowocześnimy" ją tak:  
 
for (i = 0; i < max; )  
  { 
    T[i++] = i - 1;  
    T[i++] = i - 1;  
  }  
 
ilość powtórzeń pętli zmniejszy się dwukrotnie. Czai się tu  
jednak pewne niebezpieczeństwo: tablica może mieć NIEPARZYSTĄ  
liczbę elementów. Np. dla 3-elementowej tablicy (max = 3)  
nastąpiłyby w pierwszym cyklu operacje:  
 
 i = 0;  
 0 < 3 ? == TRUE  --> T[0] = 0   // Tu nastepuje i++; //  
                      T[1] = 1  itd...  
 
To, co następuje w tak "spreparowanej" tablicy możesz  
prześledzić uruchamiając program:  
 

[P078.CPP]  

 
# include <iostream.h>  
# include <stdio.h>  
# include <conio.h>  
 
# define p(x) printf("%d\t", x)  
 
int T[99+1], i, max;  
  
main()  
{  
cout << "\nPodaj ilosc elem. tablicy T[] - 2...99 \n";  
cin >> max;  
  
cout << "T[i]\t\ti\n\n";  
  
for (i = 0; i < max; )   
  {  
    T[i++] = i - 1; p(T[i-1]); cout << "\t" << i << "\n";   
    T[i++] = i - 1; p(T[i-1]); cout << "\t" << i << "\n";  
    while (!kbhit());  

203

background image

  }   
  
return 0;  

 
Aby nie spowodować próby odwołania do nieistniejącego elementu  
tablicy, możemy zadeklarować tablicę T[max + 1]. W przypadku,  
gdy max jest liczbą nieparzystą, tablica wynikowa posiada  
parzystą liczbę elementów. Jeśli natomiast max jest parzyste,  
tworzymy jeden zbędny element tablicy, który później zostanie  
użyty, ale kompilator ani program nie będzie nam się "buntował". 
 
 
Można spróbować zastąpić w programie bardziej czasochłonne  
operacje - szybszymi. Dla przykładu, w pętli  
 
for(i = 1; i <= 100; i++)  
  {    
    n = i * 10;  
    ... 
 
można wyeliminować czasochłonne mnożenie np. tak:  
 
for(i = 1, n = 10; i <= 100; i++, n += 10)  
   {  
    ...  
 
lub wręcz wprost, jeśli dwie zmienne robocze nie są niezbędne:  
 
for(n = 10; n <= 1000; n += 10)  
   {  
    ...  
 
Jeśli wiadomo, że jakaś pętla powinna wykonać się z definicji  
choćby raz, warto wykorzystywać konstrukcję do...while, zamiast  
analizować niepotrzebnie warunek.  
 
Jeśli stosujemy w programie pętle zagnieżdżone (ang. nested  
loops), to pęta zorganizowana tak:  
 
for(i = 1; i < 5; i++)                      (1)  
   for(j = 1; j < 1000; j++)  
      { A[i][j] = i + j; }  
 
zadziała szybciej niż  
 
for(j = 1; j < 1000; j++)                   (2) 
   for(i = 1; i < 5; i++)  
      { A[i][j] = i + j; }  
 
W przypadku (1) zmienna robocza pętli wewnętrznej będzie  
inicjowana pięć razy, a w przypadku (2) - tysiąc (!) razy.  
 
Czasami zdarza się, że w programie można połączyć kilka pętli w  
jedną.  
 
   for(i = 1; i < 5; i++)  
      TAB_1[i] = i;  
 ...  
   for(k = 0; k < 5; k++)  

204

background image

      TAB_2[k] = k;  
 
Zmniejsza to i ilość zmiennych, i tekst programu i czas pracy  
komputera:  
 
      TAB_2[0] = 0; 
   for(i = 1; i < 5; i++)  
      TAB_1[i] = i;  
      TAB_2[i] = i;  
 
Czasami wykonywanie pętli do końca pozbawione jest sensu.  
Przerwać pętlę w trakcie wykonywania można przy pomocy  
instrukcji break (jeśli pętle są zagnieżcżone, często lepiej  
użyć niepopularnego goto przerywającego nie jedną - a wszystkie  
pętle). Stosując umiejętnie break, continue i goto możesz  
zaoszczędzić swojemu komputerowi wiele pracy i czasu. Rutynowym  
"szkolno-strukturalnym" zapętlaniem programu  
 
main() {  
char gotowe = 0; 
 ...  
while (!gotowe)  
  {  
    znak = wybrano_z_menu();  
       if (znak == 'q' || znak == 'Q') gotowe = 1;  
           else  
             .......  
    gotowe = 1;  
  }  
 
powodujesz często zupełnie niepotrzebne dziesiątki operacji,  
które już niczemu nie służą.  
 
char gotowe; 
main() {  
 ...  
while (!gotowe)  
  {  
    znak = wybrano_z_menu();  
       if (znak == 'q' || znak == 'Q') break;   //Quit ! 
           else  
             .......  
    gotowe = 1;  
  }  
 
Tym razem to, co następuje po else zostanie pominięte.  
 
Wskaźniki działają w C++ szybciej, niż indeksy, stosujmy je w  
miarę możliwości w pętlach, przy manipulowaniu tablicami i w  
funkcjach.  
 

INSTRUKCJE STERUJĄCE I WYRAŻENIA ARYTMETYCZNE.  

 
Na "chłopski rozum" programisty wiadomo, że na softwarowych  
rozstajach, czyli na rozgałęzieniach programów  
prawdopodobieństwo wyboru każdwgo z wariantów działania programu 
 
z reguły bywa różne. Kolejność sprawdzania wyrażeń warunkowych  
nie jest zatem obojętna. Wyobraźmy sobie lekarza, który  

205

background image

zwiezionego na toboganie narciarza pyta, czy ktoś w rodzinie  
chorował na żółtaczkę, koklusz, reumatyzm, podagrę, itp. zamiast 
 
zająć się najpierw wariantem najbardziej prawdopodobnym - czyli  
zagipsowaniem nogi nieszczęśnika. Absurdalne, prawda? Ale  
przecież (uderzmy się w piersi) nasze programy czasami postępują 
 
w taki właśnie sposób...  
 

NAJPIERW TO, CO NAJBARDZIE PRAWDOPODOBNE I NAJPROSTSZE.  

 
Jeśli zmienna x w naszym programie może przyjmować (równie  
prawdopodobne) wartości 1, 2, 3, 4, 5, to "przesiew"  
 
if (x >= 2) { ... }  
   else if (x == 1) { ... }  
   else { ... }  
 
okaże się w praktyce skuteczniejszy, niż  
 
if (x == 0) { ... }  
   else if (x == 1) { ... }  
   else { ... }  
 
Należy pamiętać, że w drabince if-else-if po spełnieniu  
pierwszego warunku - następne nie będą już analizowane.  
 
Zasada ta stosuje się także do wyrażeń logicznych, w których  
stosuje się operatory logiczne || (lub) i && (i). W wyrażeniach  
tych, których ocenę C++ prowadzi tylko do uzyskania pewności,  
jaka będzie wartość logiczna (a nie koniecznie do końca  
wyrażenia) należy zastosować kolejność:  
 
MAX || W1 || W2 || W3 ...  
MIN && W1 && W2 && W3 ...  
 
gdzie MAX - oznacza opcję najbardziej prawdopodobną, a MIN -  
najmniej prawdopodobną.  
 
Podobnie rzecz ma się z pracochłonnością (zatem i  
czso-chłonnością) poszczególnych wariantów. Jeśli wariant  
najprostszy okaże się prawdziwy, pozostałe możliwości możemy  
pominąć.  
 

NIE MNÓŻ I NIE DZIEL BEZ POTRZEBY.  

 
Prawa MATEMATYKI pozostają w mocy dla IBM PC i pozostaną zawsze, 
 
nawet dla zupełnie nieznanych nam komputerów, które skonstruują  
nasze dzieci i wnuki. Znajomość praw de Morgana i zasad  
arytmetyki jest dla programisty wiedzą niezwykle przydatną. Jako 
 
próbkę zapiszmy kilka trywialnych tożsamości przetłumaczonych na 
 
C++:  
 
2 * a == a + a == a << 1  

206

background image

16 * a == a << 4 
a * b + a * c == a * (b + c)  
~a + ~b == ~(a + b) 
 
Możnaby jeszcze dodać, że a / 2 == a >> 1, ale to nie zawsze  
prawda. Przesunięcie w prawo liczb nieparzystych spowoduje  
obcięcie części ułamkowej. W przypadku wyrażeń logicznych:  
 
(x && y) || (x && z) == x && (y || z)  
(x || y) && (x || z) == x || (y && z)  
 
W arytmetycznej sumie i iloczynie NIE MA takiej symetrii.  
 
!x && !y == !(x || y)  
!x || !y == !(x && y)  
 
Jeśli w skomplikowanych wyrażeniach arytmetycznych i logicznych  
zastosujemy zasady arytmetyki i logiki, zwykle stają się krótsze 
 
i prostsze. Podobnie jak licząc na kartce, możemy zastosować  
zmienne pomocnicze do przechowywania często powtarzających się  
wyrażeń składowych. Wyrażenie  
 
wynik = (x * x) + (x * x);  
 
możemy przekształcić do postaci  
 
zm_pomocn = x * x;  
wynik = zm_pomocn << 1;  
 
Często napisane "na logikę" wyrażenia da się łatwo  
zoptymalizować. Jako przykład zastosujmy funkcję biblioteczną  
strcmp() (string compare - porównaj łańcuchy znaków). Porównanie 
 
łańcuchów  
 
if (strcmp(string1, string2) == 0) cout << "identyczne";  
  else if (strcmp(string1, string2) < 0) cout << "krotszy";  
       else  
          cout << "dluzszy";  
 
można skrócić tak, by funkcja strcmp() była wywoływana tylko  
raz:  
 
wynik = strcmp(string1, string2);  
if (wynik == 0) 
    cout << "identyczne"; break;  
else if (wynik < 0)  
    cout << "krotszy";  
else  
    cout << "dluzszy";  
 
Jeśli pracując nad programem nie będziemy zapominać, że PC  
operuje arytmetyką dwójkową, wiele operacji dzielenia i mnożenia 
 
(długich i pracochłonnych) będziemy mogli zastąpić operacjami  
przesunięcia w lewo, bądź w prawo (ang. shift), które nasz PC  
wykonuje znacznie szybciej. Dla liczb całkowitych dodatnich  
 
x * 2 == x << 1;    x * 4 == x << 2  itp. ....  

207

background image

 

UWAGA  

________________________________________________________________ 
Takich skrótów nie można stosować w stosunku do operandów typu  
double, ani float.  
________________________________________________________________ 
 
Podobnie w przypadku dzielenia przez potęgę dwójki można  
zastąpić dzielenia znacznie szybszą operacją iloczynu  
logicznego.  
 
x % 16 == x & 0xF;  
 
Jeśli w programie wartość zmiennej powinna zmieniać się w sposób 
 
piłokształtny (tj. cyklicznie wzrastać do MAXIMUM i po  
osiągnięciu MAXIMUM spadać do zera), najprostszym rozwiązaniem  
jest  
 
x = (x + 1) % (MAXIMUM + 1);  
 
ale dzielenie trwa. Poniższy zapis spowoduje wygenerowanie kodu  
znacznie szybszego:  
 
if (x == MAXIMUM) x = 0;  
   else x++;  
 
stosując zamiast if-else operator ? : możemy to zapisać tak:  
 
(x == MAXIMUM) ? (x = 0) : (x++);  
 
Mnożenie jest zwykle trochę szybsze niż dzielenie. Zapis  
 
a = b / 10;  
 
można zatem zastąpić szybszym:  
 
a = b * .1;  
 
Jeśli mamy do czynienia ze stałą STALA, to zapis w programie  
 
y = x / STALA;      -->    y = x * (1.0 / STALA);  
 
z pozoru bzdurny spowoduje w większości implementacji  
wyznaczenie wartości mnożnika 1.0/STALA przez kompilator na  
etapie kompilacji programu (compile-time), a w ruchu (run-time)  
będzie obliczany iloczyn zamiast ilorazu.  
 
W programach często stosuje się flagi binarne (jest-nie ma). C++ 
 
stosujemy jako flagi zmienne typu int lub char a w Windows BOOL. 
 
Jeśli weźmiemy pod uwagę fakt, że operatory relacji generują  
wartości typu TRUE/FALSE, typowy zapis:  
 
if (a > b)  
   Flaga = 1;  
else  
   Flaga = 0;  

208

background image

 
zastąpimy krótszym  
 
Flaga = (a > b);  
 
Taki krótszy zapis NIE ZAWSZE powoduje wygenerowanie szybszego  
kodu. Jest to zależne od specyfiki konkretnej implementacji.  
Jeśli natomiast uprościsz swój program tak:  
 
if (x > 1) a = 3;       -->      a = 3 * (x > 1); 
  else a = 0;  
 
spowoduje to wyraźne spowolnienie programu (mnożenie trwa).  
 
Kompilator C++ rozróżnia dwa rodzaje wyrażeń:  
 
* general expressions - wyrażenia ogólne - zawierające zmienne i 
 
wywołania funkcji, których wartości nie jest w stanie określić  
na etapie kompilacji i  
* constant expressions - wyrażenia stałe, których wartość można  
wyznaczyć na etapie kompilacji.  
 
Zapis  
  
wynik = 2 * x * 3.14;  
 
możesz zatem przekształcić do postaci  
 
wynik = 2 * 3.14 * x;  
 
Kompilator przekształci to wyrażenia na etapie kompilacji do  
postaci  
 
wynik = 6.28 * x;  
 
co spowoduje zmniejszenie ilości operacji w ruchu programu. Aby  
ułatwić takie działanie kompilatora trzeba umieścić stałe obok  
siebie.

LEKCJA 23 - Co nowego w C++?  

________________________________________________________________ 
Z tej lekcji dowiesz się, jakie mechanizmy C++ pozwalają na  
stosowanie nowoczesnego obiektowego i zdarzeniowego stylu  
programowania i co programy robią z pamięcią.  
________________________________________________________________ 
 

W porównaniu z klasycznym C - C++ posiada:  

 
* rozszerzony zestaw słów kluczowych (ang. keywords):  
** nowe słowa kluczowe C++:  
 
 class - klasa,  
 delete - skasuj (dynamicznie utworzony obiekt),  
 friend - "zaprzyjaźnione" funkcje z dostępem do danych,  
 inline - wpleciony (funkcje przeniesione w formie rozwiniętej  

209

background image

 do programu wynikowego),  
 new - utwórz nowy obiekt,  
 operator - przyporządkuj operatorowi nowe działanie,  
 private - dane i funkcje prywatne klasy (obiektu), do których  
 zewnętrzne funkcje nie mają prawa dostępu,  
 protected - dane i funkcje "chronione", dostępne z  
 ograniczeniami,  
 public - dane i funklcje publiczne, dostępne bez ograniczeń,  
 template - szablon,  
 this - ten, pointer wskazujący bieżący obiekt,  
 virtual - funkcja wirtualna, abstrakcyjna, o zmiennym  
 działaniu. 
 
* nowe operatory (kilka przykładów już widzieliśmy), np.:  
 << - wyślij do strumienia wyjściowego,  
 >> - pobierz ze strumienia wejściowego. 
* nowe typy danych:  
 klasy,  
 obiekty,  
 abstrakcyjne typy danych (ang. ADT). 
* nowe zasady posługiwania się funkcjami:  
 funkcje o zmiennej liczbie argumentów,  
 funkcje "rozwijane" inline,  
 funkcje wirtualne, itp.;  
 
Przede wszystkim (i od tego właśnie rozpoczniemy) zobaczymy  
funkcje o nowych możliwościach.  
 

ROZSZERZENIE C - FUNKCJE.  

 
Funkcje uzyskują w C++ znacznie więcej możliwości. Przegląd  
rozpoczniemy od sytuacji często występującej w praktyce  
programowania - wykorzystywania domyślnych (ang. default)  
parametrów.  
 

FUNKCJE Z DOMYŚLNYMI ARGUMENTAMI.  

 
Prototyp funkcji w C++ pozwala na podanie deklaracji domyślnych  
wartości argumentów funkcji. Jeśli w momencie wywołania funkcji  
w programie jeden (lub więcej) argument (ów) zostanie pominięte, 
 
kompilator wstawi w puste miejsce domyślną wartość argumentu.  
 
Aby uzyskać taki efekt, prototyp funkcji powinien zostać  
zadeklarowany w programie np. tak:  
 
void Funkcja(int = 7, float = 1.234);  
 
Efekt takiego działania będzie następujący:  
 
Wywołanie w programie:         Efekt:  
________________________________________________________________ 
 
Funkcja(99, 5.127);            Normalnie:  Funkcja(99, 5.127);  
Funkcja(99);                               Funkcja(99, 1.234);  
Funkcja();                                 Funkcja(7, 1.234);  
________________________________________________________________ 

210

background image

 
 
[!!!] Argumentów może ubywać wyłącznie kolejno. Sytuacja:  
 
Funkcja(5.127);          //ŹLE  
Funkcja(99);             //DOBRZE 
 
jest w C++ niedopuszczalna. Kompilator potraktuje liczbę 5.127  
jako pierwszy argument typu int i wystąpi konflikt.  
 

[P079.CPP] 

 
#include <iostream.h>  
  
void fun_show(int = 1234, float = 222.00, long = 333L);  
  
main()  
{  
  fun_show();                         // Trzy arg. domyslne 
  fun_show(1);                        // Pierwszy parametr  
  fun_show(11, 2.2);                   // Dwa parametry 
  fun_show(111, 2.22, 3L);            // Trzy parametry 
  return 0; 
}  
  
void fun_show(int X, float Y, long Z)  
{  
cout << "\nX = "  << X;  
cout << ", Y = " << Y;  
cout << ", Z = "  << Z;  
}  
  
Uruchom program i przekonaj się, czy wstawianie argumentów  
domyślnych przebiega poprawnie.  
 

W KTÓRYM MIEJSCU UMIESZCZAĆ DEKLARACJE ZMIENNYCH.  

 
C++ pozwala deklarować zmienne w dowolnym miejscu, z  
zastrzeżeniem, że deklaracja zmiennej musi nastąpić przed jej  
użyciem. Umieszczanie deklaracji zmiennych możliwie blisko  
miejsca ich użycia znacznie poprawia czytelność (szczególnie  
dużych "wieloekranowych") programów. Klasyczny sposób deklaracji 
 
zmiennych:  
 
int x, y, z;  
 ...  
main()  
{  
 ...  
    z = x + y + 1;  
 ...  
}  
 
może zostać zastąpiony deklaracją w miejscu zastosowania (w tym  
np. wewnątrz pętli):  
 

211

background image

main()  
{  
 ...  
  for ( int i = 1; i <= 10; i++)  
      cout << "Biezace i wynosi: " << i;  
 ...  
}  
 
Należy jednak pamiętać o pewnym ograniczeniu. Zmienne  
deklarowane poza funkcją main() są traktowane jako zmienne  
globalne i są widoczne (dostępne) dla wszystkich innych  
elementów programu. Zmienne deklarowane wewnątrz bloku/funkcji  
są zmiennymi lokalnymi i mogą "przesłaniać" zmienne globalne.  
Jeśli wielu zmiennym nadamy te same nazwy-identyfikatory, możemy 
 
prześledzić mechanim przesłaniania zmiennych w C++. W  
przykładzie poniżej zastosowano trzy zmienne o tej samej nazwie  
"x":  
 

[P080.CPP]  

//Program demonstruje przesłanianie zmiennych  
 
#include <iostream.h>  
 
int x = 1;                 //Zmienna globalna  
void daj_x(void);          //Prototyp funkcji  
 
main()  
{  
  int x = 22;                    //Zmienna lokalna funkcji main 
  cout << ::x << "    <-- To jest globalny ::x \n";  
  cout << x <<   "    <-- A to lokalny x \n";  
  daj_x();  
  
  return 0;  

 
void daj_x(void)  
{  
  cout << "To ja funkcja daj_x(): \n";  
   
  cout << ::x << " <-- To jest globalny ::x \n";  
  cout << x <<   "    <-- A to lokalny x \n";  
   
  int x = 333;  
  cout << "A to moja zmienna lokalna - automatyczna ! \n";  
  cout << x <<   " <-- tez x ";  
}  
 
Program wydrukuje tekst:  
 
1    <-- To jest globalny ::x   
22    <-- A to lokalny x   
To ja funkcja daj_x():   
1 <-- To jest globalny ::x   
1    <-- A to lokalny x   
A to moja zmienna lokalna - automatyczna !   
333 <-- tez x  
 

212

background image

Zwróć uwagę, że zmienne deklarowane wewnątrz funkcji (tu:  
main()) nie są widoczne dla innych funkcji (tu: daj_x()).  
Operator :: (ang. scope) pozwala nam wybierać pomiędzy zmiennymi 
 
globalnymi a lokalnymi.  
 

TYP WYLICZENIOWY enum JAKO ODRĘBNY TYP ZMIENNYCH.  

 
W C++ od momentu zdefiniowania typu wyliczeniowego enum staje  
się on równoprawnym ze wszystkimi innymi typem danych. Program  
poniżej demonstruje przykład wykorzystania typu enum w C++.  
 

[P081.CPP]  

 
# include <iostream.h>  
  
enum ciuchy  
{  
niewymowne = 1, skarpetka, trampek, koszula, marynarka,   
czapa, peruka, koniec  
};  
  
main()  
{  
  ciuchy n;  
  do 
    {  
      cout << "\nNumer ciucha ? -->  (1-7,   8 = quit): ";  
      cin >> (int) n;  
 
    switch (n) 
       {  
        case niewymowne:               cout << "niewymowne";  
        break;  
        case skarpetka:                cout << "skarpetka";  
        break;  
        case trampek:                  cout << "trampek";  
        break;  
        case koszula:                  cout << "koszula";  
        break;  
        case marynarka:                cout << "marynarka";  
        break;  
        case czapa:                    cout << "czapa";  
        break;  
        case peruka:                   cout << "peruka";  
        break;  
        case koniec:                   break;  
        default: 
                cout << "??? Tego chyba nie nosze...";  
       }  
     } while (n != koniec);  
   
  return 0; 
}  
  
Zwróć uwagę w programie na forsowanie typu (int) przy pobraniu  
odpowiedzi-wyboru z klawiatury. Ponieważ w C++ "ciuchy" stanowią 

213

background image

 
nowy (zdefiniowany przed chwilą) typ danych, do utożsamienia ich 
 
z typem int niezbędne jest wydanie takiego polecenia przy  
pobieraniu danych ze strumienia cin >> . W opcjach pracy  
kompilatora możesz włączyć/wyłączyć opcję "Treat enums as int"  
(traktuj typ enum jak int) i wtedy pominąć forsowanie typu w  
programie. 
 

JEDNOCZESNE ZASTOSOWANIE DWU KOMPILATORÓW.  

 
Jak już wspomnieliśmy wcześniej kompilator C++ składa się w  
istocie z dwu różnych kompilatorów:  
 
* kompilatora C wywoływanego standardowo dla plików *.C,  
* kompilatora C++ wywoływanego standardowo dla plików *.CPP.  
 
Oba kompilatory stosują RÓŻNE metody tworzenia nazw zewnętrznych 
 
(ang. external names). Jeśli zatem program zawiera moduł, w  
którym funkcje zostały przekompilowane w trybie  
charakterystycznym dla klasycznego C - C++ powinien zostać o tym 
 
poinformowany. Dla przykładu, C++  
 
* kategorycznie kontroluje zgodność typów argumentów,  
* na swój własny użytek dodaje do nazw funkcji przyrostki (ang.  
  suffix) pozwalające na określenie typu parametrów,  
* pozwala na tworzenie tzw. funkcji polimorficznych (kilka  
różnych funkcji o tej samej nazwie), itp.  
 
Zwykły C tego nie potrafi i nie robi. Dlatego też do  
wprowadzenia takiego podziału kompetencji należy czasem  
zastosować deklarację  extern "C". Funkcja rand() w programie  
poniżej generuje liczbę losową. 
 

[P081.CPP]  

 
#include <iostream.h>  
  
extern "C" 

# include <stdlib.h>          //Prototyp rand() w STDLIB.H 

  
main()  
{  
  cout << rand();  
  return 0; 
}  
  

GENERACJA LICZB LOSOWYCH.  

 
Kompilatory C++ umożliwoają generację liczb pseudolosowych  
użytecznych często w obliczeniach statystycznych (np. metoda  
Monte Carlo) i emulacji "rozmytaj" arytmetyki i logiki  

214

background image

(ang.fuzzy math). 
 

UWAGA - Liczby PSEUDO-Losowe. 

________________________________________________________________ 
 
Funkcja rand() powoduje uruchomienie generatora liczb  
pseudolosowych. Jeśli chcesz uzyskać liczbę pseudolosową z  
zadanego przedziału wartości, najlepiej zastosuj dzielenie  
modulo:  
 
int n = rand % 10;  
 
powoduje tzw. normalizację. Reszta z dzielenia przez 10 może być 
 
wyłącznie liczbą z przedziału 0...9.  
Aby przy każdym urichomieniu aplikacji ciąg liczb pseudolosowych 
 
rozpoczynał się od innej wartości należy uruchomić generator  
liczb wcześniej - przed użyciem funkcji rand() - np.:  
 
randomize();  
 ...  
int n = rand() % 100;  
 ...  
________________________________________________________________ 
 
W programie przykładowym funkcje z STDLIB.H zostaną skompilowane 
 
przez kompilator C. Określenie trybu kompilacji deklaracją  
extern "C" jest umieszczane zwykle nie wewnątrz programu  
głównego a w dołączanych plikach nagłówkowych *.H. Jest to  
możliwość szczególnie przydatne, jeśli dysponujesz bibliotekami  
funkcji dla C a nie masz chęci, czasu, bądź możliwości  
przerabiania ich na wersję przystosowaną do wymagań C++. Drugi  
przykład poniżej zajmuje się sortowaniem krewnych przy pomocy  
funkcji C qsort().  
 

[P082.CPP]  

  
# include <iostream.h>   
# include <stdlib.h>   
# include <string.h>   
   
extern "C" int comp(const void*, const void*);   
   
main()   
{   
  int max;  
  for(;;)  
    {  
      cout << "\n Ilu krewnych chcesz posortowac? (1...6): ";  
      cin >> max;  
      if( max > 0 && max < 7) break;  
      cout << "\n Nic z tego...";  
    }  
  static char* krewni[] =   
  {   

215

background image

    "Balbina - ciotka",   
    "Zenobiusz - kuzyn",   
    "Kleofas - stryjek",   
    "Ola - kuzynka (ach)",   
    "Waleria - tez niby ciotka",   
    "Halina - stryjenka"  
  };   
  
  qsort(krewni, 6, sizeof(char*), comp);   
  
  for (int n = 0; n < max; n++)   
      cout << "\n" << krewni[n];   
  
  return 0;  
}   
   
extern "C"  
{   
int comp(const void *x, const void *y)   
  {   
    return strcmp(*(char **)x, *(char **)y);   
  }   
}   
   
Program wykonuje następujące czynności:  
 
* deklaruje prototyp funkcji typu C,  
* deklaruje statyczną tablicę wskaźników do łańcuchów znakowych, 
 
* sortuje wskaźniki,  
* wyświetla posortowane łańcuchy znakowe,  
* definiuje funkcję comp() - porównaj,  
* wykorzystuje funkcję biblioteczną C - strcmp() - String  
Compare do porównania łańcuchów znaków.  
 

O PAMIĘCI.  

 
Program w C++ dzieli dostępną pamięć na kilka obszarów o  
określonym z góry przeznaczeniu. Dla zaawansowanego programisty  
zrozumienie i efektywne wykorzystanie mechanizmów zarządzania  
pamięcią w C++ może okazać się wiedzą wielce przydatną.  
Zaczniemy, jak zwykle od "elementarza".  
 

CO PROGRAM ROBI Z PAMIĘCIĄ.  

 
W klasycznym C najczęściej stosowanymi do zarządzania pamięcią  
funkcjami są:  
 
* malloc() - przyporządkuj pamięć,  
* farmalloc() - przyporządkuj odległą pamięć,  
* realloc() - przyporządkuj powtórnie (zmienioną) ilość pamięci, 
* calloc() - przydziel pamięć i wyzeruj, 
* free() - zwolnij pamięć.  
 
Pamięć dzielona jest w obszarze programu na następujące bloki:  
 
                               ___________________ 

216

background image

niskie adresy   -->             Ngłówek programu        I. 
                                Adres startowy    
  KOD:                          Kod programu  
                               ___________________ 
                                Zmienne statyczne      II. 
  DANE:  1. Zainicjowane        Zmienne globalne  
                               ___________________  
                                Zmienne statyczne     III. 
  DANE:  2. Niezainicjowane     Zmienne globalne  
                               ___________________ 
  STERTA: (heap)                W miarę potrzeby       IV. 
                                rośnie w dół.      
                                Tu operują funkcje  
                                malloc(), free().  
                               ___________________  
 POLE NICZYJE:                                          V. 
                                
                               ___________________  
                                W miarę potrzeby        VI. 
  STOS: (stack)                 rośnie w górę.  
wysokie adresy   -->           ___________________  
 
W obszarze kodu (I.) znajdują się instrukcje. Na stosie  
przechowywane są:  
 
* zmienne lokalne,  
* argumenty przekazywane funkcji w momencie jej wywołania,  
* adresy powrotne dla funkcji (RET == CS:IP).  
 
Na stercie natomiast przy pomocy funkcji (a jak przekonamy się  
za chwilę - także operatorów C++) możemy przydzielać pamięć dla  
różnych obiektów tworzonych w czasie pracy programu (ang.  
run-time memory allocation) - np. tworzyć bufory dla łańcuchów,  
tablic, struktur itp.. Zwróć uwagę, że obszar V. - POLE NICZYJE  
może być w czasie pracy programu stopniowo udostępniany dla  
stosu (który rozrasta się "w górę"), albo dla sterty (która  
rozrasta się "w dół"). W przykładowym programie poniżej podano,  
w którym obszarze pamięci zostanie umieszczony dany element  
programu.  
 
# include <alloc.h> 
int a;                             // III.  
int b = 6;                         // II.  
 
main()  
{  
char *Dane; 
 ... 
float lokalna;                     // VI.  
 ...  
Dane = malloc(16);                 // IV.  
 ...  
}  
 

OPERATORY new I delete. 

 
Operatory new i delete działają podobnie do pary funkcji  
malloc() - free(). Pierwszy przyporządkowuje - drugi zwalnia  
pamięć. Dokładniej rzecz biorąc  

217

background image

 
- operator new może zostać zastosowany wraz ze wskaźnikiem do  
  bloku danych określonego typu:  
*  struktury danych,  
*  tablicy, itp. (wkrótce zastosujemy go także w stosunku do  
   klas i obiektów);  
- przyporządkowuje pamięć blokowi danych;  
- przypisuje począkowy adres bloku pamięci wskaźnikowi.  
 
- operator delete zwalnia pamięć przyporządkowaną poprzednio  
blokowi danych,  
 
Operatory new i delete mogą współdziałać z danymi wieloma typami 
 
danych (wcale nie tylko ze strukturami), jednakże rozpoczniemy  
do struktury Data zawierającej datę urodzenia mojej córki.  
 

[P083.CPP] 

 
# include "iostream.h"  
   
struct Data   
{  
  int dzien;  
  int miesiac;   
  int rok;   
};   
   
void main()   
{   
  Data *pointer = new Data;          
  /* Dekl. wskaznik do struct typu Data */  
  /* Przydziel pamiec dla struktury */  
 
   pointer -> miesiac = 11;    // pole "miesiac" = 11  
   pointer -> dzien = 3;   
   pointer -> rok = 1979;   
  
  cout << "\n  URODZINY CORKI: ";  
  cout << pointer -> dzien << '.';   
  cout << pointer -> miesiac << ".  ";   
  cout << "co rok !  od " << pointer -> rok << "  r.";  
  
delete pointer;            //Skasuj wskaznik - zwolnij pamiec.  
}   
  
Program tworzy w pamięci (dokł. na stercie) strukturę typu Data  
bez nazwy. O którą strukturę chodzi i gdzie jej szukać w pamięci 
 
wiemy dzięki wskaźnikowi do struktury *pointer. Zapis  
 
Data *pointer = new Data;  
 
oznacza jednoczesną deklarację i zainicjowanie wskaźnika.  
 

TWORZENIE DYNAMICZNYCH TABLIC O ZMIENNEJ WIELKOŚCI. 

 

218

background image

Jeśli mamy dane wyłącznie jednego typu (tu: int), zastosowanie  
struktury jest właściwie przysłowiowym "strzelaniem z armaty do  
wróbli". Trójelementowa tablica typu  
 
int TAB[3];  
 
zupełnie nam wystarczy. Utworzymy ją jednak nie jako tablicę  
globalną (bądź statyczną) w obszarze pamięci danych, lecz  
dynamicznie - na stercie.  
 

[P084.CPP]  

 
# include "iostream.h"  
  
main()  
{  
int *pointer = new int[3];   // Przydziel pamiec 
 
  pointer[0] = 3;            // Tabl_bez_nazwy[0] - dzien 
  pointer[1] = 11;           // Tabl_bez_nazwy[1] - miesiac 
  pointer[2] = 1979;  
 
  cout << "Data urodzenia:  "; 
  for(int i = 0; i < 3; i++) 
        cout << pointer[i] << '.';  
 
  delete pointer; 
}  
  
Uważny Czytelnik doszedł zapewne do wniosku, że skoro tablica  
tworzona jest dynamicznie w ruchu programu (run-time), to  
kompilator nie musi znać na etapie kompilacji programu  
(compile-time) wielkości tablicy! Idąc dalej, program powinien  
taką techniką tworzyć tablice o takiej wielkośći, jakiej w ruchu 
 
zażyczy sobie użytkownik. Spróbujmy zrealizować to praktycznie.  
 

[P085.CPP]  

 
# include <conio.h>  
# include <stdlib.h>   
# include <iostream.h>   
  
   
void main()   
{   
for(;;)   
 {  
  cout << "\nPodaj wielkosc tablicy (1...100) --> ";   
  int i, size;   
  cin >> size;  
 /* Na stercie tworzymy dynamiczna tablica:         */  
  int *pointer = new int[size];  
  
 /* Wypelniamy tablice liczbami naturalnymi:        */  
  for (i = 0; i < size; i++)  
    pointer[i] = i;  

219

background image

                         cout << "\n    TABLICA:   \n";  
  
 /* Sprawdzamy zawartosc tablicy:                   */  
  for (i = 0; i < size; i++)  
    cout << "  " << pointer[i];   
  
  char k = getch();  
  if(k == 'a') break;  
  delete pointer;  
 }  
}   
   
Twój dialog z programem powinien wyglądać następująco:  
  
Podaj wielkosc tablicy (1...100) -->  20 
    TABLICA:     
  0  1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  
18  19  
Podaj wielkosc tablicy (1...100) -->  100 
    TABLICA:     
  0  1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  
18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  
34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  
50  51  52  53  54  55  56  57  58  59  60  61  62  63  64 65 66 
 
67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  
83  84  85  86  87  88  89  90  91  92  93  94  95 96 97 98 99  
 
Skoro dynamiczne tablice o zmiennej wielkości "chodzą", możemy  
wykorzystać to w bardziej interesujący sposób.  
 

[P086.CPP]  

  
# include <stdlib.h>   
# include <string.h>   
# include <iostream.h>   
  
  
extern "C"  
{   
  int Fporownaj(const void* x, const void* y)   
      {   
         return (strcmp(*(char **)x, *(char **)y));   
      }   
}   
   
main()   
{   
  cout << "Wpisz maksymalna ilosc imion -->  ";   
  
  int ilosc, i;   
  cin >> ilosc;   
  
  char **pointer = new char *[ilosc];   
  
  for (i = 0; i < ilosc; i++)  
    {   
       cout << "Podaj imie Nr: " << i + 1 << "--> ";   
       char *imie = new char[80];   

220

background image

       cin >> imie;   
       if (strcmp(imie, "stop") == 0) break;   
          else  
          pointer[i] = new char[strlen(imie)+1];   
          strcpy(pointer[i], imie);   
          delete imie;   
    }   
  
  qsort(pointer, i, sizeof(char *), Fporownaj);   
  
  for (i = 0; i < ilosc; i++)   
       cout << pointer[i] << '\n';   
  for (i = 0; i < ilosc; i++)   
      delete pointer[i];   
  
  delete pointer;   
  
  return 0;  
}   
   
 
Tworzymy dynamicznie przy pomocy operatora new bezimienną  
tablicę składającą się z tablic niższego rzędu (łańcuch znaków  
to też tablica tyle, że jednowymiarowa - ma tylko długość).  
Zwróć uwagę, że w C++ wskaźnik do wskaźnika (**pointer)  
odpowiada konstrukcji "tablica składająca się z tablic". Aby  
program uczynić bardziej poglądowym spolszczymy nazwy funkcji  
przy pomocy preprocesora.  
 

[P087.CPP]  

  
# define Fporown_string strcmp 
# define Fkopiuj_string strcpy  
# define Fsortuj qsort  
 
# include <stdlib.h>   
# include <string.h>   
# include <iostream.h>   
  
  
extern "C"  
{   
  int Fporownaj(const void* x, const void* y)   
      {   
         return (Fporown_string(*(char **)x, *(char **)y));   
      }   
}   
   
main()   
{   
  cout << "Wpisz maksymalna ilosc imion -->  ";   
  
  int ilosc, i;   
  cin >> ilosc;   
  
  char **pointer = new char *[ilosc];   
  
  for (i = 0; i < ilosc; i++)  
    {   

221

background image

       cout << "Podaj imie Nr: " << i + 1 << "--> ";   
       char *imie = new char[80];   
       cin >> imie;   
       if (Fporown_string(imie, "stop") == 0) break;   
          else  
          pointer[i] = new char[strlen(imie)+1];   
          Fkopiuj_string(pointer[i], imie);   
          delete imie;   
    }   
/* w tym momencie  i == ilosc                            */ 
  Fsortuj(pointer, i, sizeof(char *), Fporownaj);   
  
  for (i = 0; i < ilosc; i++)   
       cout << pointer[i] << '\n';   
  for (i = 0; i < ilosc; i++)   
      delete pointer[i];   
  
  delete pointer;   
  
  return 0;  
}   
 
Wskaźnik może wskazywać dane o różnym stopniu złożoności:  
zmienną, tablicę, strukturę, obiekt (o czym za chwilę), ale może 
 
wskazywać także funkcję.  
 

JEŚLI ZABRAKNIE PAMIĘCI - _new_handler. 

 
Aby obsługiwać błędną sytuację - brakło pamięci na stercie -  
potrzebna nam będzie funkcja - tzw. HANDLER. Aby jedna było  
wiadomo, gdzie szukać handlera, powinniśmy operatorowi new  
przekazać informację jaka funkcja obsługuje brak pamięci i gdzie 
 
jej szukać.  
 
Możemy podstawiać na miejsce funkcji stosowanej w programie tę  
funkcję, która w danym momencie jest nam potrzebna. Jest to  
praktyka często stosowana w programach obiekktowych, więc  
przypomnijmy raz jeszcze przykładowy program - tym razem w  
trochę innym kontekście. Aby wskazać funkcję zastosujemy  
wskaźnik. . Przypomnijmy deklarację 
 
double ( *Funkcja ) (double);  
 

[P088.CPP]  

  
#include <conio.h> 
#include <math.h>   
#include <iostream.h>   
 
double Nasza_F(double);      //Deklaracja zwyklej funkcji  
double (*Funkcja)(double);   //pointer do funkcji  
  
double liczba;        //zwyczajna zmienna  
int wybor;   
  

222

background image

int main(void)   
{   
  clrscr(); 
  cout << "\nPodaj Liczbe \n";   
  cin >> Liczba;   
  cout << "CO OBLICZYC ?\n________________\n";   
  cout<<"1 - Sin \n2 - Cos \n3 - Odwrotnosc 1/X\n";  
 
    switch(cin >> wybor)   
    {   
    case 1: Funkcja = sin; break;   
    case 2: Funkcja = cos; break;   
    case 3: Funkcja = Nasza_F; break;   
    }   
  cout << "\n\nWYNIK = " << Funkcja(liczba);  
  return (0); 
}  
   
double Nasza_F(double x)   
{   
  if (x != 0)       
      x = 1/x;   
  else  
      cout <<  "???\n";  
  return x;   
}   
  
Komputer nie jest "z gumy" i nie posiada dowolnie dużej  
"rozciągliwej" pamięci. Funkcja malloc(), jeśli pamięci  
zabraknie, zwraca pusty wskaźnik (ang. NULL pointer), co można  
łatwo przetestować w programie. Jeśli natomiast stosujemy  
operator new - konsekwentnie - operator new powinien zwracać  
NULL (i próbować dokonać przypisania pointerowi zero). To też  
można sprawdzić w programie.  
 
W C++ istnieje jednak również inny, przydatny do tych celów  
mechanizm. C++ dysponuje globalnym wskaźnikiem _new_handler  
(wskaźnik do funkcji obsługującej operator new, jeśli zabraknie  
pamięci). Dzięki istnieniu tego (predefiniowanego) wskaźnika  
możemy przyporządkować "handler" - funkcję obsługującą wyjście  
przez operator new poza dostępną pamięć.  
 
Dopóki nie zażyczymy sobie inaczej, wskaźnik  
 
_new_handler == NULL            // NULL == 0 
 
i operator new w przypadku niepowodzenia próby przyporządkowania 
 
pamięci zwróci wartość NULL inicjując pusty wskaźnik (innymi  
słowy "wskaźnik do nikąd"). Jeśli jednak  
 
_new_handler != NULL  
 
to zawartość wskaźnika zostanie przez operator new uznana za  
adres startowy funkcji obsługi błędnej sytuacji (ang. addres to  
call).  
 

[P089.CPP]  

  

223

background image

# include <stdlib.h>   
# include <iostream.h>   
  
static void Funkcja()   
{   
  cout << "\nTo ja ... Funkcja - handler \n";  
  cout << '\a' << " ! BRAK PAMIECI ! ";   
  exit (1);   
}   
   
  extern void (*_new_handler)();   
  long suma;                         //Automatycznie suma = 0; 
 
void main()   
{   
  _new_handler = Funkcja;            //Inicjujemy wskaznik  
 
  for(;;)  
    {   
      char *pointer = new char[8192];   
      suma += 8192;   
      cout << "\nMam juz " << suma << " znakow w RAM\n";   
      if (pointer != 0)   
      cout << "Pointer != NULL";  
   }   
}   
   
 

SPRAWDŹ - KONIECZNIE! 

________________________________________________________________ 
W programach użytkowych, a szczególnie w tych oferowanych  
klientom jako produkty o charakterze komercyjnym należy ZAWSZE  
sprawdzać poprawność wykonania newralgicznych operacji - a  
szczególnie poprawność zarządzania pamięcią i poprawność  
operacji dyskowych. Utrata danych, lub nie zauważone i nie  
wykryte przez program przekłamanie może spowodować przykre  
skutki. Raz utracone dane mogą okazać sie nie do odzyskania. 
______________________________________________________________ 

LEKCJA 24   : SKĄD WZIĘŁY SIĘ KLASY I OBIEKTY W C++.  

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się, skąd w C++ biorą się obiekty i 
jak z nich korzystać.  
________________________________________________________________ 
 
 
Zajmiemy się teraz tym, z czego C++ jest najbardziej znany -  
zdolnością posługiwania się obiektami. Główną zaletą  
programowania obiektowego jest wyższy stopień "modularyzacji"  
programów. "Mudularyzacja" jest tu rozumiana jako możliwość  
podziału programu na niemal niezależne fragmenty, które mogą  
opracowywać różne osoby (grupy) i które później bez konfliktów  
można łączyć w całość i uruchamiać natychmiast. C++ powstał, gdy 
programy stały się bardzo (zbyt) długie. Możliwość skrócenia  

224

background image

programów nie jest jednakże jedyną zaletą C++. W długich,  
rozbudowanych programach trudno spamiętać szczegóły dotyczące  
wszystkich części programu. Jeśli grupy danych i grupy funkcji  
uda się połączyć w moduły, do których można później sięgać, jak  
do pewnej odrębnej całości, znacznie ułatwia to życie  
programiście. Na tym, w pewnym uproszczeniu, polega idea  
programowania obiektowego.  
 

JAK STRUKTURY STAWAŁY SIĘ OBIEKTAMI. 

 
W C++ struktury uzyskują "trochę więcej praw" niż w klasycznym  
C. Przykładowy program poniżej demonstruje kilka sposobów  
posługiwania się strukturą w C++.  
 

[P90.CPP]  

  
#include <iostream.h>   
   
struct Data   
{   
  int dzien;   
  int miesiac;   
  int rok;   
};   
  
Data NaszaStruktura = {3, 11, 1979}; //Inicjujemy strukture  
Data daty[16];                          //Tablca struktur   
Data *p = daty;                         //Wskaznik do tablicy  
void Fdrukuj(Data);                     //Prototyp funkcji  
int i;                                  //Licznik automat. 0  
  
int main()   
{  
  for (; i < 16; i++)  
   {   
     *(p + i) = NaszaStruktura;   
     daty[i].rok += i;   
     cout << "\nDnia ";   
     Fdrukuj(daty[i]);   
     cout << " Patrycja ";   
     if ( !i ) cout << "urodzila sie, wiek - ";  
     if (i > 0 && i < 14) cout << "miala ";   
     if (i > 13) cout << "bedzie miec ";  
     cout << i;  
     if (i == 1) cout << " roczek";   
        else cout << " lat";  
     if (i > 1 && i < 5) cout << "ka";  
     cout << '.';  
   }   
  return 0;  
}   
   
void Fdrukuj(Data Str)   
{   
char *mon[] =   
  {   
   "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca",   

225

background image

   "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada",   
   "Grudnia"  
  };   
cout << Str.dzien << ". "   
     << mon[Str.miesiac-1] << ". "  
     << Str.rok;   
}   
   
Prócz danych struktury w C++ mogą zawierać także funkcje. W  
przykładzie poniżej struktura Data zawiera wewnątrz funkcję,  
która przeznaczona jest do obsługi we właściwy sposób danych  
wchodzących w skład własnej struktury.  
 

[P091.CPP]  

  
#include <iostream.h>    
    
struct Data                         //Definicja struktury  
{    
  int dzien, miesiac, rok;  
  void Fdrukuj();                   //Prototyp funkcji   
  Data();                           //Konstruktor struktury  
};    
  
void Data::Fdrukuj()                //Definicja funkcji  
{    
char *mon[] =    
  {    
   "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca",    
   "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada",    
   "Grudnia"   
  };    
cout << dzien << ". "    
     << mon[miesiac-1] << ". "   
     << rok;    
}    
    
Data::Data(void)       //Poczatkowa data - Konstruktor 
{   
    dzien = 3;   
    miesiac = 11;   
    rok = 1979;   
}  
  
int main()    
{   
    Data NStruktura;             //Inicjujemy strukture  
  
    cout << "\n  Sprawdzamy: ";   
            NStruktura.Fdrukuj();   //Wywolanie funkcji 
    cout << " = ";  
    cout << NStruktura.dzien   << " . "   
         << NStruktura.miesiac << " . "   
         << NStruktura.rok;  
  
  for (int i=0; i < 16; i++, NStruktura.rok++)   
   {    
     cout << "\nDnia ";    
     NStruktura.Fdrukuj();    

226

background image

     cout << " Patrycja ";    
     if ( !i ) cout << "urodzila sie, wiek - ";   
     if (i > 0 && i < 14) cout << "miala ";    
     if (i > 13) cout << "bedzie miec ";   
     cout << i;   
     if (i == 1) cout << " roczek";    
        else cout << " lat";   
     if (i > 1 && i < 5) cout << "ka";   
     cout << '.';   
   }    
  return 0;   
}    
  
Zwróć uwagę, że  
* odkąd dane stały się elementem struktury, zaczęliśmy odwoływać 
 
się do nich tak:  
                        nazwa_struktury.nazwa_pola;  
* gdy funkcje stały się elementem struktury, zaczęliśmy  
 odwoływać się do nich tak:  
                        nazwa_struktury.nazwa_funkcji;  
 
Pojawiły się również różnice w sposobie definiowania funkcji:  
 
void Data::Fdrukuj()            //Definicja funkcji  
{  
 ...  
}  
 
oznacza, że funkcja Fdrukuj() jest upoważniona do operowania na  
wewnętrznych danych struktur typu Data i nie zwraca do programu  
żadnej wartości (void). Natomiast zapis:  
 
Data::Data(void)       //Poczatkowa data - Konstruktor  
 
oznacza, że funkcja Data(void) nie pobiera od programu żadnych  
parametrów i tworzy (w pamięci komputera) strukturę typu Data.  
Takie dziwne funkcje konstruujące (inicjujące) strukturę (o czym 
 
dokładniej w dalszej części książki), nazywane w C++  
konstruktorami nie zwracają do programu żadnej wartości. Zwróć  
uwagę, że konstruktory to specjalne funkcje, które:  
 
-- mają nazwę identyczną z nazwą typu własnej struktury,  
-- nie posiadają wyspecyfikowanego typu wartości zwracanej do  
   programu,  
-- służą do zainicjowania w pamięci pól struktury,  
-- nie są wywoływane w programie w sposób jawny, lecz niejawnie, 
 
   automatycznie.  
 
Podstawowym praktycznym efektem dodania do struktur funkcji  
stała się możliwość skutecznej ochrony danych zawartych na  
polach struktury przed dostępem funkcji z zewnątrz struktury.  
Przed dodaniem do struktury jej własnych wewnętrznych funkcji -  
wszystkie funkcje pochodziły z zewnątrz, więc "hermetyzacja"  
danych wewnątrz była niewykonalna. Zasady dostępu określa się w  
C++ przy pomocy słów:  
 
public - publiczny, dostępny,  

227

background image

protected - chroniony, dostępny z ograniczeniami,  
private - niedostępny spoza struktury.  
 

Przykładowy program poniżej demonstruje tzw. "hermetyzację"  

struktury (ang. encapsulation). W przykładzie poniżej:  
 
* definiujemy strukturę;  
* definiujemy funkcje;  
* przekazujemy i pobieramy dane do/od struktury typu Zwierzak. 
 
Zmienna int schowek powinna sugerować ukrytą przez strukturę i  
niedostępną dla nieuprawnionych funkcji część danych struktury a 
 
nie cechy anatomiczne zwierzaka.  
 

[STRUCT.CPP]  

 
# include "iostream.h"  
 
 //UWAGA: schowek ma status private, jest niedostepny 
 
struct Zwierzak  
{  
private: 
   int schowek;            //DANE PRYWATNE - niedostepne 
public:  
   void SCHOWAJ(int Xwe);  //Funkcje dostepne zzewnatrz  
   int ODDAJ(void);  
};  
  
void Zwierzak::SCHOWAJ(int Xwe) //definicja funkcji 
{  
   schowek = Xwe;  
}  
  
int Zwierzak::ODDAJ(void)  
{  
   return (schowek);  
}  
  
main()  
{  
Zwierzak Ciapek, Azor, Kotek; // Struktury "Zwierzak"  
int Piggy;                    // zwykla zmienna  
  
   Ciapek.SCHOWAJ(1);  
   Azor.SCHOWAJ(22);  
   Kotek.SCHOWAJ(-333);  
   Piggy = -4444;  
  
   cout << "Ciapek ma: " << Ciapek.ODDAJ() << "\n";  
   cout << "Azor ma:   " << Azor.ODDAJ() << "\n";  
   cout << "Kotek ma:  " << Kotek.ODDAJ() << "\n";  
   cout << "Panna Piggy ma: " << Piggy << "\n";  
 
return 0;  
}  

228

background image

  
// Proba nieautoryzowanego dostepu do danych prywatnych obiektu: 
// cout << Ciapek.schowek;  
// printf("%d", Ciapek.schowek); 
// nie powiedzie sie  
 
Powiedzie sie natomiast próba dostępu do "zwykłej" zmiennej -  
dowolną metodą - np.:  
 
printf("%d", Piggy);     //Prototyp !    # include <stdio.h> 
 
Jeśli podejmiesz próbę odwołania się do "zakapsułkowanych"  
danych w zwykły sposób - np.:  
 
cout << Ciapek.schowek;  
 
kompilator wyświetli komunikat o błędzie:  
 
Error: 'Zwierzak::schowek' is not accessible in function main()  
(pole schowek struktury typu Zwierzak (np. str. Ciapek) nie jest 
 
dostępne z wnętrza funkcji main(). )  
 
Do klas i obiektów już tylko maleńki kroczek. Jak przekonasz się 
 
za chwilę - struktura Ciapek jest już właściwie obiektem, a typ  
danych Zwierzak jest już właściwie klasą obiektów. Wystarczy  
zamienić słowo "struct" na słowo "class".  
 

[CLASS.CPP]  

 
# include "iostream.h"  
 
        //w klasach schowek ma status private AUTOMATYCZNIE 
        //slowo private stalo sie zbedne 
 
class Zwierzak  
{  
   int schowek;   
public:  
   void SCHOWAJ(int Xwe);  //Funkcje dostepne zzewnatrz  
   int ODDAJ(void);  
};  
  
void Zwierzak::SCHOWAJ(int Xwe) 
{  
   schowek = Xwe;  
}  
  
int Zwierzak::ODDAJ(void)  
{  
   return (schowek);  
}  
  
main()  
{  
Zwierzak Ciapek, Azor, Kotek; // obiekty klasy "Zwierzak"  
int Piggy;                    // zwykla zmienna  
  

229

background image

   Ciapek.SCHOWAJ(1);  
   Azor.SCHOWAJ(22);  
   Kotek.SCHOWAJ(-333);  
   Piggy = -4444;  
  
   cout << "Ciapek ma: " << Ciapek.ODDAJ() << "\n";  
   cout << "Azor ma:   " << Azor.ODDAJ() << "\n";  
   cout << "Kotek ma:  " << Kotek.ODDAJ() << "\n";  
   cout << "Panna Piggy ma: " << Piggy << "\n";  
 
return 0;  
}  
 
Kompilator nawet nie mrugnął. Zmiana słowa struct na słowo class 
 
nie sprawiła mu zatem widocznie przykrości. Mało tego, zwróć  
uwagę, że długość wynikowego pliku STRUCT.EXE i CLASS.EXE jest  
IDENTYCZNA. Wynikałoby z tego, że sposób tworzenia wynikowego  
kodu przez kompilator w obu wypadkach był identyczny. 
 

O KLASACH I OBIEKTACH.  

 
Klasy służą do tworzenia formalnego typu danych. W przypadku  
klas wiadomo jednak "z definicji", że będzie to bardziej złożony 
 
typ (tzw. agregat) zawierający praktycznie zawsze i dane  
"tradycyjnych" typów i funkcje (nazywane "metodami"). Podobnie  
jak definiując strukturę tworzysz nowy formalny typ danych, tak  
i tu - definiując klasę tworzysz nowy typ danych. Jeśli  
zadeklarujesz użycie zmiennych danego typu formalnego, takie  
zmienne to właśnnie obiekty. Innymi słowy, klasy stanowią  
definicje formalnego typu, natomiast obiekty - to zmienne danego 
 
typu (danej klasy).  
 
Zamiast słowa struct stosujemy przy klasach słowo class.  
 
class Klasa  
{  
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void);  
     int Funkcja(int arg);  
};  
 
Nasza pierwsza świadomie tworzona klasa nazywa się "Klasa" i  
stanowi nowy formalny typ zmiennych. Jeśli zadeklarujesz zmienną 
 
takiej klasy (tego typu formalnego), to taka zmienna będzie  
właśnie OBIEKTEM.  
 
Nasza pierwsza prawdziwa Klasa zawiera dane:  
 
prywatna_tab[80] - prywatną tablicę;  
dane - publiczną daną prostą typu int;  
oraz funkcje:  
Inicjuj() - zainicjuj - utwórz obiekt danej klasy w pamięci;  
Funkcja() - jakaś funkcja publiczna.  

230

background image

 
Gdyby była to zwykła struktura, jej definicja w programie  
wyglądałaby tak:  
 
struct Klasa  
{  
private: 
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void);  
     int Funkcja(int arg);  
};  
 
 
Jeżeli w dalszej części programu chcielibyśmy zastosować  
struktury takiego typu, deklaracja tych struktur musiałaby  
wyglądać tak:  
 
struct rodzaj_struktur  
{  
private: 
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void);  
     int Funkcja(int arg);  
} str1, str2, .... , nasza_struktura; 
 
bądź tak: 
  
struct rodzaj_struktur  
{  
private: 
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void);  
     int Funkcja(int arg);  
}; 
 ...  
(struct) rodzaj_struktur  str1, str2, .... , nasza_struktura; 
 
Słowo kluczowe struct jest opcjonalne. Moglibyśmy więc  
zadeklarować strukturę w programie, wewnątrz funkcji main(): 
 
struct rodzaj_struktur  
{  
private: 
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void);  
     int Funkcja(int arg);  
};  
 
main()  
{  
 ... 
  struct rodzaj_struktur nasza_struktura;  

231

background image

//lub równoważnie:  
  rodzaj_struktur nasza_struktura;  
 
Do pól struktury możemy odwoływać się przy pomocy operatora  
kropki (ang. dot operator). Podobnie dzieje się w przypadku  
klas. Jeśli zadeklarujemy zmienną typu Klasa, to ta zmienna  
będzie naszym pierwszym obiektem. 
 
class Klasa  
{  
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void)  
     int Funkcja(int our_param);  
} Obiekt;  
 
Podobnie jak wyżej, możemy zadeklarować nasz obiekt wewnątrz  
funkcji main():  
 
class Klasa  
{  
     int prywatna_tab[80]  
public:  
     int dane; 
     void Inicjuj(void)  
     int Funkcja(int argument);  
};  
 
main()  
{  
 ... 
  Klasa Obiekt;  
 ...  
 
Przypiszemy elementom obiektu wartości:  
 
main()  
{  
 ... 
  Klasa Obiekt;  
  Obiekt.dane = 13;  
 ...  
 
Taką samą metodą, jaką stosowaliśmy do danych - pól struktury,  
możemy odwoływać się do danych i funkcji w klasach i obiektach. 
 
main()  
{  
 ... 
   Klasa Obiekt;  
   Obiekt.dane = 13;     Obiekt.Funkcja(44); 
  ... 
 
Przyporządkowaliśmy obiektowi nie tylko dane, ale także funkcje  
poprzez umieszczenie prototypów funkcji wewnątrz deklaracji  
klasy:  
 
class Klasa  
{  

232

background image

 ... 
public:  
 ... 
     void Inicjuj(void)              /* Prototypy funkcji */ 
     int Funkcja(int argument);  
};  
 

UWAGA! 

________________________________________________________________ 
W C++ nie możemy zainicjować danych wewnątrz deklaracji klasy:  
 
class Klasa  
{  
private: 
     int prywatna_tab[80] = { 1, 2, ... };     //ŹLE !  
public:  
     int dane = 123;                    //ŹŁE ! 
 ...  
________________________________________________________________ 
 
Inicjowanie danych odbywa się w programie głównym przy pomocy  
przypisania (dane publiczne), bądź za pośrednictwem funkcji  
należącej do danej klasy i mającej dostęp do wewnętrznych danych 
 
klasy/obiektu (dane prywatne). Inicjowania danych mogą dokonać  
także specjalne funkcje - tzw. konstruktory. 
 
Dane znajdujące się wewnątrz deklaracji klasy mogą mieć status  
public, private, bądź protected. Dopóki nie zażądasz inaczej -  
domyślnie wszystkie elementy klasy mają status private. Jeżeli  
część obiektu jest prywatna, to oznacza, że żaden element  
programu spoza obiektu nie ma do niej dostępu. W naszej Klasie  
prywatną część stanowi tablica złożona z liczb całkowitych: 
 
 (default - private:) int prywatna_tab[80];  
 
Do (prywatnych) elementów tablicy dostęp mogą uzyskać tylko  
funkcje związane (ang. associated) z obiektem danej klasy.  
Funkcje takie muszą zostać zadeklarowane wewnątrz definicji  
danej klasy i są nazywane członkami klasy - ang. member  
functions. Funkcje mogą mieć status private i stać się dzięki  
temu wewnętrznymi funkcjami danej klasy (a w konsekwencji  
również prywatnymi funkcjami obiektów danej klasy). Jest to  
jedna z najważniejszych cech nowoczesnego stylu programowania w  
C++. Na tym polega idea hermetyzacji danych i funkcji wewnątrz  
klas i obiektów. Gdyby jednak cała zawartość (i dane i funkcje)  
znajdujące się w obiekcie zostały dokładnie "zakapsułkowane", to 
 
okazałoby się, że obiekt stał się "ślepy i głuchy", a w  
konsekwencji - niedostępny i kompletnie nieużyteczny dla  
programu i programisty. Po co nam obiekt, do którego nie możemy  
odwołać się z zewnątrz żadną metodą? W naszym obiekcie, w  
dostępnej z zewnątrz części publicznej zadeklarowaliśmy zmienną  
całkowitą dane oraz dwie funkcje - Inicjuj() oraz Funkcja().  
Jeśli dane i funkcje mają status public, to oznacza, że możemy  
się do nich odwołać z dowolnego miejsca programu i dowolnym  
sposobem. Takie odwołania przypominają sposób odwoływania się do 
 
elementów struktury:  

233

background image

 
main()  
{  
 ... 
Obiekt.dane = 5;            //Przypisanie wartości zmiennej.  
Obiekt.Inicjuj();           //Wywołanie funkcji Inicjuj()  
 ... 
Obiekt.Funkcja(3);          //Wywołanie funkcji z argumentem 
  

ZAWSZE PUBLIC ! 

_______________________________________________________________
Dane zawarte w obiekcie, podobnie jak zwykłe zmienne wymagają  
zainicjowania. Funkcja inicjująca dane - zawartość obiektu musi  
zawsze posiadać status public aby mogła być dostępna z zewnątrz  
i zostać wywołana w programie głównym - funkcji main(). Funkcje  
i dane dostępne z zewnątrz stanowią tzw. INTERFEJS OBIEKTU. 
_______________________________________________________________ 

LEKCJA 25: PRZYKŁAD OBIEKTU.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak praktycznie projektuje się 
klasy i obiekty. Twój pierwszy obiekt zacznie działać.  
________________________________________________________________ 
 

Nasz pierwszy, doświadczalny obiekt będzie zliczać ile razy użytkownik 
nacisnął określony klawisz - np. literę "A".

 Najpierw 
podejdziemy do problemu "klasycznie". Utworzymy strukturę  
Licznik, którą można wykorzystać do przechowywania istotnych dla 
nas informacji: 
 
char znak - znak do zliczania  
int ile - ile razy wystąpił dany znak.  
 
Zwróć uwagę, że Licznik oznacza tu typ struktur (nowy formalny  
typ danych) a licznik oznacza naszą roboczą zmienną danego typu. 
 
struct Licznik    //Licznik - nowy typ struktur 
{   
public:          //Status public jest domyślny dla struktur 
  char znak;  
  int ile;  
 ... 
} licznik;        //Zmienna typu "Licznik" 
 
Do pól struktury licznik.znak i licznik.ile możemy odwoływać się 
 
w programie w następujący sposób:  
 
//Przypisanie (zainicjowanie pola struktury)  
     licznik.znak = 'A';    
     cin >> licznik.znak; 
 

234

background image

//Odczyt (wyprowadzenie) bież. zawartości pola struktury. 
     cout << licznik.znak;   
 
Potrzebna nam będzie funkcja, przy pomocy której przekażemy do  
struktury informację, jaki znak powinien być zliczany. Nazwijmy  
tę funkcję Inicjuj(). Funkcja Inicjuj() powinna nam zainicjować  
pole struktury tzn. po przekazaniu jej jako argumentu tego  
znaku, który ma podlegać zliczaniu, funkcja powinna "przenieść"  
znak i zapisać go w polu licznik.znak naszej roboczej struktury. 
 
Wywołanie funkcji w programie powinno wyglądać tak:  
 
main()  
{  
 ....  
Inicjuj('A');   
 ....  
  //UWAGA: Nie tak:  
  //licznik.Inicjuj() - funkcja jest zewnętrzna ! 
 
Aby funkcja inicjująca pole struktury zadziałała prawidłowo, jej 
 
definicja powinna wyglądać tak:  
 
void Inicjuj(char x)       //Deklaracja zmiennej znak.  
{  
  licznik.znak = x;        //x - wewnętrzna zmienna funkcji  
  licznik.ile = 0;  
}  
 
Inicjując strukturę licznik funkcja zeruje pole "ile" struktury. 
 
Przyda nam się jeszcze jedna funkcja PlusJeden(). Ta funkcja  
powinna zwiększyć zmienną służącą do zliczania ile razy wystąpił 
 
interesujący nas znak po każdym pojawieniu się odpowiedniego  
znaku (w tym przypadku "A").  
 
void PlusJeden(void)          //Definicja funkcji  
{                             //incrementującej licznik 
    licznik.ile++; 

 
Zbudowaliśmy licznik, który składa się z danych rozmieszczonych  
na polach struktury oraz dwu stowarzyszonych ze strukturą  
funkcji. Jeśli spróbujemy zastosować to w programie, gdzie:  
 
char znak_we - znak wczytany z klawiatury;  
 
program będzie wyglądać tak:  
 
void main()  
{  
  char znak_we;  
  Inicjuj('A');   
 
  cout << "\nWpisz tekst zawierajacy litery A" 
  cout << "\nK - oznacza Koniec zliczania: ";  
 
  for(;;)                          //Wczytujemy znaki 

235

background image

    { 
      cin >> znak_we;  
      if (znak_we == 'k' || znak_we == 'K') break; 
      if(znak_we == licznik.znak) PlusJeden();  
    }  
....  
 
W tekście mogą wystąpić zarówno duże jak i małe litery. Jeśli  
zechcemy zliczać i jedne i drugie, możemy posłużyć się funkcją  
biblioteczną C zamieniającą małe litery na duże - toupper().  
Najpierw poddamy wczytany zank konwersji a następnie porównamy z 
 
"zadanym" na polu licznik.znak:  
 
if(licznik.znak == toupper(znak_we)) PlusJeden();  
 
Po przerwaniu pętli przez użytkownika wystarczy sprawdzić jaka  
wartość jest wpisana w polu licznik.ile i możemy wydrukować  
wynik zliczania wystąpień litery 'A' we wprowadzonym tekście. 
 
  cout << "\nLitera " << licznik.znak  
       << " wystąpila " << licznik.ile  
       <<   " razy.";  
 
Program w całości będzie wyglądał tak: 
 

[P092.CPP] 

  
# include <iostream.h>   
# include <ctype.h>           //Prototyp f. toupper()  
  
struct Licznik    
{    
  char znak;   
  int ile;   
} licznik;    
  
void Inicjuj(char x)    
{   
  licznik.znak = x;   
  licznik.ile = 0;   
}   
  
void PlusJeden(void)   
{   
    licznik.ile++;  
}  
  
void main()  
{   
  char znak_we;   
  Inicjuj('A');    
  
  cout << "\nWpisz tekst zawierajacy litery A";  
  cout << "\nPierwzse wytapienie litery k lub K";  
  cout << "\n - oznacza Koniec zliczania: ";   
  
  for(;;)    
    {  

236

background image

      cin >> znak_we;   
      if (znak_we == 'k' || znak_we == 'K') break;  
      if(licznik.znak == toupper(znak_we)) PlusJeden();   
    }   
  
  cout << "\nLitera " << licznik.znak   
       << " wystapila " << licznik.ile   
       <<   " razy.";   
}  
  
Jeśli dane i funkcje połączymy w jedną całość - powstanie  
obiekt. Zawartość naszego obiektu powinna wyglądać tak:  
 
Dane: 
char znak;   
int ile;   
Funkcje: 
void Inicjuj(char);  
void PlusJeden(void);  
 
Łączymy w całość funkcje operujące pewnymi danymi i te właśnnie  
dane. Co więcej, jeśli zaistnieją takie funkcje, które nie będą  
wykorzystywane przez nikogo więcej poza własnym obiektem i poza  
jego składnikami: funkcją Inicjuj() i funkcją PlusJeden(),  
funkcje te nie muszą być widoczne, ani dostępne dla reszty  
programu. Takie funkcje mogą wraz z danymi zostać uznane za  
prywatną część obiektu. Takie praktyki, szczególnie w programach 
 
przeznaczonych dla środowiska Windows są uzasadnione i  
pożyteczne. Rozważmy obiekt, modularyzację i hermetyzację  
obiektu na konkretnych przykładach.  
 

Zacznijmy od zdefiniowania klasy. 

 
class Licznik 
{  
  char znak;  
  int ile;  
public: 
   void Inicjuj(char);  
   void PlusJeden(void);  
};  
 
Następny krok, to zdefiniowanie obu funkcji. Zwróć uwagę, że  
funkcje nie są już definiowane "niezależnie", lecz w stosunku do 
 
własnej klasy: 
 
void Licznik::Inicjuj(char x)  
{  
  znak = x;  
  ile = 0;  
}  
 
void Licznik::PlusJeden(void)  
{  
  ile++;  
}  
 

237

background image

Skoro funkcje widzą już wyłącznie własną klasę, zapis  
 
licznik.znak    może zostać uproszczony do -->    znak 
i  
licznik.ile                             do -->      ile  
 
Aby wskazać, że funkcje są członkami klasy Licznik stosujemy  
operator :: (oper. widoczności/przesłaniania - ang. scope  
resolution operator). Taki sposób zapisu definicji funkcji  
oznacza dla C++, że funkcja jest członkiem klasy (ang. member  
function). Logika C++ w tym przypadku wygląda tak:  
 
* Prototypy funkcji należy umieścić w definicji klasy.  
* Definicje funkcji mogą znajdować się w dowolnym miejscu  
programu, ponieważ operator przesłaniania :: pozwala rozpatrywać 
 
klasę podobnie jak zmienne globalne.  
 
* Wstawiając operator :: pomiędzy nazwę klasy i prototyp funkcji 
 
informujemy C++ że dana funkcja jest członkiem określonej klasy. 
 
 
Funkcje - członkowie klas nazywane są często METODAMI. 
Definicje klas i definicje funkcji - METOD są często umieszczane 
 
razem - w plikach nagłówkowych. Jeśli posługujemy się taką  
metodą, wystarczy dołączyć odpowiedni plik dyrektywą # include.  
Kompilator C++ skompiluje wtedy automatycznie wszystkie funkcje, 
 
które znajdzie w dołączonych plikach nagłówkowych. 
 
Możemy przystąpić do utworzenia programu.  
 
main()  
{  
  char znak_we;            //Dekl. zwyklej zmiennej 
  Licznik licznik;         //Deklarujemy obiekt klasy Licznik 
  licznik.Inicjuj('A');    //Inicjujemy licznik  
 ...  
 
Możemy teraz określić ilość wprowadzonych z klawiatury liter 'A' 
 
oraz 'a' i wyprowadzić  ją na ekran monitora. Pojawia się jednak 
 
pewien problem. Nie uda się sięgnąć z zewnątrz do prywatnych  
danych obiektu tak, jak poprzednio: 
 
      if(licznik.znak == toupper(znak_we)) ....  
 
Potrzebna nam będzuie jeszcze jedna metoda autoryzowana do  
dostępu do danych obiektu:  
 
char Licznik::Pokaz(void);  
 
która nie będzie w momencie wywołania pobierać od programu  
żadnych argumentów (void), natomiast pobierze znak z pola char  
Licznik.znak i przekaże tę informację w postaci zmiennej typu  
char do programu. Definicja takiej metody powinna być  
następująca:  

238

background image

 
char Licznik::Pokaz(void)  

  return znak;  

 
Ten sam problem wystąpi przy próbie pobrania od obiektu efektów  
jego pracy - stanu pola licznik.ile. Do tego też niezbędna jest  
autoryzowana do dostępu metoda. Nazwiemy ją Efekt():  
 
int Licznik::Efekt(void)  
{  
  return ile;  
}  
 

Program w wersji obiektowej będzie wyglądać tak:  

 

[P093.CPP] 

 
# include <ctype.h>  
# include <iostream.h>  
  
class Licznik  
{   
  char znak;   
  int ile;   
public:  
   void Inicjuj(char);   
   void PlusJeden(void);   
   char Pokaz(void);   
   int Efekt(void);  
};   
  
void main()   
{   
  char znak_we;  
  Licznik licznik;    
  licznik.Inicjuj('A');   
  
  cout << "\nWpisz tekst zawierajacy litery A";   
  cout << "\nPierwsze wytapienie litery k lub K";   
  cout << "\n - oznacza Koniec zliczania: ";    
   
  for(;;)     
    {   
      cin >> znak_we;    
      if (znak_we == 'k' || znak_we == 'K') break;   
      if(licznik.Pokaz() == toupper(znak_we))   
         licznik.PlusJeden();    
    }    
   
  cout << "\nLitera " << licznik.Pokaz()  
       << " wystapila " << licznik.Efekt()    
       <<   " razy.";    
}   
   
/* Definicje wszystkich funkcji:                          */  

239

background image

  
void Licznik::Inicjuj(char x)   
{   
  znak = x;   
  ile = 0;   
}   
  
void Licznik::PlusJeden(void)   
{   
  ile++;   
}   
  
char Licznik::Pokaz(void)  
{  
  return znak;   
}  
  
int Licznik::Efekt(void)   
{   
  return ile;   
}   
  
Przejdziemy teraz do bardziej szczegółowego omówienia  
zasygnalizowanego wcześniej problemu inicjowania struktur w  
pamięci przy pomocy funkcji o specjalnym przeznaczeniu - tzw.  
KONSTRUKTORÓW.

LEKCJA 26: CO TO JEST KONSTRUKTOR. 

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, w jaki sposób w pamięci  
komputera są tworzone obiekty.  
________________________________________________________________ 
 
 
C++ zawiera specjalną kategorię funkcji - konstruktory w celu  
automatyzacji inicjowania struktur (i obiektów). Konstruktory to 
specjalne funkcje będące członkami struktur (kategorii member  
functions) które są automatycznie wywoływane i dokonują  
zainicjowania struktury zgodnie z naszymi życzeniami, po  
napotkaniu w programie pierwszej deklaracji struktury/obiektu  
danego typu.  
 

PRZYKŁADOWY KONSTRUKTOR.  

 
Struktura Licznik zawiera funkcję inicjującą obiekt (niech  
obiekt będzie na razie zmienną typu struktura):  
  
struct Licznik               //Typ formalny struktur 
{    
  char znak;   
  int ile;   
} licznik;                   //Przykladowa struktura 
  
void Inicjuj(char x)         //Funkcja inicjująca 
{   

240

background image

  licznik.znak = x;   
  licznik.ile = 0;   
}   
 
Zdefiniujmy naszą strukturę w sposób bardziej  
"klasowo-obiektowy":  
 
struct Licznik 
{  
private: 
  char znak;  
  int ile;  
public: 
   void Inicjuj(char);  
   void PlusJeden(void);  
};  
 
Funkcja Inicjuj() wykonuje takie działanie jakie może wykonać  
konstruktor struktury (obiektu), z tą jednak różnicą, że  
konstruktor jest wywoływany automatycznie. Jeśli wyposażymy  
strukturę Licznik w konstruktor, to funkcja Inicjuj() okaże się  
zbędna. Aby funkcja Inicjuj() stała się konstruktorem, musimy  
zmienić jej nazwę na nazwę typu struktury, do której konstruktor 
 
ma należeć. Zwróć uwagę, że konstruktor, w przeciwieństwie do  
innych, "zwykłych" funkcji nie ma podanego typu wartości  
zwracanej: 
 
struct Licznik 
{  
private: 
  char znak;  
  int ile;  
public: 
   Licznik(void);       //Konstruktor nie pobiera argumentu 
   void PlusJeden(void);  
};  
 
Teraz powinniśmy zdefiniować konstruktor. Zrobimy to tak, jak  
wcześniej definiowaliśmy funkcję Inicjuj().  
  
Licznik::Licznik(void)   //Konstruktor nie pobiera argumentu 
{   
  ile = 0;   
}   
 
Jeśli formalny typ struktur (klasa) posiada kostruktor, to po  
rozpoczęciu programu i napotkaniu deklaracji struktur danego  
typu konstruktor zostanie wywołany automatycznie. Dzięki temu  
nie musimy "ręcznie" inicjować struktur na początku programu.  
Jednakże nasz przykładowy konstruktor nie załatwia wszystkich  
problemów - nie ustawia w strukturze zmiennej (pola) int znak -  
określającego, który znak powinien być zliczany w liczniku. W  
tak zainicjowanej strukturze zmienna ile jest zerowana, ale  
zawartość pola znak pozostaje przypadkowa. Niby wszystko w  
porządku, ale wygląda to niesolidnie. Czy nie możnaby przekazać  
parametru przy pomocy konstruktora? Można! Konstruktor  
"bezparametrowy"  
 
Licznik::Licznik(void)    

241

background image

 
taki, jak powyżej to tylko szczególny przypadek - tzw.  
konstruktor domyślny (ang. default constructor).  
 

PRZEKAZYWANIE ARGUMENTÓW DO KOSTRUKTORA.  

 
Czasem chcemy zainicjować nową strukturę już z pewnymi  
ustawionymi parametrami. Te początkowe parametry struktury  
możemy przekazać jako argumenty konstruktora. 
 
struct Licznik 
{  
private: 
  char znak;  
  int ile;  
public: 
   Licznik(char);         //Konstruktor z argumentem typu char 
   void PlusJeden(void);  
};  
 
  
Licznik::Licznik(char x)   //Konstruktor z jednym argumentem 
{   
 ... 
}   
 
main()  
{  
  Licznik licznik('A');     //Deklaracja struktury typu Licznik  
// oznacza to automatyczne wywołanie konstruktora z argumentem 
.... 
  
Poniewż nowy konstruktor pobiera od programu argument typu  
znakowego char, więc i definicję konstruktora należy zmienić: 
 
Licznik::Licznik(char x) //Konstruktor z jednym argumentem 
{   
  ile = 0;   
  znak = x; 
}   
 
Jeśli parametrów jest więcej niż jeden, możemy je przekazać do  
konstruktora, a konstruktor wykorzysta je do zainicjowania  
struktury w następujący sposób:  
 
struct Sasiedzi                  //sąsiedzi 
{  
private: 
    char Tab_imion[4];  
 ... 
public:  
    Sasiedzi(char *s1, char *s2, char *s3, char s4);   
 ...  
};  
 
main()  
{  
  Sasiedzi chopy("Helmut", "Ulrich", "Adolf", "Walter");  
....  

242

background image

 
Przekazanie konstruktorowi argumentów i w efekcie automatyczne  
ustawiamie przez konstruktor paramatrów struktury już w momencie 
 
zadeklarowania struktury w programie rozwiązuje wiele problemów. 
 
W C++ istnieje jednakże pewne dość istotne ograniczenie - nie  
możemy zadeklarować tablicy złożonej z obiektów posiadających  
konstruktory, chyba że wszystkie konstruktory są bezparametrowe  
(typu default constructors).  
 
Udoskonalmy teraz nasz program zliczający wystąpienia w tekście  
litery a posługując się konstruktorem struktury.  
 
[P094.CPP]        /* Wersja ze strukturą */ 
  
# include <ctype.h>   
# include <iostream.h>   
   
struct Licznik   
{    
private:  
  char znak;    
  int ile;    
public:   
   Licznik(char);                //Konstruktor  
   void PlusJeden(void);    
   char Pokaz(void);    
   int Efekt(void);   
};    
   
Licznik::Licznik(char x)            //Def. konstruktora  
{    
  znak = x;    
  ile = 0;    
}    
  
void main()    
{    
  Licznik  licznik('A');      //Zainicjowanie przez konstruktor  
  
  cout << "Sprawdzamy: znak    ile? " << "\n\t\t"   
       <<  licznik.Pokaz() << "\t";   
  cout <<  licznik.Efekt();  
  
  cout << "\nWpisz tekst zawierajacy litery A";   
  cout << "\nPierwsze wytapienie litery k lub K";    
  cout << "\n - oznacza Koniec zliczania: ";     
  for(;;)      
    {    
      char znak_we;  
      cin >> znak_we;     
      if (znak_we == 'k' || znak_we == 'K') break;    
      if(licznik.Pokaz() == toupper(znak_we))    
         licznik.PlusJeden();     
    }     
    
  cout << "\nLitera " << licznik.Pokaz()   
       << " wystapila " << licznik.Efekt() << " razy.";  
}    

243

background image

    
/* Definicje pozostałych funkcji:                          */   
  
void Licznik::PlusJeden(void) { ile++; }  
char Licznik::Pokaz(void) { return (znak); }  
int Licznik::Efekt(void) { return (ile); }  
   
Po zamianie słowa kluczowego struct na class (licznik ze  
struktury stanie się obiektem, a Licznik - z formalnego typu  
struktur - klasą) wystarczy w programie zlikwidować zbędne słowo 
 
"private" i wersja obiektowa programu jest gotowa do pracy.  
 

[P095.CPP]        /* Wersja z klasą i obiektem */ 

  
# include <ctype.h>   
# include <iostream.h>   
   
class Licznik   
{    
  char znak;    
  int ile;    
public:   
   Licznik(char);                //Konstruktor  
   void PlusJeden(void);    
   char Pokaz(void);    
   int Efekt(void);   
};    
   
Licznik::Licznik(char x) //Def. konstruktora  
{    
  znak = x;    
  ile = 0;    
}    
  
void main()    
{    
  Licznik  licznik('A');      //Zainicjowanie obiektu licznik 
  
  cout << "Sprawdzamy:  znak    ile? " << "\n\t\t"   
       <<  licznik.Pokaz() << "\t";   
  cout <<  licznik.Efekt();  
  
  cout << "\nWpisz tekst zawierajacy litery A";   
  cout << "\nPierwsze wytapienie litery k lub K";    
  cout << "\n - oznacza Koniec zliczania: ";     
  for(;;)      
    {    
      char znak_we;  
      cin >> znak_we;     
      if (znak_we == 'k' || znak_we == 'K') break;    
      if(licznik.Pokaz() == toupper(znak_we))    
         licznik.PlusJeden();     
    }     
    
  cout << "\nLitera " << licznik.Pokaz()   
       << " wystapila " << licznik.Efekt()     
       <<   " razy.";     
}    

244

background image

    
void Licznik::PlusJeden(void) { ile++; }  
char Licznik::Pokaz(void) { return znak; }  
int Licznik::Efekt(void) { return ile; }  
   
Pora w tym miejscu zaznaczyć, że C++ oferuje nam jeszcze jedno  
specjalne narzędzie podobnej kategorii. Podobnie, jak do  
tworzenia (struktur) obiektów możemy zastosować konstruktor, tak 
 
do skasowania obiektu możemy zastosować tzw. desruktor (ang.  
destructor). Nazwy konstruktora i destruktora są identyczne z  
nazwą macieżystego typu struktur (macieżystej klasy), z tym, że  
nazwa destruktora poprzedzona jest znakiem "~" (tylda). 
 

CO TO JEST DESTRUKTOR.  

 
Specjalna funkcja - destruktor (jeśli zadeklarujemy zastosowanie 
 
takiej funkcji) jest wywoływana automatycznie, gdy program  
zakończy korzystanie z obiektu. Konstruktor towrzy, a destruktor 
 
(jak sama nazwa wskazuje) niszczy strukturę (obiekt) i zwalnia  
przyporządkowaną pamięć. Przykład poniżej to program  
manipulujący stosem, rozbudowany tak, by zawierał i konstruktor  
i destruktor struktury (obiektu). Zorganizujmy zarządzanie  
pamięcią przeznaczoną dla stosu w taki sposób:  
 
struct Stos  
{  
private: 
  int *bufor_danych;   
  int licznik;   
public:  
     Stos(int ile_RAM);                  /* Konstruktor 
     int Pop(int *ze_stosu);  
     int Push(int na_stos);  
};  
 
gdzie:  
*bufor_danych - wskaźnik do bufora (wypełniającego rolę stosu),  
licznik - wierzchołek stosu, jeśli == -1, stos jest pusty. 
Stos::Stos(...) - konstruktor inicjujący strukturę typu Stos  
(lub obiekt klasy Stos),  
ile_RAM - ilość pamięci potrzebna do poprawnego działanie stosu, 
*ze_stosu - wskaźnik do zmiennej, której należy przypisać  
wartość zdjętą właśnie ze stosu, 
na_stos - liczba przeznaczona do zapisu na stos. 
 
Zajmijmy się teraz definicją konstruktora. Wywołując konstruktor 
 
w programie (deklarując użycie w programie struktury typu Stos)  
przekażemy mu jako argument ilość potrzebnej nam pamięci RAM w  
bajtach. Do przyporządkowznia pamięci na stercie dla naszego  
stosu wykorzystamy funkcję malloc().  
 
Stos::Stos(int n_RAM)                     //Konstruktor - def. 
{  
  licznik = -1;   
  bufor_danych = (int *) malloc(n_RAM);  

245

background image

}  
 
Posługując się funkcją malloc() przyporządkowujemy buforowi  
danych, w oparciu o który organizujemy nasz obiekt (na razie w  
formie struktury) - stos 100 bajtów pamięci, co pozwala na  
rozmieszczenie 50 liczb typu int (po 2 bajty każda). Liczbę  
potrzebnych bajtów pamięci - 100 przekazujemy jako argument  
konstruktorowi w momencie deklaracji struktury typu Stos. Nasza  
struktura w programie będzie się nazywać nasz_stos. 
 
main()  
{  
 ... 
  Stos  nasz_stos(100);  
 ...  
 
Kiedy wykorzystamy naszą strukturę w programie, możemy zwolnić  
pamięć przeznaczoną dla struktury posługując się funkcją  
biblioteczną C  free(). Przykład przydziału pamięci przy pomocy  
pary operatorów new - delete już był, przedstawimy tu zatem  
tradycyjną (coraz rzadziej stosowaną metodę) opartą na  
"klasycznych" funkcjach z biblioteki C. Funkcją free() posłużymy 
 
się w destruktorze struktury nasz_stos - ~Stos(). Destruktory są 
 
wywoływane automatycznie, gdy kończy się działanie programu, lub 
 
też, gdy struktura (obiekt) przestaje być widoczna / dostępna w  
programie. Obiekt (struktura) przestaje być widoczny (podobnie  
ja zwykła zmienna lokalna/globalna), jeśli opuszczamy tę  
funkcję, wewnątrz której obiekt został zadeklarowany. Jest to  
właściwość bardzo ważna dla naszego przykładowego stosu. W  
naszym programie przykładowym pamięć przydzielona strukturze  
stack pozostaje zarezerwowana "na zawsze", nawet wtedy, gdy nasz 
 
stos przestaje być "widoczny" (ang. out of scope). Obiekt może  
przestać być widoczny np. wtedy, gdy działa funkcja "nie  
widząca" obiektu. Idąc dalej tym torem rozumowania, jeśli  
destruktor zostanie wywołany automatycznie zawsze wtedy, gdy  
obiekt przestanie być widoczny, istnienie destruktora w  
definicji typu struktur Stos pozwala na automatyczne wyzerowanie 
 
stosu. Deklarujemy destruktor podobnie do konstruktora, dodając  
przed nazwą destruktora znak ~ (tylda):  
 
struct Stos  
{  
 ...  
public:  
 ...  
  ~Stos(void);  
 ...  

 
Jeśli program zakończy się lub struktura przestanie być  
widoczna, zostanie wywołany destruktor struktury nasz_stos i  
pamięć zostanie zwolniona. Praktycznie oznacza to, że możemy  
zwolnić pamięc przyporządkowaną strukturze w taki sposób:  
 
Stos::~Stos(void)          //Definicja destruktora 

246

background image

{  
  free(bufor_danych); 
  cout << "\n Destruktor: Struktury juz nie ma..."; 

 
Od momentu zdefiniowania konstruktora i destruktora nie musimy  
się już przejmować technicznymi szczegółami ich działania. W  
dalszej części programu destruktor i konstruktor będą wywoływane 
 
automatycznie. Pozostaje nam pamiętać, że  
 
* stos może się nazywać dowolnie, a deklarujemy go tak:  
 
Stos nazwa_struktury;  
 
i dalej stosem możemy posługiwać się przy pomocy funkcji:  
 
nazwa_struktury.Push()   
nazwa_struktury.Pop()  
 
Wszystkie wewnętrzne sprawy stos będzie załatwiał samodzielnie.  
W tym konkrertnym przypadku część "prac organizacyjnych"  
związanych z utworzeniem w pamięci struktury i zainicjowaniem  
początkowych wartości pól załatwi za nas konstruktor i  
destruktor. Na tym właśnie polega idea nowoczesnego  
programowania w C++. Przykładowy program umieszcza liczby na  
stosie a następnie pobiera je ze stosu i drukuje na ekranie.  
Pełny tekst programu w wersji ze strukturą - poniżej. 
 

[P096.CPP] 

  
# include <iostream.h>   
# include <alloc.h>   
  
/* -----------------------poczatek pliku STOS.HPP------------ */ 
 
  
# define OK 1  
  
struct Stos   
{   
private:  
  int *bufor_danych;    
  int licznik;    
public:   
  Stos(int);                  /* Konstruktor */  
  ~Stos(void);                /* Destruktor  */  
int Pop(int*);   
int Push(int);   
};   
  
Stos::Stos(int n_RAM)                     //Konstruktor - def.  
{   
  licznik = -1;    
  bufor_danych = (int *) malloc(n_RAM);   
  cout << "Konstruktor: Inicjuje strukture. ";  
}   
  
Stos::~Stos(void)          //Definicja destruktora  

247

background image

{   
  free(bufor_danych);  
  cout << "\n Destruktor: Struktury juz nie ma...";  
}  
  
  
int Stos::Pop(int* ze_stosu)   
{   
   if(licznik == -1)  return 0;   
    else  *ze_stosu = bufor_danych[licznik--];   
   return OK;   
}   
   
int Stos::Push(int na_stos)   
{   
   if(licznik >= 49)  return 0;   
    else  bufor_danych[++licznik] = na_stos;   
   return OK;   
}   
/* --------------------------koniec pliku STOS.HPP----------- */ 
 
 
void main()   
{   
  Stos nasz_stos(100);    //Dekl. struktury typu Stos  
       int i, Liczba;   
  
     cout << "\nZAPISUJE NA STOS LICZBY:\n";   
  
        for(i = 0; i < 10; i++)  
          {   
    nasz_stos.Push(i + 100);   
    cout << i + 100 << ",  ";   
          }   
     cout << "\nKoniec. \n";  
     cout << "ODCZYTUJE ZE STOSU:\n";   
        for(i = 0; i < 10; i++)  
          {   
            nasz_stos.Pop(&Liczba);   
            cout << Liczba << ",  ";   
        }   
}   
  
 
W C++ częstą praktyką jest umieszczanie tzw. implementacji  
struktur (klas) w plikach nagłówkowych. Szkielet naszego  
programu mógłby wyglądać wtedy tak:  
 
# include <iostram.h>  
# include <alloc.h>  
# include <A:\STOS.HPP>  
 
void main()  
{  
 ...  

 
Wykażemy, że zamiana struktury na klasę odbędzie się całkiem  
bezboleśnie. Mało tego, jeśli dokonamy zmian w implementacji w  
pliku nagłówkowym (struct --> class i usuniemy słowo private)  

248

background image

nasz program główny nie zmieni się WCALE !  
 
Oto plik nagłówkowy A:\INCLUDE\STOSCL.HPP:  
 

[P097.CPP]  

   
# include <iostream.h>   
# include <alloc.h>  
  
/* ---------------------poczatek pliku STOSCL.HPP------------ */ 
 
   
# define OK 1   
   
class Stos    
{    
  int *bufor_danych;     
  int licznik;     
public:    
  Stos(int);                  /* Konstruktor */   
  ~Stos(void);                /* Destruktor  */   
int Pop(int*);    
int Push(int);    
};    
   
Stos::Stos(int n_RAM)                     //Konstruktor - def.   
{    
  licznik = -1;     
  bufor_danych = (int *) malloc(n_RAM);    
  cout << "Konstruktor: Inicjuje obiekt klasy Stos. ";   
}    
   
Stos::~Stos(void)          //Definicja destruktora   
{    
  free(bufor_danych);   
  cout << "\n Destruktor: Obiektu juz nie ma...";   
}   
   
int Stos::Pop(int* ze_stosu)   
{    
   if(licznik == -1)  return 0;    
    else  *ze_stosu = bufor_danych[licznik--];    
   return OK;    
}    
    
int Stos::Push(int na_stos)    
{    
   if(licznik >= 49)  return 0;    
    else  bufor_danych[++licznik] = na_stos;    
   return OK;    
}    
/* ------------------------koniec pliku STOSCL.HPP----------- */ 
 
  
void main()    
{    
  Stos nasz_stos(100);           //OBIEKT Klasy  Stos   
       int i, Liczba;    
   

249

background image

     cout << "\nZAPISUJE NA STOS LICZBY:\n";    
   
        for(i = 0; i < 10; i++)   
          {    
    nasz_stos.Push(i + 100);    
    cout << i + 100 << ",  ";    
          }    
     cout << "\nKoniec. \n";   
     cout << "ODCZYTUJE ZE STOSU:\n";    
        for(i = 0; i < 10; i++)   
          {    
            nasz_stos.Pop(&Liczba);    
            cout << Liczba << ",  ";    
        }    
}    
 
Struktury w robią się coraz bardziej podobne do czegoś nowego  
jakościowo, zmienia się również (dzięki tym nowym cechom) styl  
programowania. 
 

A CO Z UNIAMI ? 

________________________________________________________________ 
Unie są w C++ traktowane podobnie jak struktury, z tym, że pola  
unii mogą się nakładać (ang. overlap) i wobec tego nie wolno  
stosować słowa kluczowego private w uniach. Wszystkie elementy  
unii muszą mieć status public. Unie mogą także posiadać  
konstruktory. 
________________________________________________________________ 
 

A JEŚLI BĘDZIE WIĘCEJ KLAS i STRUKTUR ?  

 
Po zdefiniowaniu nowego formalnego typu struktur możesz  
zastosować w programie wiele zmiennych danego typu. We  
wszystkich przykładach powyżej stosowano pojedynczą strukturę  
WYŁĄCZNIE DLA ZACHOWANIA JASNOŚCI PRZYKŁADU. Mało tego. W C++  
różne struktury mogą korzystać z funkcji o tej samej nazwie W  
RÓŻNY SPOSÓB. Ta ciekawa zdolność nazywa się rozbudowywalnością  
funkcji (ang. overloading - dosł. "przeciążanie"). Dokładniej  
tym problemem zajmiemy się w części poświęconej klasom i  
obiektom. Teraz jedynie prosty przykład na strukturach.  
 

[P098.CPP] 

  
#include <iostream.h>   
#include <stdio.h>   
#include <time.h>   
   
struct Data   
{   
  int miesiac, dzien, rok;   
  void Display(void);                   //Metoda "wyswietl"  
};   
    
void Data::Display(void)    
{    
char *mon[] =    

250

background image

  {    
   "Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca",    
   "Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada",    
   "Grudnia"   
  };    
cout << dzien << ". "    
     << mon[miesiac] << ". "   
     << rok;    
}    
  
struct Czas   
{   
  int godz, minuty, sekundy;   
  void Display(void);           // znow metoda "wyswietl"  
};   
   
void Czas::Display(void)   
{   
  char napis[20];   
  
  sprintf(napis, "%d:%02d:%02d %s",   
 (godz > 12 ? godz - 12 : (godz == 0 ? 12 : godz)),   
  minuty, sekundy,   
  godz < 12 ? "rano" : "wieczor");   
  cout << napis;   
}   
   
main()   
{   
  
  time_t curtime = time(NULL);   
  struct tm tim = *localtime(&curtime);   
  
  Czas teraz;   
  Data dzis;   
  
  teraz.godz = tim.tm_hour;   
  teraz.minuty = tim.tm_min;   
  teraz.sekundy = tim.tm_sec;   
  dzis.miesiac = tim.tm_mon;   
  dzis.dzien = tim.tm_mday;   
  dzis.rok = 1900 + tim.tm_year;   
  
  cout << "\n Jest teraz --> ";   
  teraz.Display();   
  cout << " dnia ";   
  dzis.Display();       cout << "\a";  
   
return 0;  
}   
   
Funkcja Display() wywoływana jest w programie dwukrotnie przy  
pomocy tej samej nazwy, ale za każdym razem działa w inny  
sposób. C++ bezbłędnie rozpoznaje, która wersja funkcji ma  
zostać zastosowana i w stosunku do której struktury (których  
danych) funkcja ma zadziałać. 
 
Aby struktura stała się już całkowicie klasą, pozostało nam do  
omówienia jeszcze kilka ciekawych nowych własności.  
Najważniejszą chyba (właśnie dlatego, że tworzącą zdecydowanie  

251

background image

nową jakość w programowaniu) jest możliwość dziedziczenia cech  
(ang. inheritance), którą zajmiemy się w następnej lekcji.  
 

Zadania

________________________________________________________________ 
1. Sprawdź, czy zamiana struktur na klasy nie zmienia sposobu  
działania programów, ani długości kodów wynikowych.  
2. Opracuj program zliczający wystąpienia ciągu znaków - np.  
"as" we wprowadzanym tekście.  
_______________________________________________________________ 

LEKCJA 27: O DZIEDZICZENIU.  

________________________________________________________________ 
W trakcie tej lakcji dowiesz się na czym polega dziedziczenie.  
________________________________________________________________ 
 
Dziedziczenie (ang inheritance) jest próbą naśladowania w  
technice programowania najcenniejszego bodaj wynalazku Matki  
Natury - zdolności przekazywania cech. Jeśli wyobrazimy sobie  
typy struktur konik, lew, słoń, czy krokodyl, to jest oczywiste, 
że struktury te będą posiadać wiele wspólnych cech. Wspólnymi  
cechami mogą być zarówno wspólne dane (parametry) - np. nogi =  
4; jak i wspólne wykonywane przez nie funkcje - np. jedz(),  
śpij(), oddychaj() itd.. Mogą występować oczywiście i różnice,  
ale wiele danych i funkcji okaże się wspólnych.  
 

LOGIKA DZIEDZICZENIA.  

 
Rozwijając dalej myśl naszkicowaną we wstępie, w kategoriach  
obiegowego języka naturalnego można rzec, że słoń Trombalski  
byłby tu strukturą typu formalnego Słoń. Funkcjami wewnętrznymi  
słonia Trombalskiego i np. krokodyla Eugeniusza mogłyby być  
wspólne czynności tych struktur (lub obiektów): 
 
jedz()   
śpij()   
oddychaj()   
 
Projektanci C++ wpadli na pomysł naśladowania mechanizmu  
dziedziczenia. Zamiast tworzyć te wszystkie struktury  
oddzielnie, możemy zdefiniować w C++ jeden ogólny typ struktur  
(ang. generic structure), nazywany inaczej STRUKTURĄ BAZOWĄ  
(ang. base structure). Wszystkie wymienione wyżej struktury  
(słoń, krokodyl, itp.) stałyby się wtedy strukturami pochodnymi  
(ang. derived structures). Nasza struktura bazowa mogłaby  
nazywać się znów np. Zwierzak.  
 
Ponieważ niektóre funkcje są wspólne dla wszystkich struktur  
(wszystkie Zwierzaki muszą jeść, spać, itp.), moglibyśmy  
przyjąć, że każda struktura pochodna od bazowego typu Zwierzak  
musi zawierać funkcje jedz(), spij() i oddychaj(). Jeśli  
zdefiniujemy strukturę bazową Zwierzak i zadeklarujemy w tej  
klasie funkcje jedz(), spij() i oddychaj(), możemy spodziewać  
się, że struktura pochodna słoń powinna odziedziczyć funkcje -  

252

background image

cechy po strukturze bazowej Zwierzak. . Słoń może oczywiście  
mieć i swoje odrębne cechy - dane i funkcje - np.:  
 
Slon.flaga_ssak 
Slon.trabie() 
Slon.tupie()  
 
"Gramatyka" C++ przy opisywaniu wzajemnego pokrewieństwa  
struktur (i klas) wygląda następująco:  
 
struct NazwaStrukturyPochodnej : NazwaStrukturyBazowej  
{   
private: 
    Lista danych i funkcji prywatnych   
public:   
    Lista danych i funkcji publicznych   
} Lista struktur danego typu;   
 
a dla klas i obiektów:  
 
class NazwaKlasyPochodnej : dostęp NazwaKlasyBazowej  
{   
    Lista danych i funkcji prywatnych   
public:   
    Lista danych i funkcji publicznych   
} Lista obiektow danej klasy;   
  
Bazowy typ struktur w C++ wyglądałaby tak:   
  
struct Zwierzak  
{   
  void jedz();   
  void spij();   
  void oddychaj();   
};   
  
Jeśli chcemy zasygnalizować, że pochodny typ struktur Slon ma  
odziedziczyć coś po typie bazowym Zwierzak, musimy w definicji  
klasy pochodnej podać nazwę klasy bazowej (jeśli mamy  
dziedziczyć - należy wskazać po kim): 
  
struct Slon : Zwierzak 
{   
  int trabie(); 
  int tupie();   
};   
  
Przed nazwą typu struktury (klasy) bazowej (tu: Zwierzak) może  
pojawić się słowo określające zasady dostępu do danych i funkcji 
 
(tu: public).  
 

RÓŻNIE MOŻNA DZIEDZICZYĆ...  

________________________________________________________________ 
* Jeśli użyjemy w tym miejscu słowa public (przy strukturach  
domyślne), to atrybuty dostępu zostaną odziedziczone wprost.  
Oznacza to, że to, co było prywatne w strukturze bazowej  
zostanie przeniesione jako prywatne do struktury pochodnej, a  
to, co było publiczne w strukturze bazowej zostanie przeniesione 

253

background image

 
jako publiczne do struktury pochodnej.  
* Jeśli natomiast użyjemy w tym miejscu słowa private, to  
wszystko, co struktura pochodna odziedziczy po strukturze  
bazowej stanie się w strukturze pochodnej prywatne.  
________________________________________________________________ 
 
Opracowanie przykładowego programu ilustrującego mechanizm  
dziedziczenia rozpoczniemy od zdefiniowania bazowego typu  
struktur i struktury pochodnej. 
 
struct Zwierzak  
{   
  int nogi;              <-- dane 
 
  void jedz();           <-- funkcje 
  void spij();   
  void oddychaj();   
};   
  
struct Slon : Zwierzak 
{   
  int flaga_ssak; 
  int trabie(); 
  int tupie();   
};   
 
Zdefiniujemy teraz wszystkie funkcje należące do powyższych  
struktur. Funkcje będą tylko zgłaszać się na ekranie napisem, by 
 
prześledzić kolejność ich wywołania. 
  
void Zwierzak::jedz(void) { cout << "Jem conieco...\n"; }   
void Zwierzak::spij(void) { cout << "Cosik mi sie sni...\n"; }   
void Zwierzak::oddychaj(void) { cout << "Dyszę cieżko...\n"; }   
void Slon::trabi(void) { cout << "Tra-ta-ta...\n"; }   
void Slon::tupie(void) { cout << "Kroczem...na zachód\n"; }  
 
Aby przekonać się, co struktura typu Slon rzeczywiście  
odziedziczy "po przodku", zredagujemy program główny.  
 
# include <iostream.h>   
 ...  
void main()  
{  
   Slon Choleryk;                    //Deklaracja struktury  
 ... 
   cout << "\nNogi odziedziczylem: " << Choleryk.nogi; 
   cout << "\nA teraz kolejno funkcje:  \n";  
   Choleryk.jedz();  
   Choleryk.spij();  
   Choleryk.oddychaj();  
   Choleryk.trabi();  
   Choleryk.tupie();  

 
Mimo, że tworząc strukturę Słoń nie zadeklarowaliśmy w jej  
składzie ani funkcji jedz(), ani spij(), ani danych nogi, możemy 
 
zastosować funkcję Choleryk.jedz(), ponieważ Choleryk  

254

background image

odziedziczył tę funkcję po strukturze bazowej Zwierzak. Dzięki  
dziedziczeniu możemy posługiwać się danymi i funkcjami  
należącymi do obu typów struktur - bazowego: Zwierzak i  
pochodnego: Slon.  
 

A CO Z UNIAMI ?  

_______________________________________________________________ 
Unie nie mogą brać udziału w dziedziczeniu. Unia nie może być   
ani typem bazowym ani typem pochodnym.   
_______________________________________________________________ 
 
Program w całości będzie wyglądał tak:  
 

[P099.CPP]  

 
# include <iostream.h>    
   
struct Zwierzak    
{     
  int nogi;   
  void jedz();  
  void spij();     
  void oddychaj();     
};     
void Zwierzak::jedz(void) { cout << "Jem conieco...\n"; }  
void Zwierzak::spij(void) { cout << "Cosik mi sie sni...\n"; }  
void Zwierzak::oddychaj(void) { cout << "Dysze ciezko...\n"; }  
    
struct Slon : Zwierzak   
{     
  int flaga_ssak;   
  void trabi();  
  void tupie();     
};     
  
void Slon::trabi(void) { cout << "Tra-ta-ta...\n"; }    
void Slon::tupie(void) { cout << "Kroczem...na wschod\n"; }   
  
void main()    
{    
   Slon Choleryk;     
   Choleryk.nogi = 4;   Choleryk.flaga_ssak = 1;       
   cout << "\nNogi odziedziczylem: " << Choleryk.nogi;   
   cout << "\nA teraz kolejno funkcje:  \n";    
   Choleryk.jedz();    
   Choleryk.spij();    
   Choleryk.oddychaj();    
   Choleryk.trabi();    
   Choleryk.tupie();    
   if(Choleryk.flaga_ssak == 1) cout << "SSak!"; 
}   

255

background image

LEKCJA 28: DZIEDZICZENIE ZŁOŻONE. 

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak można odziedziczyć wiele  
cech po wielu różnych przodkach.  
________________________________________________________________ 
 
Jeśli zechcemy dziedziczyć dalej według schematu  
dziadek-ojciec-syn-wnuk...? Nic nie stoi na przeszkodzie. Przy  
okazji zwróć uwagę, że następne pokolenia są coraz bardziej  
złożone (tak być nie musi, ale może). W przykładzie poniżej  
dziedziczymy według schematu Punkt-Okrąg-Elipsa.  
 

[P100.CPP]  

 
//Przyklad dziedziczenia "wielopokoleniowego" 
  
#include "stdio.h"  
#include "conio.h"  
  
struct punkt              //BAZOWY typ struktur - punkt(x, y) 
{  
int x;                    //wspolrzedne punktu na ekranie  
int y;  
};  
  
struct kolo: punkt        //Str. pochodna - kolo(x, y, R) 
{  
int promien;              //wspolrzedne srodka x,y dziedziczymy  
};  
  
struct elipsa: kolo       //dziedziczymy x,y i promien  
{  
int mniejszy_promien;     //Str. pochodna elipsa(x, y, R, r) 
};  
  
punkt P;                  //deklarujemy trzy struktury  
kolo C;  
elipsa E;  
  
main()  
{   
  clrscr();  
  
  P.x = C.x = E.x = 1;     //Nadajemy wartosci polom struktur  
  P.y = C.y = E.y = 2;  
  
  C.promien = E.promien = 4;  
  E.mniejszy_promien = 3;  
                            //Sprawdzamy zawartosc pol struktur 
  printf("%d %d %d %d %d %d \n",  
          P.x, C.x, E.x, P.y, C.y, E.y);  
  printf("%d %d %d",  
         C.promien, E.promien, E.mniejszy_promien );  
getch();  
return 0;  
}  
  
Można dziedziczyć po więcej niż jednym przodku także w inny  

256

background image

sposób. Kwadrat, dla przykładu, dziedziczy cechy po prostokątach 
 
i po rombach jednocześnie (jest jednocześnie szczególnym  
przypadkiem prostokąta i szczególnym przypadkiem rombu). Typ  
pochodny w tym wypadku, zamiast "dziadka" i "ojca" powinien mieć 
 
DWU RÓŻNYCH OJCÓW (!). W C++ takie dziedziczenie po dwu różnych  
typach bazowych jednocześnie nazywa się DZIEDZICZENIEM  
WIELOBAZOWYM (ang. multi-base inheritance). A oto przykład  
takiego dziedziczenia.  
 

[P101.CPP]  

 
#include <iostream.h>  
  
struct BAZOWA1  
{                            //Struktura bazowa pierwsza  
public:   
    void Funkcja_a(void);   
};   
  
struct BAZOWA2  
{                            //Struktura bazowa druga  
public:   
    void Funkcja_b(void);   
};   
  
struct POCHODNA : BAZOWA1, BAZOWA2      //Lista "przodkow" 

     public:   
     void Funkcja_c(void);   
     };   
  
void BAZOWA1::Funkcja_a(void){cout << "To ja F_a().\n";}  
void BAZOWA2::Funkcja_b(void){cout << "To ja F_b().\n";}   
void POCHODNA::Funkcja_c(void){cout << "To ja F_c().\n";}   
  
void main()   
{   
  POCHODNA dziecko;       //Dekl. strukt. typu pochodnego 
  
  dziecko.Funkcja_a();   
  dziecko.Funkcja_b();   
  dziecko.Funkcja_c();   
}  
  
Słowo public jest w strukturach zbędne. Zostało użyte wyłącznie  
z pobudek "dydaktycznych" - dla zwrócenia uwagi na status  
funkcji - członków struktury. 
 
Zarówno pokoleń w schemacie dziadek-ojciec-syn, jak i struktur  
(klas) bazowych w schemacie baza_1-baza_2-....-baza_n może być  
więcej niż 2.  
 

DZIEDZICZENIE KLAS.  

 
Oto "klasowo-obiektowa" wersja poprzedniego programu  

257

background image

przykładowego ze słonikiem Cholerykiem. Typy struktur Zwierzak i 
 
Slon nazwiemy klasami, (odpowiednio - klasą bazową i klasą  
pochodną) a strukturę Slon Choleryk nazwiemy obiektem.  
 

[P102.CPP]  

 
#include <iostream.h>    
   
class Zwierzak               //Klasa bazowa (base class) 
{     
public: 
  int nogi;   
   
  void jedz();     
  void spij();     
  void oddychaj();     
};     
   
void Zwierzak::jedz(void) { cout << "Jem conieco...\n"; }     
void Zwierzak::spij(void) { cout << "Cosik mi sie sni...\n"; }  
void Zwierzak::oddychaj(void) { cout << "Dysze ciezko...\n"; }  
    
class Slon : public Zwierzak           
{     
public: 
  int flaga_ssak;   
   
  void trabi();   
  void tupie();     
};     
  
void Slon::trabi(void) { cout << "Tra-ta-ta...\n"; }    
void Slon::tupie(void) { cout << "Kroczem...na wschod\n"; }   
  
void main()    
{    
   Slon Obiekt;        
/*  obiekt Obiekt klasy Slon                               */ 
   Obiekt.nogi = 4;   Obiekt.flaga_ssak = 1;       
   cout << "\nNogi odziedziczylem: " << Obiekt.nogi;   
   cout << "\nA teraz kolejno funkcje:  \n";    
   Obiekt.jedz();    
   Obiekt.spij();    
   Obiekt.oddychaj();    
   Obiekt.trabi();    
   Obiekt.tupie();    
   if(Obiekt.flaga_ssak) cout << "Jestem ssakiem !";  
}   
 
Pamiętając o problemie domyślnego statusu członków  
struktur/public i klas/private) możemy przejść do klas i  
obiektów.  
 

O KLASACH SZCZEGÓŁOWO.  

 
Aby wykazać możliwość modularyzacji programu zaprojektujemy  

258

background image

moduł w postaci pliku nagłówkowego. Moduł będzie zawierać  
definicję naszej prywatnej klasy obiektów ZNAK.  
 
Zaczynamy od danych, które będą nam potrzebne do tworzenia w  
programach (różnych !) obiektów typu Znak.  
 
class ZNAK  
{  
  char znak_dany;              //Kod ASCII znaku  
 ...  
 
Aby obiekt został zainicjowany (tzn. wiedział jakim znakiem ma  
być w danym programie) dodamy do definicji klasy  
jednoparametrowy konstruktor  
 
class ZNAK  
{  
  char znak_dany;  
public:  
  ZNAK(...);  
 ...  
 
Dane mogą być prywatne, natomiast konstruktor i funkcje-metody  
powinny być publiczne, by można było wywoływać je w programach.  
Konstruktor będziemy wywoływać w programach tak:  
 
  ZNAK Obiekt('a'); 
 
Znaczy to: Utwórz w RAM obiekt klasy ZNAK pod nazwą "Obiekt" i  
wytłumacz mu, że jest znakiem 'a'.  
 
Konstruktor powinien pobierać od programu jeden argument typu  
char i przekazywać go obiektowi klasy ZNAK na jego pole danych  
znak_dany. Definicja konstruktora będzie zatem wyglądać tak:  
 
ZNAK::ZNAK(char x)  
{  
  znak_dany = x;  
}  
 
Zakres dopuszczalnych znaków zawęzimy np. do kodów ASCII 65...90 
 
(od A do Z). Jeśli użytkownik "nie trafi", ustawimy zawsze "*"  
(asterisk). Dodatkowo, dla "elegancji" zamienimy ewentualne małe 
 
litery na duże. 
  
ZNAK::ZNAK(char x)  
{   
  znak_dany = x;  
  if(znak_dany < 65 || znak_dany >122) znak_dany = '*';  
  if(znak_dany > 97) znak_dany -= 32;    
}  
 
A jeśli użytkownik nie zechce podać żadnego znaku i zda się na  
domyślność obiektu? Żaden problem, wystarczy do klasy ZNAK dodać 
 
bezparametrowy konstruktor domyślny. Konstruktory domyślne  
spełniają w C++ taką właśnie rolę:  
 

259

background image

class ZNAK  
{  
  char znak_dany;  
public:  
  ZNAK(char);       //Konstruktor zwykly ("jednoznakowy") 
  ZNAK(void);       //Konstruktor domyślny (bezparametrowy) 
 ...  
 
Słowo void (tu opcjonalne) może nie wystąpić. Aby "kłuło w  
oczy", który konstruktor jest konstruktorem domyślnym (ang.  
default konstructor), większość programistów zapisuje to tak:  
 
class ZNAK  
{  
  char znak_dany;  
public:  
  ZNAK(char);      
  ZNAK();       //Z daleka widać, że nic nie ma ! 
 ...  
 
Definicja konstruktora bezparametrowego będzie wyglądać tak:  
 
ZNAK::ZNAK() { znak_dany = 'X'; }  
 
W zależności od sposobu zadeklarowania obiektu w programie C++  
wywoła automatycznie albo konstruktor ZNAK(char), albo  
konstruktor domyślny ZNAK():  
 
ZNAK obiekt;     //Nie sprecyzowano jaki, konstruktor domyślny  
ZNAK obiekt('m');   //Wiadomo jaki, konstruktor jednoparametrowy 
 
 
Dzięki temu, że C++ "pedantycznie" sprawdza przed wywołaniem  
funkcji zgodność typów argumentów przekazywanych do funkcji  
(konstruktor to też funkcja) i porównuje typ argumentów z  
życzeniem programisty wyrażonym w prototypie - bezbłędnie  
rozpozna (mimo identycznej nazwy), którą funkcję należy  
zastosować.  
 
Dodajmy do klasy ZNAK deklaracje (prototypy) funkcji-metod:  
  
class ZNAK   
{   
  char znak_dany;   
  
public:  
  ZNAK(char);  
  ZNAK();  
  void Pokaz_sie();  
  void Znikaj();   
  void Skacz();   
};   
 
i zdefiniujmy te metody.  
 
void ZNAK::Pokaz_sie(void)   
{   
  cout << znak_dany << '\a';   
}   
  

260

background image

void ZNAK::Znikaj(void)   
{   
  cout << "\b" << ' ';           //'\b' == Back Space   
}   
  
void ZNAK::Skacz(void)   
{   
  for(int i = 0; i < 100; i++)  
  {  
  gotoxy(rand()%50, rand()%50);   
  cout << znak_dany;  
  getch();         
  }  
}  
 
Jeśli implementację klasy ZNAK umieścimy w pliku nagłówkowym  
 
A:\ZNAK.H  
 
//_____________________________________________________________ 
# include <stdlib.h>  
# include <conio.h>  
# include <iostream.h>  
  
class ZNAK   
{   
  char znak_dany;   
  
public:  
  ZNAK(char);  
  ZNAK();  
  void Pokaz_sie();  
  void Znikaj();   
  void Skacz();   
};   
  
ZNAK::ZNAK()  
{  
  znak_dany = 'X';  
}  
  
ZNAK::ZNAK(char x)  
{   
  znak_dany = x;  
  if(znak_dany < 65 && znak_dany >122) znak_dany = '*';  
  if(znak_dany > 97) znak_dany -= 32;    
}  
  
void ZNAK::Pokaz_sie(void)   
{   
  cout << znak_dany << '\a';   
}   
  
void ZNAK::Znikaj(void)   
{   
  cout << "\b" << ' ';           //'\b' == Back Space   
}   
  
void ZNAK::Skacz(void)   
{   

261

background image

  for(int i = 0; i < 100; i++)  
  {  
  gotoxy(rand()%50, rand()%50);   
  cout << znak_dany;  
  getch();         
  }  
}  
//_____________ koniec pliku A:\INCLUDE\ZNAK.H _________________ 
 
 
to nasz program może wyglądać tak: 
 
[P103.CPP] 
 
# include <a:\znak.h>  
 
void main()   
{   
char litera;  
  
    clrscr();  
    cout << '\n' << "Podaj znak:  ";   
    cin >> litera;   
  
    ZNAK Obiekt(litera);   
    cout << "\nSTART" << "\n\n\n";   
  
    getch();  
    Obiekt.Pokaz_sie();   
    getch();  
    Obiekt.Znikaj();   
    getch();  
    Obiekt.Skacz();   
  
    ZNAK Obiekt2;            //To bedzie domyslny 'X'  
    Obiekt2.Skacz();  
}   
  
I tu już widać pewne cechy nowoczesnego obiektowego stylu  
programowania. Tym razem sprwdzenie, czy słowo class można  
spokojnie zamienić na słowo struct pozostawim dociekliwym  
Czytelnikom.

LEKCJA 29: FUNKCJE I OVERLOADING.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak jeszcze w C++ można  
wykorzystywać funkcje.  
________________________________________________________________ 
 
w C++ jedna funkcja może być definiowana wielokrotnie a każda z  
wersji funkcji może być przystosowana do obsługi innego typu  
argumentów. C++ wybiera tę właściwą wersję funkcji  
automatycznie.  
 

262

background image

JEDNA NAZWA FUNKCJI - WIELE ZASTOSOWAŃ.  

 
Overloading funkcji bywa czasem w podręcznikach dzielony na  
odrębne zagadnienia:  
 
* funkcja może tolerować różną liczbę argumentów (co dało się  
spokojnie realizować również w klasycznym C - vide printf());  
* funkcja może tolerować różne typy argumentów;  
* funkcja może realizować różne operacje dla różnych  
 
Wyobraźmy sobie, że mamy funkcję wydrukuj(), która potrafi  
wysłać na ekran otrzymany znak:  
 
void wydrukuj(char znak)  
{  
cout << znak;  
}  
 
Tak zdefiniowaną funkcję możemy wywołać w programie w  
następujący sposób:  
 
wydrukuj('Z');  
 
Czasem jednak wygodniej byłoby, gdyby nasza funkcja była  
bardziej elastyczna i pozwalała na wykonanie szerszego zakresu  
operacji, np.:  
 
wydrukuj('Z');  
wydrukuj(75);   // 75 to kod ASCII znaku, zamiast znaku bezpośr. 
 
wydrukuj("Wiecej niz znak - tekst");  
 
W klasycznym języku C wymaga to zdefiniowania nowej funkcji,  
natomiast w C++ to, że funkcja wydrukuj() została już  
zdefiniowana w niczym nie przeszkadza. Poniżej definjujemy taką  
funkcję.  
 
...  
class KLASA  
{  
public:  
  void wydrukuj(char znak);  
  void wydrukuj(int kod_ASCII);  
  void wydrukuj(char *string);      //wskaźnik do lancucha 
}  
 
Łańcuch znaków jest widziany jako jednowymiarowa tablica  
zawierająca dane typu znakowego, czyli w taki sposób:  
 
char TABLICA[9] ={ "123456789" };  
 
Definice powinny mieć następującą postać:  
 
void KLASA::wydrukuj(char znak)    {cout << znak;};  
void KLASA::wydrukuj(int kodASCII) {cout << (char) kodASCII;}; 
void KLASA::wydrukuj(char *string) {cout << string;};  
 
Zapis:  
 
cout << (char) kodASCII; 

263

background image

 
oznacza forsowanie typu - zamień typ int na typ char -  
przyporządkowanie kodowi ASCII - znaku. Wywołanie tej funkcji w  
programie może spowodować różne działanie, w zależności od typu  
i ilości argumentów, z którym(i) funkcja zostaje wywołana.  
Wywołania funkcji mogą wyglądać np. tak:  
 
KLASA Obiekt1, Obiekt2; 
main() {  
...  
Obiekt1.wydrukuj('A');    //Wydrukuje się litera  A  
Obiekt1.wydrukuj(99);     //Wydrukuje się litera  c  
Obiekt2.wydrukuj("napis");  //Wydrukuje się napis.  
...  

 
Taki sposób postępowania umożliwia funkcjom większą elastyczność 
 
i pozwala operować bez konfliktów na różnych rodzajach danych.  
 
Język C posiada funkcje służące do kopiowania łańcuchów  
znakowych: strcpy() i strncpy(). Funkcja biblioteczna strncpy()  
przerywa proces kopiowania po zakończeniu łańcucha żródłowego,  
bądź po skopiowaniu zadanej ilości znaków. Dzięki mechanizmowi  
overloadingu możemy utworzyć naszą własną funkcję  
kopiuj_string(), która zależnie od sytuacji zadziała jak  
strcpy(), bądź tak jak strncpy().  
 

[P104.CPP] 

  
# include <iostream.h>   
  
/* dwa porototypy - dwie wersje funkcji  kopiuj_string() */   
/*                source:   destination:  len:           */ 
 
void kopiuj_string(char*, const char*);       //Dwa argumenty   
void kopiuj_string(char*, const char*, int);  //a tu trzy  
  
static char Piggie[20], Kermit[32];   
   
main()   
{   
  kopiuj_string(Piggie, "Panna Piggie");   
  kopiuj_string(Kermit, "Kermit - to protokul transmisji", 6);   
  cout << Kermit << " oraz " << Piggie;   
  
  return 0;  
}   
   
void kopiuj_string(char *destin, const char *source)   
{   
while((*destin++ = *source++) != '\0') /* instr. pusta */ ;    
}   
   
void kopiuj_string(char *destin, const char *source, int len)   
{   
while (len && (*destin++ = *source++) != '\0') --len;  
while (len--) *destin++ = '\0';  
}   

264

background image

  

Source- Destination.  

________________________________________________________________ 
source - tu: źródłowy łańcuch znaków. Ogólnie - źródło. Typowy  
skrót src. 
destin - tu: łańcuch przeznaczenia. Ogólnie destination -  
miejsce przeznaczenia. Typowy skrót dest, dst, destin.  
len - tu: długość.  
________________________________________________________________ 
 

O FUNKCJACH WPLECIONYCH - TYPU inline.  

 
Czsami zależy nam na przyspieszeniu działania programu  
obiektowego (zwykle kosztem zwiększenia długości pliku). Jeśli w 
 
źródłowym tekście programu następuje wywołanie funkcji typu  
inline, to kompilator wstawia w to miejsce całe ciało funkcji  
(funkcje typu inline nie mają bezpośredniego ani wyłącznego  
odniesienia do obiektowego stylu programowania). Dla przykładu,  
jeśli nadalibyśmy naszej funkcji wydrukuj() status funkcji  
inline, to fragment programu:  
 
 
obiekt.wydrukuj(65);         //Kod ASCII  
 
zostałby zastąpiony wstawionym w to miejsce ciałem funkcji  
wydrukuj():  
 
 .... 
cout << (char) 65; 
 ....  
 
Jest to skuteczna metoda przyspieszenia działania programów.  
Jeśli chcemy zastosować technikę funkcji inline w stosunku do  
metod należących do danej klasy, powinniśmy użyć słowa  
kluczowego "inline" w definicjach funkcji. Zwróć uwgę, że w  
samej definicji klasy słowo inline NIE POJAWIA SIĘ: 
 

[P105.CPP]  

  
# include <iostream.h>  
  
class Klasa   
{   
public:   
  void wydrukuj(char* tekst);   
  void wydrukuj(char Znak);   
  void wydrukuj(int KodASCII);  
};   
  
inline void Klasa::wydrukuj(char* tekst)   
{   
  cout << tekst;   
}   
  
inline void Klasa::wydrukuj(char Znak)  

265

background image

{  
  cout << Znak;  
}   
  
inline void Klasa::wydrukuj(int KodASCII)   
{  
  cout << (char) KodASCII;  
}  
  
void main()   
{   
  Klasa Obiekt;   
  cout << "Obiekt wyprowadza dane: " << '\n';   
  Obiekt.wydrukuj(65);  
  Obiekt.wydrukuj('B');   
  Obiekt.wydrukuj("C i juz");  
}   
  
 
Wszystkie wersje funkcji wydrukuj() otrzymały status inline.  
Oznacza to, że funkcje te nie będą w programie wywoływane lecz  
całe ciała funkcji zostaną wstawione do programu w miejsca  
wywołań. Jest to mechanizm podobny do wstawiania do programu  
makrorozkazów z tą różnicą, że w przypadku funkcji inline C++  
przeprowadza dodatkowo sprawdzenie zgodności typów argumentów  
(ang. type checking). W naszym przypadku kompilator C++ wstawi  
do programu ciało funkcji tyle razy, ile razy funkcja powinna  
zostać wywoływana. Zastosowanie funkcji inline jest opłacalne,  
jeżeli ciało funkcji jest stosunkowo krótkie.  
 

A CZY NIE MOŻNA WEWNĄTRZ KLASY ?  

________________________________________________________________ 
Można. Jeśli umieścimy pełną definicję funkcji wewnątrz  
definicji klasy, to taka funkcja staje się AUTOMATYCZNIE funkcją 
 
typu inline.  
________________________________________________________________ 
 
Status inline możemy nadać wszystkim trzem wersjom funkcji  
wydrukuj() umieszczając definicje funkcji bezpośrednio wewnątrz  
definicji klasy:  
  
class Klasa  
{  
public: 
  inline void wydrukuj(char* a) { cout << a; }  
  inline void wydrukuj(char z) { cout << z; }  
  inline void wydrukuj(int kod) { cout << (char) kod; }  
}; 
 
W większości przypadków daje to efekt pozytywny. Jeśli  
definiujemy funkcje wewnątrz klasy, są to zwykle funkcje o  
krótkim ciele.  
 

OVERLOADING KONSTRUKTORÓW.  

 
W C++ możemy poddać overloadingowi także konstruktory.  

266

background image

 
UWAGA: destruktorów nie można poddać overloadingowi. 
 
Overloading konstruktorów nie wyróżnia się niczym specjalnym.  
Wyobraźmy sobie, że tworzymy obiekt klasy Klasa o nazwie Obiekt. 
 
Jeśli chcemy, by konstruktor przy zakładaniu Obiektu przekazał  
mu łańcuch znaków "zzzz", możemy to zrobić na dwa sposoby. Raz  
polecimy konstruktorowi przekazać do obiektu łańcuch znaków  
"zzzz", a za drugim razem polecimy przekazać do obiektu  
czterokrotnie znak 'z':  
 
Obiekt("zzzz");    /* albo */   Obiekt('z', 4);  
 
Jeśli w programie zadeklarujemy obiekt danej klasy, spowoduje to 
 
automatyczne wywołanie konstruktora z parametrem podanym w  
momencie deklaracji obiektu.  
 
class Klasa  
{  
public:  
  Klasa(char*);  
  Klasa(char, int);  
};  
 
Wersje konstruktora Klasa::Klasa() powinniśmy zdefiniować tak:  
 
Klasa::Klasa(char *tekst) { cout << tekst; }  
 
Klasa::Klasa(char Znak, ile = 4);  
{  
  for(int i = 1; i < ile; i++)  
      cout << Znak;   
}  
 
Dodajmy jeszcze jeden kontruktor domyślny. Konstruktory domyślne 
 
działają według zasady, którą w naturalnym języku dałoby się  
przekazać mniej więcej tak: "dopóki nie zdecydowano inaczej...". 
 
Dopóki nie zdecydowano inaczej - obiekt otrzyma znak 'x'.  
 
class Klasa  
{  
public:  
  Klasa(); 
  Klasa(char*);  
  Klasa(char, int);  
};  
 ...  
Klasa::Klasa(void)  
{  
  cout << 'x'; 

 
Praktyczne zastosowanie w programie będzie wyglądać tak:  
 

267

background image

[P106.CPP] 

  
# include <iostream.h>   
  
class Klasa   
{   
public:   
  Klasa();  
  Klasa(char*);   
  Klasa(char, int);   
};   
  
Klasa::Klasa(void)   
{   
  cout << 'x';  
}  
  
Klasa::Klasa(char *tekst)   
{   
  cout << tekst;   
}   
  
Klasa::Klasa(char Znak, int ile = 4)   
{   
  for(int i = 0; i < ile; i++) cout << Znak;  
}   
  
static char *p = "\nJestem Obiekt.";   
  
void main()   
{   
  Klasa Obiekt1;            //Konstr. domyślny   
  Klasa Obiekt2('A');       // ile - domyslnie == 4   
  Klasa Obiekt3('B', 3);   
  Klasa Obiekt4(p);   
}  

LEKCJA 30: WYMIANA DANYCH MIĘDZY OBIEKTAMI.  

________________________________________________________________ 
 
W trakcie tej lekcji dowiesz się, jak można wymieniać dane i  
informacje pomiędzy różnymi obiektami.  
________________________________________________________________ 
 
 
Hermetyzacja danych jest cenną zdobyczą, ale od czasu do czasu  
obiekty powinny dokonywać pomiędzy sobą wymiany informacji,  
także tych wewnętrznych - prywatnych. Ten problem może sprawiać  
programiście trochę kłopotów - należy zatem poświęcić mu trochę  
uwagi.  
 

DOSTĘP DO DANYCH PRZY POMOCY FUNKCJI KATEGORII friend.  

 
Aby wyjaśnić mechanizmy dostępu do danych obiektów będziemy  
potrzebować:  

268

background image

 
* wielu obiektów;  
* danych prywatnych obiektów (dostęp do publicznych,  
"niezakapsułkowanych" danych jest prosty i oczywisty);  
* funkcji o specjalnych uprawnieniach. 
 
Takie funkcje o specjalnych uprawnieniach - z możliwością  
odwoływania się do prywatnych danych wielu obiektów (a nie tylko 
 
swojego) muszą w C++ posiadać status "friend" (ang. friend -  
przyjaciel).  
 
Nasz przykładowy program będzie operował tablicą złożoną z  
obiektów klasy Licznik.  
 
class Licznik 
{  
  char moja_litera;  
  int ile; 
public:  
  void Inicjuj_licznik(char);  
  void Skok_licznika(void);  
  void Pokazuj(); 
};  
 
 ...  
Licznik TAB[MAX]; 
 
Obiekty - liczniki będą zliczać wystąpienie (każdy swojego)  
określonego znaku w strumieniu znaków wejściowych (wczytywanym z 
 
klawiatury). Tablica będzie się składać z MAX == 26 elementów -  
obiektów - liczników, po jednym dla każdej dużej litery  
alfabetu. Tablica będzie nazywać się TAB[26]. Po zadeklarowaniu: 
 
 
nazwa_klasy TAB[MAX];  
 
kolejne obiekty będą się nazywać:  
 
nazwa_klasy Obiekt1 == TAB[0];       //Licznik 1 - 'A' 
nazwa_klasy Obiekt2 == TAB[1];       //Licznik 2 - 'B' 
 ...                                    ... 
nazwa_klasy ObiektN == TAB[N-1];  
 
Po wprowadzeniu znaku z klawiatury wywołamy wbudowaną do każdego 
 
obiektu funkcję Skok_licznika(), która doda jedynkę do  
wewnętrznego licznika obiektu. Wywołując funkcję zastosujemy  
zamiast typowej składni  
 
ObiektK.Skok_licznika();  
 
odpowiadającą jej w tym wypadku notację  
 
TAB[i].Skok_licznika(); 
 
Powinniśmy jeszcze przed wywołaniem funkcji sprawdzić, czy znak  
jest dużą literą alfabetu. W przykładowym programie zrobimy to  
tak:  

269

background image

 
 ... 
  cin >> znak;                     //Pobranie znaku z klawiatury 
  for(int i = 0; i < 26; i++) 
     {  
       if(i == (znak - 'A')) TAB[i].Skok_licznika();  
     } 
 ... 
 
Dzięki temu wewnętrzny licznik obiektu TAB[2] zostanie  
powiększony tylko wtedy, gdy znak - 'A' == 2 (znak jest literą  
C, bo 'C' - 'A' == 2).  
 
Można to zapisać skuteczniej. 
... 
   cin >> znak; 
   TAB[znak - 'A'].Skok_licznika();    //Inkrementacja licznika  
 ... 
 
bądź jeszcze krócej:  
 
 ... 
   TAB[getch() - 'A'].Skok_licznika();  
 ... 
 
Istnieje tu wszakże niebezpieczeństwo próby odwołania się do  
nieistniejącego elementu tablicy, przed czym powinniśmy się  
wystrzegać. 
 
W wyniku działania programu otrzymamy zliczoną ilość  
występowania danej litery w strumieniu znaków wejściowych.  
 

[P107.CPP]  

  
# include <ctype.h>            //prototyp toupper()  
# include <iostream.h>   
  
class Licznik  
{   
  char moja_litera;   
  int ile;  
public:   
  void Inicjuj(char);   
  void Skok_licznika();   
  void Pokazuj();  
};   
  
void Licznik::Inicjuj(char z)   
{   
  moja_litera = z;   
  ile = 0;   
}   
  
void Licznik::Skok_licznika(void)   
{   
  ile++;   
}   
  
void Licznik::Pokazuj(void)   

270

background image

{   
  cout << "Znak " << moja_litera << " wystapil "   
       << ile << " razy" << '\n';   
}  
main()   
{   
const MAX = 26; 
Licznik TAB[MAX];   
register int i;   
  
/* inicjujemy liczniki:  -------------------------------*/   
  
for(i = 0; i < MAX; i++)   
   {   
     TAB[i].Inicjuj('A' + i);   
   }  
/* pracujemy - zliczamy: -------------------------------*/  
  
cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n';  
for(;;)   
  { char znak;  
    cin >> znak;  
    if(znak == '.') break;  
    for(i = 0; i < MAX; i++)   
       {   
         if(i == (znak - 'A')) TAB[i].Skok_licznika();   
       }   
  }  
/* sprawdzamy: ----------------------------------------*/  
  
char sprawdzamy;  
cout << '\n' << "Podaj znak do sprawdzenia: " << '\n';   
cin >> sprawdzamy;  
cout << "Wyswietlam wyniki zliczania: \n";   
  TAB[toupper(sprawdzamy) - 'A'].Pokazuj();   
  
return 0;   
}  
  
Jeśli chcielibyśmy zliczyć ilość wszystkich wprowadzonych  
znaków, powinniśmy zsumować dane pobrane od wielu obiektów.  
 
Jeśli dane przechowywane w obiektach mają status danych  
prywatnych, to dostęp do tych danych może być utrudniony. Do  
tego momentu dostęp do danych prywatnych obiektu mogliśmy  
uzyskać tylko posługując się autoryzowaną do tego metodą -  
własną funkcją wewnętrzną tegoż obiektu. Ale wtedy nie mieliśmy  
dostępu do danych innych obiektów a tylko do jednego -  
"własnego" obiektu funkcji. Jeśli zatem chcielibyśmy zsumować  
zawartości wielu obiektów - liczników, to należy do tego  
zastosować tzw. funkcję "zaprzyjaźnioną" - friend function.  
Jeśli deklarując funkcję zastosujemy słowo kluczowe friend, to  
taka zaprzyjaźniona z klasą funkcja uzyska prawo dostępu do  
prywatnych elementów danej klasy. Zadeklarujemy taką przykładową 
 
zaprzyjaźnioną funkcję o nazwie Suma(). Funkcja będzie pobierać  
jako parametr ilość obiektów do zsumowania i sumować zawartości  
wewnętrznych liczników obiektów.  
 
const MAX = 26; 

271

background image

 
class Licznik  
{   
  char moja_litera;   
  int ile;  
public:   
  void Inicjuj(char);   
  void Skok_licznika();   
  void Pokazuj();  
  friend int Suma(int);  
} TAB[MAX]; 
 
Zadeklarowana w taki sposób zaprzyjażniona funkcja ma prawo  
dostępu do prywatnych elementów wszystkich obiektów klasy  
Licznik. Typowe zastosowanie funkcji typu friend polega właśnie  
na dostępie do danych wielu różnych obiektów. Powinniśmy  
zsumować zawartość pól  
 
TAB[i].ile  
 
dla wszystkich obiektów (od i = 0 aż do i = MAX). Zwróć uwagę,  
że definiując funkcję Suma() nie stosujemy powtórnie słowa  
kluczowego friend. A oto definicja:  
 
int Suma(int ilosc_obiektow)  
{  
      int i, suma = 0;  
   for(i = 0; i < ilosc_obiektow; i++)  
      suma += TAB[i].ile;  
 
      return (suma);  
}  
 
Dzięki zastosowaniu słowa "friend", funkcja Suma() jest  
zaprzyjaźniona ze wszystkimi 26 obiektami, ponieważ wszystkie  
obiekty należą do tej klasy, w której zadeklarowaliśmy funkcję: 
 
class ...  
{  
 ... 
friend int Suma(...);  
 ...  
} ... ;  
 

Tablica TAB[MAX] złożona z obiektów klasy Licznik została zadeklarowana 
nazewnątrz funkcji main() ma więc status tablicy GLOBALNEJ

. Funkcja Suma() ma dostęp do prywatnych danych  
wszystkich obiektów, możemy więc zastosować ją w programie w  
następujący sposób: 
 

[P108.CPP] 

 
# include <ctype.h> 
# include <iostream.h>  
 
class Licznik  
{   

272

background image

  char moja_litera;   
  int ile;  
public:   
  void Inicjuj(char);   
  void Skok_licznika();   
  void Pokazuj();  
  friend int Suma(int);  
}  
const MAX = 26; 
Licznik TAB[MAX]; 
register int i;   
 
main()   
{   
/* inicjujemy liczniki: -------------------------------*/  
  
for(i = 0; i < MAX; i++)   
   {   
     TAB[i].Inicjuj('A' + i);   
   }  
/* pracujemy - zliczamy: -------------------------------*/  
  
cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n';  
for(;;)   
  { char znak;  
    cin >> znak;  
    if(znak == '.') break;  
    for(i = 0; i < MAX; i++)   
       {   
         if(i == (znak - 'A')) TAB[i].Skok_licznika();   
       }   
  }  
/* sprawdzamy: ----------------------------------------*/  
  
char sprawdzamy;  
cout << '\n' << "Podaj znak do sprawdzenia: " << '\n';   
cin >> sprawdzamy;  
cout << "Wyswietlam wyniki zliczania: \n";   
  TAB[toupper(sprawdzamy) - 'A'].Pokazuj();   
  
cout << "\n Wszystkich liter bylo " << Suma(MAX); 
 
return 0;   
}  
 
void Licznik::Inicjuj(char zn)   
{   
  moja_litera = zn;   
  ile = 0;   
}   
  
void Licznik::Skok_licznika(void) { ile++; }  
  
void Licznik::Pokazuj(void)   
{   
  cout << "Znak " << moja_litera << " wystapil "   
       << ile << " razy" << '\n';   
}  
 
int Suma(int ilosc_obiektow)  

273

background image

{  
      int i, suma = 0;  
   for(i = 0; i < ilosc_obiektow; i++)  
      suma += TAB[i].ile;  
 
      return (suma);  
}  
 
Tak działa funkcja typu friend. Zwróćmy tu uwagę, że funkcja  
taka nie jest traktowana dokładnie tak samo, jak metoda  
wchodząca w skład klasy i obiektu. Metoda, czyli "własna"  
funkcja obiektu odwołuje się do jego pola (danych) w taki  
sposób:  
 
void Licznik::Skok_licznika(void)  
{  
    ile++;                 //Wiadomo o ktory obiekt chodzi  
}  
 
Funkcja klasy friend odwołuje się do pól obiektów tak:  
 
int Suma(int liczba)  
{  
 ... 
     suma += TAB[i].ile; 
/* - wymaga dodatkowo wskazania, o który obiekt chodzi - */ 
}  
 
Należy pamiętać, że dla funkcji kategorii friend wszystkie  
obiekty należące do danej klasy mają status public - są  
dostępne.  
 

O ZAPRZYJAŹNIONYCH KLASACH.  

 
W C++ mogą być zaprzyjaźnione ze sobą wzajemnie także klasy.  
Pozwala to metodom zdefiniowanym wewnątrz jednej z klas na  
dostęp do prywatnych danych obiektów innych klas. W przypadku  
zaprzyjaźnionych klas słowem kluczowym friend poprzedzamy nazwę  
klasy (a nie każdej zaprzyjaźnionej metody z osobna, choć  
zamierzony skutek właśnie na tym polega). Oto praktyczny  
przykład zaprzyjaźnionych klas.  
 

[P109.CPP] 

 
# include <iostream.h>  
  
class Data1;          //Deklaracja (a nie definicja!) klasy 
 
class TEZ_DATA  
{  
  int dz, rok;  
public:  
  TEZ_DATA() {}  
  TEZ_DATA(int d, int y) { dz = d; rok = y;}  
  void Pokazuj() {cout << '\n' << rok << '-' << dz;}  
  friend Data1;                       //"zaprzyjazniona" klasa 
};  

274

background image

 
class Data1                           //Tu DEFINICJA klasy 
{  
  int mc, dz, rok;  
public:  
  Data1(int m, int d, int y) { mc = m; dz = d; rok = y; }  
  operator TEZ_DATA();  
};  
  
static int TAB[] = {31,28,31,30,31,30,31,31,30,31,30,31};  
  
/* ---- funkcja - metoda konwersji - definicja ----------- */ 
 
Data1::operator TEZ_DATA(void)  
{  
  TEZ_DATA DT_Obiekt(0, rok);  
  for (int i = 0; i < mc-1; i++)  
  DT_Obiekt.dz += TAB[i];  
  DT_Obiekt.dz += dz;  
  return DT_Obiekt;  
}  
  
main()  
{  
  Data1 dt_Obiekt(11,17,89);  
  TEZ_DATA DT_Obiekt;  
  DT_Obiekt = dt_Obiekt;  
  DT_Obiekt.Pokazuj();  
 
return 0; 
}  
  
Zaprzyjaźnione są klasy Data1 i TEZ_DATA. Dzięki temu metody  
zadeklarowane wewnątrz zaprzyjaźnionej klasy Data1 mają dostęp  
do prywatnych danych obiektów klasy TEZ_DATA. Ponieważ klasa to  
nowy formalny typ danych, a obiekt to dane takiego nowego typu,  
nic nie stoi na przeszkodzie, by obiekty przekazywać do funkcji  
jako argumenty (tak jak wcześniej obiekty typów typowych - int,  
float itp.).  
 
W C++ mamy jeszcze jedną metodę wymiany danych. Możemy nadać  
elementom klas i obiektów status static (statyczny).  
 

WYMIANA INFORMACJI PRZY POMOCY DANYCH STATYCZNYCH.  

 
Jeśli element klasy został zadeklarowany jako element statyczny  
(przy pomocy słowa kluczowego static), to bez względu na to jak  
wiele obiektów danej klasy utworzymy, w pamięci będzie istnieć  
TYLKO JEDEN EGZEMPLARZ (kopia) tego elementu. W przykładowym  
programie z obiektami-licznikami możemy osiągnąc taki efekt  
nadając zmiennej ile (stan licznika) status static int ile:  
 
class Licznik 

     char moja_litera;  
     static int ile;     
 ...  
};  
 

275

background image

Jeśli utworzymy wiele obiektów takiej klasy, to wszystkie te  
obiekty będą posługiwać się tą samą (wspólną!) zmienną ile. Dla  
przykładu, jeśli zechcemy zliczać ile razy w strumieniu danych  
wejściowych pojawiły się np. znaki 'a' , 'b' i 'c', możemy  
utworzyć trzy obiekty - liczniki: licznik_a, licznik_b i  
licznik_c. wszystkie te liczniki będą posługiwać się wspólną  
zmienną statyczną ile:  
 
class Licznik 

public:  
     char moja_litera;  
     static int ile; 
     Licznik(char);               //Konstruktor 
 ... 
};  
 
Do zainicjownia obiektów posłużymy się konstruktorem. Deklaracja 
 
obiektu spowoduje automatyczne wywołanie kostruktora i  
zainicjowanie obiektu w pamięci. Przy okazji przekazujemy  
obiektom znaki do zliczania.  
 
Licznik licznik_a('a'), licznik_b('b'), licznik_c('c');  
 
Jeśli teraz w strumieniu wejściowym pojawi się któraś z  
interesujących nas liter (a, b, bądź c), zostanie wywołana  
właściwa wersja metody Skok_licznika():  
 
int main(void)  
{  
  char litera;  
 ... 
  cin >> litera;  
 ...  
  if(litera == licznik_a.moja_litera) licznik_a.Skok_licznika(); 
 
  if(litera == licznik_b.moja_litera) licznik_b.Skok_licznika(); 
 
 ...  

 
Zmienna ile jest zmienną statyczną, więc wsztstkie trzy funkcje  
dokonają inkrementacji zmiennej znajdującej się pod tym samym  
fizycznym adresem pamięci. Jeśli dla wszystkich obiektów danej  
klasy jakaś zmienna oznacza zawartość tego samego adresu  
pamięci, możemy się odwołać do tej zmiennej również tak:  
 
nazwa_klasy::nazwa_zmiennej 
 
Ten sposób można jednakże stosować wyłącznie wobec statycznych  
elementów klasy o statusie danych publicznych. Jeśli są to dane  
prywatne nie można jeszcze dodatkowo zapominać o hermetyzacji i  
zasadach dostępu. Jeżeli pole danej klasy jest polem statycznym, 
 
możemy do niego odwoływać się na dwa sposoby. Za pośrednictwem  
obiektów w taki sposób:  
 
identyfikator_obiektu.identyfikator_pola  
 

276

background image

A za pośrednictwem nazwy klasy (podobnie jak do zmiennych  
globalnych), taką metodą: 
 
identyfikator_klasy::identyfikator_pola 
 
Możemy zmodyfikować program przykładowy posługując się  
(globalną) zmienną statyczną. Zamiast wszystkich liter będziemy  
zliczać tylko wystąpienia 'a', 'b' i 'c'. 
 

[P110.CPP]  

  
# include "ctype.h"  
# include "iostream.h"   
  
class Licznik   
{    
public:  
  char moja_litera;    
  static int ile;   
  Licznik(char);             //Konstruktor   
  void Skok_licznika();    
  void Pokazuj();   
};  
  
void main()    
{    
/* inicjujemy liczniki:  -------------------------------*/    
   
Licznik licznik_a('a'), licznik_b('b'), licznik_c('c');   
  
/* pracujemy - zliczamy: -------------------------------*/   
   
cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n';   
for(;;)    
  { char znak;   
    cin >> znak;   
    if(znak == '.') break;   
if (znak == licznik_a.moja_litera) licznik_a.Skok_licznika();  
if (znak == licznik_b.moja_litera) licznik_b.Skok_licznika();  
if (znak == licznik_c.moja_litera) licznik_c.Skok_licznika();  
  }   
/* sprawdzamy: ----------------------------------------*/   
   
  
cout << "Wyswietlam wyniki zliczania: \n";    
  licznik_a.Pokazuj();   
  licznik_b.Pokazuj();   
  licznik_c.Pokazuj();  
  
}   
  
Licznik::Licznik(char z)    
{    
  moja_litera = z;    
  ile = 0;    
}    
   
void Licznik::Skok_licznika(void)    
{    

277

background image

  ile++;    
}    
   
void Licznik::Pokazuj(void)    
{    
  cout << "Znak " << moja_litera << " wystapil "    
       << ile << " razy" << '\n';    
}   
  
Tym razem Twój dialog z programem może wyglądać np. tak:  
 
C:\>program  
Wpisz ciag zankow zakonczony kropka [.]  
aaa bbb cccc qwertyQWERTYPOLIPOLIpijesz? nie ojojojojoj.  
Wyswietlam wyniki zliczania:   
Znak a wystapil 10 razy  
Znak b wystapil 10 razy  
Znak c wystapil 10 razy  
  
Jak widać, program się myli. Wszystkie funkcje wyświetlają  
(odwołują się do) zawartości tego samego wspólnego pola.  
 
Charakter (status) statyczny możemy nadać również funkcji  
(metodzie) należącej do danej klasy. Jeśli funkcja otrzyma  
status static, w pamięci będzie istnieć tylko jeden egzemplarz  
danej funkcji i do takiej funkcji można będzie odwoływać się  
podobnie jak do zmiennej statycznej posługując się nazwą obiektu 
 
lub nazwą klasy: 
 
nazwa_obiektu.Funkcja(...);   /* lub */   
nazwa_klasy::Funkcja(...);  
 
Jeżeli funkcja jest tylko jedna, jej działanie nie zależy od  
tego ile obiektów danej klasy zostało utworzone i jakie nazwy  
nadamy tym obiektom. W przykładowym programie powyżej "aż się  
prosi", by nadać status funkcji statycznej metodzie  
wyświetlającej wyniki zliczania:  
 
class Licznik  
{  
 ...  
 static void Pokazuj(void);  
 ...  
}  
 
Sprawdzenie, czy wtedy program przestanie "robić błędy"  
pozostawiamy bardziej dociekliwym Czytelnikom jako zadanie  
domowe.

LEKCJA 31: PRZEKAZANIE OBIEKTÓW JAKO ARGUMENTÓW DO 
FUNKCJI.  

________________________________________________________________ 
W trakcie tej lekcji poznasz sposoby manipulowania obiektami  
przy pomocy funkcji. Poznasz także trochę dokładniej referencje. 
________________________________________________________________ 

278

background image

 
Typowy sposób przekazywania argumentów do funkcji w C++ to  
przekazanie przez wartość (ang. by value). W przypadku obiektów  
oznacza to w praktyce przekazanie do funkcji kopii obiektu. Jako 
 
przykład zastosujemy program zliczający wystąpienia znaków w  
strumieniu wejściowym. Zmienimy w tym programie sposób  
wyprowadzenia wyników. Funkcji Pokazuj() przekażemy jako  
argument obiekt. Obiekt-licznik zawiera w środku tę informację,  
której potrzebuje funkcja - ilość zliczonych znaków. Zacznijmy  
od zdefiniowania klasy. 
  
class Licznik  
{  
public:  
  char moja_litera;  
  int ile;    
  Licznik(char litera);  
  void Skok_licznika();  
};  
 
W programie głównym możemy zastosować konstruktor do  
zainicjowania obiektu np. tak:  
 
main()  
{  
  Licznik licznik_a('a');  
 ... 
 
Zdefiniujmy funkcję. Obiekt licznik_a będzie argumentem funkcji  
Pokazuj(). Funkcja powinna wyprowadzić na ekran zawartość pola  
licznik_a.ile. Deklaracja - prototyp takiej pobierającej obiekt  
funkcji będzie wyglądać tak: 
 
wart_zwracana Nazwa_funkcji(nazwa_klasy nazwa_obiektu); 
 
Nazwa klasy spełnia dokładnie taką samą rolę jak każdy inny typ  
danych. W naszym przypadku będzie to wyglądać tak: 
 
void Pokazuj(Licznik obiekt); 
 
Ponieważ "obiekt" jest parametrem formalnym i jego nazwa nie  
jest tu istotna, możemy pominąć ją w prototypie funkcji (w  
definicji już nie!) i skrócić zapis do postaci:  
 
void Pokazuj(Licznik); 
 
Funkcja Pokazuj() otrzyma w momencie wywołania jako swój  
argument kopię obiektu, którą jako argument formalny funkcji  
nazwaliśmy "obiekt". W naszym programie wywołanie tej funkcji  
będzie wyglądać tak:  
 
  Pokazuj(licznik_a);  
 
Obiekt "licznik_a" jest tu BIEŻĄCYM ARGUMENTEM FAKTYCZNYM. Typ  
(tzn. tu: klasa) argumentu faktycznego musi być oczywiście  
zgodny z zadeklarowanym wcześniej typem argumentu formalnego  
funkcji.  
 
Jeśli funkcja dostała własną kopię obiektu, może odwołać się do  

279

background image

elementów tego obiektu w taki sposób:  
 
void Pokazuj(Licznik obiekt) 
{  
  cout << obiekt.ile;  
}  
 
albo np. tak:  
 
int Pokazuj(Licznik obiekt)  
{  
  return (obiekt.ile);  

 
Należy podkreślić, że funkcja Pokazuj() NIE MA DOSTĘPU do  
oryginalnego obiektu i jego danych. Podobnie jak było to w  
przypadku przekazania zmiennej do funkcji i tu funkcja ma do  
dyspozycji WYŁĄCZNIE SWOJĄ "PRYWATNĄ" KOPIĘ obiektu. Funkcja nie 
 
może zmienić zawartości pól oryginalnego obiektu.  
 
Podobnie, jak w przypadku "zwykłych" zmiennych, jeśli chcemy by  
funkcja działała na polach oryginalnego obiektu, musimy funkcji  
przekazać nie kopię obiektu a wskaźnik (pointer) do tego  
obiektu. Oto program przykładowy w całości:  
 

[P110.CPP]  

 
//UWAGA: Program moze wymagac modelu wiekszego niz SMALL !  
  
# include "ctype.h"   
# include "iostream.h"    
   
class Licznik    
{     
public:   
  char moja_litera;     
  int ile;    
  Licznik(char);    
  void Skok_licznika();     
};   
   
/* Prototypy funkcji (dwie wersje): ---------------- */  
  
void Pokazuj1(Licznik);  
  
int Pokazuj2(Licznik);  
  
  
void main()     
{     
/* inicjujemy licznik:  -------------------------------*/     
    
Licznik licznik_a('a');  
   
/* pracujemy - zliczamy: -------------------------------*/    
    
cout << "Wpisz ciag zankow zakonczony kropka [.]" << '\n';    
for(;;)     

280

background image

  {   
    char znak;    
    cin >> znak;    
    if(znak == '.') break;    
if (znak == licznik_a.moja_litera) licznik_a.Skok_licznika();   
  }    
  
/* sprawdzamy: ----------------------------------------*/    
    
cout << "Wyswietlam wyniki zliczania litery a: \n";     
  
  Pokazuj1(licznik_a);  
  cout << '\n' << Pokazuj2(licznik_a);  
  
}    
   
Licznik::Licznik(char z)     
{     
  moja_litera = z;     
  ile = 0;     
}     
    
void Licznik::Skok_licznika(void)     
{     
  ile++;     
}     
  
/* ------------ Definicje funkcji: ---------------- */  
  
void Pokazuj1(Licznik Obiekt)  
{  
  cout << Obiekt.ile;  
}  
  
int Pokazuj2(Licznik Obiekt)  
{  
  return (Obiekt.ile);  
}       
 

UWAGA:  

________________________________________________________________ 
Programy manipulujące obiektami w taki sposób mogą wymagać  
modelu pamięci większego niż przyjmowany domyślnie model SMALL.  
Typowy komunikat pojawiający się przy zbyt małym modelu pamięci  
to:  
 
Error 43: Type mismatch in parameter to call to  
Pokazuj1(Licznik)...  
(Źły typ argumentu przy wywołaniu funkcji Pokazuj(...)...)  
 
Programy obiektowe są z reguły szybke, ale niestety dość  
"pamięciochłonne". W IDE BORLAND C++ masz do dyspozycji opcję:  
 
Options | Compiler | Code generation | Model  
 
Dokładniejsze informacje o modelach pamięci znajdziesz w dalszej 
 
części książki.  
________________________________________________________________ 

281

background image

 
 

O PROBLEMIE REFERENCJI.  

 
Typowy (domyślny) sposób przekazywania argumentów do funkcji w  
C++ polega na tzw. "przekazaniu przez wartość" i jest inny niż  
Pascalu, czy Basicu. Ponieważ w polskich warunkach do C/C++  
większość adeptów "dojrzewa" po przebrnięciu przez Basic i/lub  
Pascal, programiści ci obciążeni są już pewnymi nawykami i  
pewnym schematyzmem myślenia, który do C++ niestety nie da się  
zastosować i jest powodem wielu pomyłek. To, co w Basicu wygląda 
 
zrozumiale (uwaga, tu właśnie pojawia się automatyzm myślenia):  
 
PRINT X            REM Wyprowadź bieżącą wartość zmiennej X  
INPUT X            REM Pobierz wartość zmiennej X  
 
a w Pascalu:  
 
writeln(X);        { Wyprowadź bieżacą wartość zmiennej X }  
readln(X);         { Pobierz wartość zmiennej X }  
 
przyjmuje w C/C++ formę zapisu wyraźnie dualnego:  
 
printf("%d", X);   //Wyprowadź wartość zmiennej X  
scanf("%d", &X);   //Pobierz wartość zmiennej X 
 
Na czym polega różnica? Jeśli odrzucimy na chwilę automatyzm i  
zastanowimy się nad tą sytuacją, zauważymy, że w pierwszym  
przypadku (wyprowadzanie istniejących już danych - PRINT,  
wrilteln, printf()) w celu poprawnego działania funkcji  
powinniśmy przekazać jej BIEŻĄCĄ WARTOŚĆ ARGUMENTU X (adres  
zmiennej w pamięci nie jest funkcji potrzebny). Dla Basica,  
Pascala i C++ bieżąca wartość zmiennej kojarzoana jest z jej  
identyfikatorem - tu: "X". W drugim jednakże przypadku (pobranie 
 
danych i umieszczenie ich pod właściwym adresem pamięci) jest  
inaczej. Funkcji zupełnie nie interesuje bieżąca wartść zmiennej 
 
X, jest jej natomiast do poprawnego działania potrzebny adres  
zarezerwowany dla zmiennej X w pamięci. Ale tu okazuje się, że  
Basic i Pascal postępują dokładnie tak samo, jak poprzednio:  
 
INPUT X      i      read(X);  
 
Oznacza to, że X nie oznacza dla Pascala i Basica bieżącej  
wartości zmiennej, lecz oznacza (DOMYŚLNIE) przekazanie do  
funkcji adresu zmiennej X w pamięci. Funkcje oczywiście  
"wiedzą", co dostały i dalej już one same manipulują danymi we  
właściwy sposób.  
 
W C++ jest inaczej. Zapis:  
 
Funkcja(X);  
 
oznacza w praktyce, że zostaną wykonane następujące operacje:  
 
* spod adresu pamięci przeznaczonego dla zmiennej X zostanie  
(zgodnie z zadeklarowanym formatem) odczytana bieżąca wartość  

282

background image

zmiennej X;  
* wartość X zostanie zapisana na stos (PUSH X);  
* zostanie wywołana funkcja Funkcja();  
* Funkcja() pobierze sobie wartość argumentu ze stosu (zgodnie z 
 
formatem zadeklarowanym w prototypie Funkcji()).  
* Funkcja() zadziała zgodnie ze swoją definicją i jeśli ma coś  
do pozostawienia (np. return (wynik); ) pozostawi wynik.  
 
Jak widać:  
 
* funkcja "nie wie", gdzie w pamięci umieszczony był przekazany  
jej argument;  
* funkcja komunikuje się "ze światem zewnętrznym" (czyli własnym 
 
programem, bądź funkcją wyższego rzędu - wywołującą) tylko za  
pośrednictwem stosu;  
* funkcja dostaje swoją "kopię" argumentu z którym działa;  
* funkcja nie ma wpływu na "oryginał" argumentu, który pozostaje 
 
bez zmian.  
 

REFERENCJA - CO TO TAKIEGO ?  

 
Zastanówmy się, czym właściwie jest referencja zmiennej w C++.  
Pewne jest, że jest to alternatywny sposób odwołania się do  
zmiennej. Zacznijmy od trywialnego przykładu odwołania się do  
tej samej zmiennej mającej swoją właściwą nazwę "zmienna" i  
referencję "ksywa".  
 
# include "iostream.h"  
 
main()  
{  
  int zmienna;  
  int& ksywa;  
 ...  
 
Aby "ksywa" oznaczała tę samą zmienną, referencję należy  
zainicjować:  
 
int& ksywa = zmienna;  
 
Zainicjujemy naszą zmienną "zmienna" i będziemy robić z nią  
cokolwiek (np. inkrementować). Równocześnie będziemy sprawdzać,  
czy odwołania do zmiennej przy pomocy nazwy i referencji będą  
pozostawać równoważne.  
 

[P111.CPP] 

 
/* UWAGA: Program moze potrzebowac modelu wiekszego niz   
  domyslnie ustawiany MODEL SMALL               */  
 
# include "iostream.h"   
  
main()   
{   

283

background image

  int zmienna = 6666;   
  int& ksywa = zmienna;   
  
  cout << '\n' << "Zmienna" << " Ksywa";  
  cout << '\n' << zmienna << '\t' << ksywa;   
  
  for (register int i = 0; i < 5; i++, zmienna += 100)   
      cout << '\n' << zmienna << '\t' << ksywa;  
  
  return 0;   
}  
  
Dialog (a właściwie monolog) powinien wyglądać tak:  
 
C:\>program  
  
Zmienna Ksywa  
6666    6666 
6666    6666  
6766    6766  
6866    6866  
6966    6966  
7066    7066  
 
Referencje i wskaźniki można stosować a C++ niemal wymiennie  
(dokładniej - nie jest to wymienność wprost, a uzupełnianie na  
zasadzie odwrotności-komplementarności).  
 

TO NIE WSZYSTKO JEDNO!.  

________________________________________________________________ 
Mogłoby się wydawać, że operator adresowy & zyskał dwa RÓŻNE  
zastosowania: określenie adresu w pamęci oraz tworzenie  
wskazania. Aby rozróżnić te dwie sytuacje zwróć uwagę na  
"gramatykę" zapisu. Jeśli identyfikator zminnej jest poprzedzony 
 
określeniem typu zminnej:  
 
int &zmienna;     /* lub */     int &zmienna = ... ;  
 
to zmienną nazywamy "zmienną referencyjną". Jeśli natomiast  
identyfikator nie został poprzedzony określeniem typu:  
 
p = &zmienna;  
 
to mówimy wtedy o adresie zmiennej.  
Przekazanie argumentu do funkcji poprzez referencję jest w  
istocie zbliżone do przekazania wskaźnika do argumentu. Zwróć  
uwagę, że przekazanie wskaźnika do obiektu może zwykle odbyć się 
 
szybciej niż sporządzenie kopii obiektu i przekazanie tej kopii  
do funkcji. Zastosowanie w deklaracji funkcji operatora  
adresowego & pozwala nam stosować syntaktykę zapisu taką "jak  
zwykle" - przy przekazaniu przez wartość. Jeśli nie chcemy  
ryzykować zmian wprowadzonych do oryginalnego parametru  
przekazanego funkcji poprzez wskazanie, możemy zadeklarować  
oryginalny parametr jako stałą (kompilator "dopilnuje" i  
uniemożliwi zmianę wartości):  
 
nazwa_funkcji(const &nazwa_obiektu);  

284

background image

________________________________________________________________ 
 
Poprosimy C++ by pokazał nam konkretne fizyczne adresy  
skojarzone z identyfikatorami "zmienna" i "ksywa". Operator &  
oznacza dla C++  
 
&X --> adres w pamięci zmiennej X  
 

[P112.CPP]  

 
/* UWAGA: Program moze potrzebowac modelu wiekszego niz   
  domyslnie ustawiany MODEL SMALL               */  
 
# include "iostream.h"  
 
main()  
{  
  int zmienna = 6666;  
  int& ksywa = zmienna;  
 
  cout << "Zmienna (ADR-hex)   Ksywa (ADR-hex): \n\n"; 
  cout << hex << &zmienna << "\t\t" << &ksywa;  
 
  return 0;  
}  
 
Monolog programu powinien wyglądać tak:  
 
Zmienna (ADR-hex)   Ksywa (ADR-hex):   
  
0x287efff4            0x287efff4  
 
Fizyczny adres pamięci, który "kojarzy się" C++ ze zmienną i  
ksywą jest identyczny. Referencja nie oznacza zatem ani  
sporządzania dodatkowej kopii zmiennej, ani wskazania do  
zmiennej w rozumieniu wskaźnika (pointer). Jest to inna metoda  
odwołania się do tej samej pojedynczej zmiennej.  

LEKCJA 33: WSKAŹNIKI DO OBIEKTÓW.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak posługiwać się obiektami  
za pośrednictwem wskaźników.  
________________________________________________________________ 
 
Wskaźniki do obiektów funkcjonują podobnie jak wskaźniki do  
struktur. Operator -> pozwala na dostęp zarówno do danych jak i  
do funkcji. Dla przykładu wykorzystamy obiekt naszej prywatnej  
klasy Licznik.  
 
class Licznik 
{  
public:  
  char moja_litera;  
  int ile;  
  Licznik(char znak) { moja_litera = z; ile = 0; }  

285

background image

  void Skok_licznika(void) { ile++; } 
};  
 
Aby w programie można było odwołać się do obiektu nie poprzez  
nazwę a przy pomocy wskaźnika, zadeklarujemy wskaźnik do  
obiektów klasy Licznik: 
 
Licznik *p;  
 
Wskaźnik w programie możemy zastosować np. tak:  
 
p->Skok_licznika();  
 
(czytaj: Wywołaj metodę "Skok_licznika()" w stosunku do obiektu  
wskazywanego w danym momencie przez wskaźnik p) 
 
Trzeba pamiętać, że sama deklaracja w przypadku referencji i  
wskaźników nie wystarcza. Przed użyciem należy jeszcze  
zainicjować wskaźnik w taki sposób, by wskazywał na nasz  
obiekt-licznik. Wskaźnik do obiektu inicjujemy w taki sam sposób 
 
jak każdy inny pointer:  
 
p = &Obiekt;  
 
Możemy przystąpić do utworzenia programu przykładowego.  
 

[P119.CPP] 

  
# include "ctype.h"    
# include "iostream.h"    
   
class Licznik   
{    
public:    
  char moja_litera;    
  int ile;    
  Licznik(char z) { moja_litera = z; ile = 0; }    
  void Skok_licznika(void) { ile++; }   
};    
   
void main()    
{    
  char znak;    
  cout << "\nPodaj litere do zliczania: ";    
  cin >> znak;   
   
  Licznik Obiekt1(znak), Obiekt2('a'), *p1, *p2;    
  p1 = &Obiekt1;   
  p2 = &Obiekt2;  
  cout << "\n Wpisz ciag znakow";   
  cout << "zakonczony kropka [.] i [Enter] \n";   
  for(;;)   
     {    
      cin >> znak;    
      if(znak == '.') break;   
      if(znak == p1->moja_litera) p1->Skok_licznika();    
      if(znak == p2->moja_litera) p2->Skok_licznika();  
     }    

286

background image

  cout << "\nBylo " << p1->ile;   
  cout << " liter: " << p1->moja_litera;    
  p1 = p2;  
  cout << "\nBylo " << p1->ile;   
  cout << " liter: " << p1->moja_litera;    
}    
   
  
Możemy oczywiście np. stosować przypisanie, inkrementować i  
dekrementować pointer oraz realizować arytmetykę na wskaźnikach  
dokładnie tak samo, jak w przypadku innych zmiennych.  
 

this - WSKAŹNIK SPECJALNY.  

 
Poświęcimy teraz chwilę uwagi pewnemu specjalnemu wskaźnikowi.  
Specjalnemu (i ważnemu) na tyle, że aż "dorobił się" w C++  
własnego słowa kluczowego "this".  
 
Każdej funkcji - metodzie zadeklarowanej wewnątrz klasy zostaje  
w momencie wywołania w niejawny sposób (ang. implicitly)  
przekazany wskaźnik do obiektu (w stosunku do którego funkcja ma 
 
zadziałać). Pointer wskazuje funkcji w pamięci ten obiekt,  
którego członkiem jest dana funkcja. Bez istnienia takiego  
właśnie wskaźnika nie moglibyśmy stosować spokojnie funkcji, nie 
 
moglibyśmy odwoływać się do pola obiektu, gdybyśmy nie wiedzieli 
 
jednoznacznie, o który obiekt chodzi. Program posługuje się  
automatycznie niejawnym wskaźnikiem do obiektu (ang. implicit  
pointer). Możemy wykorzystać ten istniejący, choć do tej pory  
nie widoczny dla nas pointer posługując się słowem kluczowym  
this (ten). This pointer wskazuje na obiekt, do którego należy  
funkcja. Korzystając z tego wskaźnika funkcja może bez cienia  
wątpliwości zidentyfikować właśnie ten obiekt, z którym pracuje  
a nie obiekt przypadkowy.  
 

FUNKCJE KATEGORII static NIE OTRZYMUJĄ POINTERA this.  

Należy pamiętać, że wskaźnik this istnieje wyłącznie podczas  
wykonywania metod (ang. class member function execution), za  
wyjątkiem funkcji statycznych. 
 
Jeśli w programie zadeklarujemy klasę Klasa:  
 
class Klasa  
{  
  int dane; 
 ... 

 
a wewnątrz tej klasy metodę Pokazuj():  
 
class Klasa  
{  
  int dane; 
public:  
  void Pokazuj();  

287

background image

 ...  
}  
 
void Klasa::Pokazuj(void)  
{  
  cout << dane;  
}  
 
To zdefiniowanie funkcji Pokazuj() z zastosowaniem pointera this 
 
i notacji wskaźnikowej (p->), jak poniżej, będzie równoważne:  
 
void Klasa::Pokazuj(void)  
{  
  cout << this->dane;  
}  
 
Przypomnijmy, że taka notacja wskaźnikowa oznacza:  
"Wyprowadź zawartość pola "dane" obiektu, na który wskazuje  
wskaźnik" (ponieważ jest to wskaźnik this, więc chodzi o własny  
obiekt).  

LEKCJA 34 OVERLOADING OPERATORÓW. 

________________________________________________________________ 
Podczas tej lekcji poznasz możliwości dostosowania operatorów  
C++ do własnego "widzimisię" i do potrzeb własnych obiektów.  
________________________________________________________________ 
 
Niemal od początku niniejszej książki korzystamy z operatorów  
poddanych overloadingowi. Są to operatory << i >> , które  
pierwotnie wykonywały bitowe przesunięcie w lewo i w prawo.  
Owerloading tych operatorów "załatwił" za nas producent  
(Borland, Microsoft, czy inny). Jak widzisz, nie powoduje to w  
dalszym użytkowaniu tych operatorów żadnych zauważalnych  
komplikacji, a często ułatwia tworzenie programów. Zwróć uwagę,  
że overloading operatorów (jak i definicje klas) może znajdować  
się w dołączonych plikach nagłówkowych i po jednorazowym  
wykonaniu może być "niewidoczny" dla programistów tworzących  
programy aplikacyjne. 
 
Jeśli projektujemy (definiujemy) nową klasę, dodajemy do C++  
nowy, lecz pełnoprawny typ danych. Autorzy C++ nie byli w stanie 
 
przewidzieć jakie klasy i jakie obiekty mogą wymyślić kolejne  
pokolenia programistów w ramach swojej radosnej twórczości.  
Wprowadzili zatem do C++ jasne i jednoznaczne algorytmy  
postępowania z typami "typowymi". C++ doskonale wie jak dodawać, 
 
mnożyć, czy odejmować np. liczby int, long, float itp., nie wie  
jednak jak dodać do siebie obiekty klas CString (CString = Class 
 
String = klasa "łańcuch znaków"), TOdcinek (to taki kawałek  
prostej) itp.. A przecież miło byłoby, gdyby rozbudować  
 działanie operatorów tak, by było możliwe ich typowe  
zastosowanie w stosunku do naszych własnych, "nietypowych"  
obiektów:  

288

background image

 
int x, y;     int z = x + y;      //To operator + załatwia sam 
float x, y;   float z = x + y;  
 
Zanim jednak stanie się możliwe postępowanie takie: 
 
class CString x, y, z;     z = x + y;  
 
class Nasza_Klasa obiekt1, obiekt2, obiekt3;    
obiekt3 = obiekt1 + obiekt2;  
 
itp., itd. ... 
 
musimy "uzupełnić" C++ i "wyjaśnić" operatorom, co właściwie ma  
w praktyce oznaczać operacja   obiekt1 = obiekt2 + obiekt3; .  
Jest wyczuwalne intuicyjnie, że działanie operatorów w stosunku  
do różnych obiektów może być różne. Dla przykładu - wiesz  
zapewne, że inaczej wygląda algorytm mnożenia liczb zespolonych, 
 
a inaczej liczb całkowitych rzeczywistych. Dlatego też wykonanie 
 
operacji mnożenia wymaga od operatora * podjęcia różnych  
działań:  
 
class Liczba_zespolona x, y, z;         z = x * y;  
 
int x, y, z;                            z = x * y;  
 
Czasem może się zdarzyć, że dla dwu różnych klas działanie  
jakiegoś operatora jest identyczne, częściej jednak (i tak  
należy się spodziewać) działanie operatora dla każdej klasy  
będzie odrębne i unikalne.  
 
Pójdźmy w tym rozumowaniu o krok dalej. Skoro rozszerzenie  
obszaru zastosowań jakiegoś operatora na obiekty nowej  
(nieznanej wcześniej klasy) wymaga zdefiniowania nowego  
algorytmu działania operatora, C++ będzie potrzebował do tego  
celu specjalnych środków, które powinny być łatwo rozpoznawalne. 
 
Do opisu algorytmów służą generalnie w C++ funkcje i tu Autorzy  
nie wprowadzili wyjątku. Zastrzegli jednak dla tych specjalnych  
funkcji specjalną nazwę:              operator ...();  
 
I tak funkcja precyzująca nowy algorytm dodawania (nowy sposób  
działania operatora + ) będzie się nazywać:  
 
operator+();  
 
a np. funkcja określająca nowy algorytm mnożenia (nowy sposób  
działania operatora * ) będzie się nazywać:  
 
operator*();  
 
Spróbujmy zastosować taką filozofię w praktyce programowania.  
 

NIESTETY NIE WSZYSTKIE OPERATORY MOŻNA ROZBUDOWAĆ. 

________________________________________________________________ 
Są w C++ operatory, których nie możemy poddać overloadingowi. Są 
 

289

background image

to:  
 
.   ::   .*   ?:  
 
.   operator kropki umożliwia dostęp do pól struktur i obiektów; 
 
::  operator "widoczności-przesłaniania" (ang. scope);  
.*  wskazanie członka klasy (ang. pointer-to-member);  
?:  operator warunkowy.  
________________________________________________________________ 
 
Wszystkie pozostałe operatory możemy poddać overloadingowi i  
przypisywać im potrzebne nam działanie. 
 

OVERLOADING OPERATORA [+] (DWUARGUMENTOWEGO). 

 
Zaczniemy od operatora + należącego do grupy "dwuargumentowych  
operatorów arytmetycznych" (ang. binary arithmetic operator).  
Zwracamy tu już na początku rozważań uwagę na przynależność  
operatora do określonej grupy, ponieważ overloading różnych  
opertorów należących do tej samej grupy przebiega podobnie.  
Ponieważ znak + może być także operatorem jednoargumentowym  
(ang. unary plus, o czym za chwilę), podkreślamy, że tym razem  
chodzi o plus jako operator dodawania. Overloading operatora  
przeprowadzimy w stosunku do obiektów prostej, znanej Ci już z  
poprzednich przykładów klasy Data, którą (w celu upodobnienia  
się do maniery stosowanej w Windows i bibliotekach klas)  
nazwiemy tym razem CData. "Namówimy" operator + do  
przeprowadzenia operacji na obiektach (dokładniej na polach  
obiektów):  
 
CData nowadata = staradata + 7;       // W tydzien pozniej  
 
Operator + musi oczywiście "wiedzieć", na którym polu obiekty  
klasy CData przechowują liczbę dni i jak związane są (logicznie) 
 
pola obiektu dz, mc, rok. Jest rzeczą zrozumiałą, że samo  
dodanie dni do pola dz może nie wystarczyć, ponieważ data  
37.11.93 jest niedopuszczalna.  
 
Jeśli staradata jest obiektem klasy CData z zawartymi wewnątrz  
danymi, to w wyniku działania "nowego" operatora + powinien  
powstać obiekt nowadata klasy CData, którego pola zostaną w  
sensowny sposób powiększone o dodaną liczbę dni. Rozważ  
działanie programu (najlepiej skompiluj i uruchom).  
 
[P120.CPP]  
 
/*  Overloading operatora dwuargumentowego +     */  
 
# include <iostream.h>  
 
class CData  
{  
  int dz, mc, rok;  
public:  
  CData() {}         //Konstruktor domyslny (pusty) 
  CData(int d, int m, int y) { mc = m; dz = d; rok = y; }  
  void Pokazuj() { cout << dz << '.' << mc << '.' << rok; }  

290

background image

  CData operator+(int);       //TU! overloading operatora +  
};  
  
static int TAB[] = {31,28,31,30,31,30,31,31,30,31,30,31};  
 
/* Definicja funkcji operatorowej: ------------------------ */ 
 
CData CData::operator+(int n)  
{  
  CData kopia_obiektu = *this;  
  n += kopia_obiektu.dz;  
  while (n > TAB[kopia_obiektu.mc-1]) 
    {  
      n -= TAB[kopia_obiektu.mc-1];  
      if (++kopia_obiektu.mc == 13) 
         { kopia_obiektu.mc = 1; kopia_obiektu.rok++; }  
    }  
  kopia_obiektu.dz = n;  
  return (kopia_obiektu);  
}  
  
main()  
{  
  CData staradata(31, 1, 94);    //Kostruktor z argumentami 
  CData nowadata;                //Pusty konstruktor 
  cout << "\n Stara data: ";  
  staradata.Pokazuj();  
  cout << "\n Podaj ile minelo dni --> ";  
  int n;  
  cin >> n; 
  nowadata = staradata + n; 
  cout << "\n Jest zatem -->  "; 
  nowadata.Pokazuj();  
  return 0; 
}  
  
Do tej pory do danych prywatnych obiektu mogliśmy sięgnąć  
wyłącznie przy pomocy zdefiniowanej wewnątrz klasy  
funkcji-metody. Metodą umożliwiającą nam dostęp do prywatnych  
danych obiektu jest tu zadeklarowana wewnątrz klasy (a więc  
mająca "status prawny" metody) funkcja operatorowa. Przyjrzyjmy  
się tej funkcji dokładniej:  
 
CData CData::operator+(int n)  
{  
  CData kopia_obiektu = *this; 
 ...  
  return (kopia_obiektu);  

 
Funkcja   
* została zdefiniowana dla obiektów klasy CData (z innymi  
postępować nie potrafi);  
Jeśli operator + zostanie umieszczony pomiędzy obiektem klasy  
CData, a liczbą typu int:  
                             .... staradata + n;  
* funkcja pobiera liczbę n jako argument (jawnie);  
* funkcja pobiera obiekt klasy CData jako swój drugi argument  
(niejawnie, dzięki pointerowi this); 
* funkcja zwróci obiekt klasy CData (ze zmodyfikowanym polem);  

291

background image

 
Nowy obiekt zwrócony przez funkcję zostanie przypisany  
 
nowadata = ... ;      // <-- return(kopia_obiektu);  
 
W prawym polu operatora (operator jest dwuargumentowy, ma więc  
swoje lewe i prawe pole) może pojawić także stała. Operacja:  
 
nowadata = staradata + 14;  
 
zostanie wykonana poprawnie.  
 
Ale to nie wszystko. Jeśli wystąpi układ odwrotny - np.:  
 
nowadata = 14 + staradata;  
 
nasz operator "zgłupieje". Doszedłszy do operatora + C++ "nie  
będzie jeszcze wiedział" (analizuje wyrażenia arytmetyczne od  
lewej do prawej), KTÓRY obiekt wystąpi za chwilę. Jedno jest  
pewne, nie zawsze musi być to "własny" obiekt funkcji, do  
którego mamy pointer this. Aby uzyskać jednoznaczność sytuacji,  
funkcja operatorowa powinna tu w jawny sposób pobierać przed  
zadziałaniem dwa argumenty:  
 
CData operator+(int n, CData obiekt);  
 
aby działanie:  
 
CData obiekt_wynik;     obiekt_wynik = n + obiekt;  
 
stało się wykonalne. Pojawia się tu wszakże pewien problem.  
Wskaźnik this wskazuje własny obiekt funkcji-metody, a tym razem 
 
funkcja potrzebuje dostępu nie do pola własnego obiektu, lecz do 
 
pola "obcego" obiektu przekazanego jej jako argument. Ale w C++  
możemy:  
 
* zdefiniować dwie (i więcej) funkcji o tej samej nazwie (każda  
na inną ewentualność);  
* możemy nadać funkcji status friend (wtedy nie będąc metodą też 
 
uzyska dostęp do danych obiektu).  
 
Definicja naszej klasy CData zawierająca deklaracje dwu funkcji  
operatorowych operator+() różniących się zastosowaniem i (po  
czym rozpozna je C++) liczbą argumentów, będzie wyglądać tak:  
 
class CData  
{  
  int dz, mc, rok;  
public:  
  CData() {}  
  CData(int d, int m, int y) { mc = m; dz = d; rok = y; }  
  void Pokazuj() { cout << dz << '.' << mc << '.' << rok; }  
/* Dwie funkcje operatorowe: ------------------------------ */ 
  CData operator+(int);  
  friend CData operator+(int, CData&); 
};  
 

292

background image

Zastosowaliśmy zamiast kopii obiektu bezpośrednio przekazywanej  
funkcji - referencję do obiektu klasy CData - CData&. Klasa  
zawiera:  
* prywatne dane;  
* dwa konstruktory;  
* własną metodę - funkcję operatorową operator+();  
* deklarację zaprzyjaźnionej z klasą funkcji kategorii friend  
(choć jest to funkcja o tej samej nazwie, jej status i  
uprawnienia są nieco inne).  
 

NIE WSZYSTKO, CO WEWNĄTRZ JEST METODĄ.  

________________________________________________________________ 
Nawet, jeśli wewnątrz definicji klasy zdefiniujemy w pełni  
funkcję (nadając jej status inline), nie stanie się ona metodą!  
Słowo kluczowe friend określa status funkcji jednoznacznie, bez  
względu na to, w którym miejscu w tekście programu umieścimy  
definicję ciała funkcji.  
________________________________________________________________ 
 
 
W zasadzie ciało funkcji jest na tyle proste (wymagamy od niej  
tylko zwrotu obiektu ze zmodyfikowanym polem danych), że możemy  
skorzystać z rozbudowanego wcześniej operatora + i całe ciało  
zdefiniować tak:  
 
class CData  
{  
  int dz, mc, rok;  
public:  
 ... 
  CData operator+(int);  
  friend CData operator+(int n, CData& x) { return (x + n); } 
};  
 
Jeśli w operacji dodawania argumenty zastosujemy we  
wcześniejszej kolejności:  
 
  return (obiekt + liczba);  
 
to zostanie tu wykorzystany operator + rozbudowany poprzednio  
przez metodę  CData::operator+(int). Program w całości może  
zatem wyglądać tak:  
 

[P121.CPP]  

 
# include "iostream.h"  
 
class CData  
{  
  int dz, mc, rok;  
public:  
  CData() {}  
  CData(int d, int m, int y) { mc = m; dz = d; rok = y; }  
  void Pokazuj() { cout << dz << '.' << mc << '.' << rok; }  
  CData operator+(int);  
  friend CData operator+(int n, CData& x) { return (x + n); } 
};  

293

background image

  
static int TAB[] = {31,28,31,30,31,30,31,31,30,31,30,31};  
 
CData CData::operator+(int n)  
{  
  CData kopia_obiektu = *this;  
  n += kopia_obiektu.dz;  
  while (n > TAB[kopia_obiektu.mc-1]) 
    {  
      n -= TAB[kopia_obiektu.mc-1];  
      if (++kopia_obiektu.mc == 13) 
         { kopia_obiektu.mc = 1; kopia_obiektu.rok++; }  
    }  
  kopia_obiektu.dz = n;  
  return (kopia_obiektu);  
}  
  
main()  
{  
  CData staradata(31, 1, 94);    //Kostruktor z argumentami 
  CData nowadata, jeszczejednadata;  
  cout << "\n Stara data: ";  
  staradata.Pokazuj();  
  cout << "\n Podaj ile minelo dni --> ";  
  int n;  
  cin >> n; 
  nowadata = staradata + n; 
  cout << "\n Jest zatem -->  "; 
  nowadata.Pokazuj();  
  cout << "\n Testuje nowy operator:  "; 
  jeszczejednadata = (1+n) + staradata;  
  jeszczejednadata.Pokazuj(); 
  return 0; 
}  
 
Operator + w obu sytuacjach działa poprawnie. Być może wpadłeś  
na pomysł, że operator - (minus) też mamy już z głowy. Niby tak, 
 
ale tylko w takim zakresie, w jakim nasza funkcja operatorowa  
poprawnie będzie obsługiwać ujemne liczby dni. Jeśli zechcesz  
podać ujemną liczbę dni (zmuszając funkcję do odejmowania  
zamiast dodawania), twój dialog z programem będzie wyglądał np.  
tak:  
  
C:\>program 
 Stara data: 31.1.94  
 Podaj ile minelo dni -->  -10 
 Jest zatem -->  21.1.94  
 Testuje nowy operator:  22.1.94  
 
lub tak:  
 
C:\>program  
 Stara data: 31.1.94  
 Podaj ile minelo dni -->  -150 
 Jest zatem -->  -119.1.94  
 Testuje nowy operator:  -118.1.94  
 
Funkcja operatorowa została napisana w taki sposób, że po  
przekroczeniu wartości -31 program będzie wypisywał bzdury. Jako 

294

background image

 
zadanie domowe - spróbuj zmodyfikować algorytm w taki sposób, by 
 
rozszerzyć zakres poprawnych wartości.  
 

Możesz dodawać obiekty minusem. 

________________________________________________________________ 
* Należy tu zwrócić uwagę, że dodawanie obiektów może wykonywać  
nie tylko i nie koniecznie operator + . Jeśli zechcesz, możesz  
do tego celu zastosować dowolnie wybrany operator (np. -, *  
itp.). W celu ułatwienia zrozumienia zapisu (i tylko dlatego)  
większość programistów rozbudowuje działanie operatorów zgodnie  
z ich pierwotnym zastosowaniem.  
* DOWOLNOŚĆ, ALE NIE PEŁNA!  
O tyle, o ile działanie operatora może być zmienione, to ilość  
argumentów potrzebnych operatorowi pozostaje w C++ "sztywna"  
(patrz przykład z n!). 
________________________________________________________________ 
 
W bardzo podobny sposób możesz rozbudowywać inne arytmetyczne  
operatory dwuargumentowe (*, /, -, itp.) w stosunku także do  
innych klas.  
 

OVERLOADING OPERATORÓW JEDNOARGUMENTOWYCH ++ I -- .  

 
Typowe operatory jednoargumentowe to ++ i --. Jako przykładem  
posłużymy się problemem zlicznia znaków pobieranych ze  
strumienia wejściowego.  
 
Zaczniemy od redefinicji postinkrementacji licznika. Musimy  
zastosować funkcję operatorową. Funkcja, chcąc operować na  
obiektach musi w stosunku do tych obiektów posiadać status  
friend, lub być metodą. Prototyp funkcji operatorowej potrzebnej 
 
do wykonania overloadingu operatora jednoargumentowego ++  
wygląda w postaci ogólnej tak:  
 
typ_zwracany nazwa_klasy::operator++(lista argumentów);  
 
Funkcje operatorowe zwracają zwykle wartość zgodną co do typu z  
typem obiektów z którymi współpracują. Jeśli identyfikatory b, c 
 
i d reprezentują obiekty, nic nie stoi na przeszkodzie, by stał  
się możliwy zapis:  
 
class Klasa  
{  
 ...  
} x, y, z;  
 ... 
z = x + y;  
 
Dodajemy dwa obiekty x i y tego samego typu (tej samej klasy), a 
 
wynik przypisujemy obiektowi z, który także jest obiektem tego  
samego typu. Jeśli możnaby jeszcze zastosować operator  
przypisania tak:  

295

background image

 
z = q = x + y;  
 
operator przypisania = zwracałby nam w efekcie obiekt tego  
samego typu. Funkcje operatorowe muszą przestrzegać tych samych  
zasad, które obowiązują wyrażenia: typ argumentów x, y, z, q,  
... powinien być zgodny, rezultat operacji (x + y) powinien być  
obiektem tego samego typu, co obiekty x, y, z, q. Dokonując  
overloadingu operatorów powinniśmy precyzyjnie określić typ  
wartości zwracanej w wyniku działania operatora.  
 
Stosowaną poprzednio do inkrementacji liczników metodę  
Skok_licznika() zastąpimy w definicji klasy funkcją operatorową: 
 
 
class Licznik  
{  
public:  
  char moja_litera;  
  int ile;  
  Licznik(char);  
  Licznik operator++();  
}; 
 
Powinniśmy teraz zdefiniować funkcję operatorową. Ponieważ pole  
obiektu, które zamierzamy inkrementować nazywa się:  
 
obiekt.ile       // Licznik::ile; 
 
funkcja powinna zadziałać tak:  
 
Licznik Licznik::operator++(void)  
{  
  this->ile++;  
  return (*this); 

 
Przetłumaczmy tę notację na "ludzki język". Funkcja operatorowa: 
 
 
* nie pobiera żadnych jawnych argumentów (void);  
* jest metodą, zatem w momencie wywołania otrzymuje w niejawny  
sposób wskaźnik *this do "własnego" obiektu;  
* posługując się wsakźnikiem this inkrementuje zawartość pola  
  int ile własnego obiektu;  
* zwraca obiekt (zmodyfikowany) klasy Licznik (tj. dokładniej -  
zwraca wskaźnik this do własnego-zmodyfikowanego obiektu.  
 
Ponieważ funkcja operatorowa jest metodą zadeklarowaną wewnątrz  
klasy, bez problemu uzyska dostęp do wewnętrznych pól obiektów  
tej klasy i wykona inkrementację licznika. Możemy zatem  
zastosować wyrażenie typu:  
 
Licznik licznik_a;           licznik_a++; 
 
Funkcja jest metodą wraz ze wszystkimi właściwymi metodom  
przywilejami. Zapis możemy zatem uprościć do postaci:  
 
Licznik Licznik::operator++(void)  
{  

296

background image

  ile++;  
  return (*this); 

 
a tak skrócone ciało funkcji umieścić w definicji klasy obok  
definicji konstruktora: 
 
class Licznik  
{  
public:  
  char moja_litera;  
  int ile;  
  Licznik(char z) { ile = 0; moja_litera = z; } 
  Licznik operator++() { ile++; return (this); }  
}; 
 
Aby nie zaciemniać obrazu, przy pomocy licznika będziemy tym  
razem zliczać wszystkie znaki za wyjątkiem kropki. Ponieważ  
licznik nie będzie miał swojej ulubionej litery, możemy  
zastosować pusty konstruktor. 
 
[P121.CPP]  
/* --------------------- POST - inkrementacja ----------- */  
# include "iostream.h"   
  
class Licznik   
{   
public:   
  int ile;   
  Licznik() { ile = 0;}  
  Licznik operator++() { ile++; return (*this); }   
} obiekt;  
  
void main()   
{   
   cout << "\n Wpisz kilka znakow: ";   
   char znak;   
   for(;;)   
    {   
      cin >> znak;   
      if(znak == '.') break;   
      obiekt++;   
    }   
   cout << "\n Wpisales " << obiekt.ile << " znakow";   
}  
  
Podobnie jak wcześniej, preinkrementacja i postinkrementacja  
wymagają odrębnego overloadingu. Dokładnie rzecz ujmując,  
zgodnie ze standardem ANSI C, odrębny overloading nie jest już  
niezbędny, wykorzystamy to jednak jako pretekst do wykonania go  
dwiema różnymi technikami. Ponieważ logika jest bardzo podobna,  
pomijamy tu (chyba już zbędny) komemtarz. Dla ułatwienia Ci  
porównania, zestawiliśmy obok siebie różne funkcje operatorowe  
napisane różnymi technikami (notacja wskaźnikowa i  
referencyjna).  
 

[P122.CPP]  

 
/* -------- PRE - inkrementacja ------------------------- */   

297

background image

# include "iostream.h"    
   
class Licznik    
{    
public:    
  int ile;    
  Licznik() { ile = 0;}   
  Licznik operator+(int n = 1)   
  { this->ile += n; return (*this); }    
  Licznik friend operator++(Licznik& x)  
  { x + 1; return (x); }  
} obiekt;   
   
void main()  
{    
   cout << "\n Wpisz kilka znakow: ";    
   char znak;    
   for(;;)    
    {    
      cin >> znak;    
      if(znak == '.') break;    
      ++obiekt;    
    }    
   cout << "\n Wpisales " << obiekt.ile << " znakow";    
   cout << "\n I dodamy jeszcze sto! --> ";  
   obiekt + 100;  
   cout << obiekt.ile;  
}   
  
 
Poniżej inny przykład tego samego overloadingu odnośnie tej  
samej klasy Licznik (w trochę inny sposób). 
 

[P123.CPP]  

 
# include "conio.h"  
# include "iostream.h"  
  
class Licznik   
{   
public:   
     char moja_litera;   
     int ile;   
     Licznik() { ile = 0; }   //Pusty konstruktor  
     Licznik(char);   
     Licznik operator++();    //Funkcja pre/post-inkrementacji  
     Licznik operator--();    //Funkcja pre/post-dekrementacji   
};   
  
Licznik::Licznik(char z) { moja_litera = z; ile = 10; }   
Licznik Licznik::operator++(void) { ile++; return *this; }  
Licznik Licznik::operator--(void) { ile--; return *this; }   
  
  
void main()   
{   
  Licznik obiekt1('A'), obiekt2;    //obiekt2 - "pusty"  
  
  cout << "\n Wpisz napis z max. 10 literami [A]: \n   ";   

298

background image

  for(;;)  
    {   
      char litera = getch();    cout << litera;  
  
      if(obiekt1.ile == 0) break;  
        
      if(litera == obiekt1.moja_litera) obiekt1--;   
        
      ++obiekt2;                  //Ten zlicza wszystkie znaki  
                                  //metoda PRE - inkrementacji 
      if(obiekt2.ile > 30) cout << "\n NIE PRZESADZAJ \n";  
    }   
  
  cout << "\n Koniec: " << obiekt1.ile;   
  cout << " liter " << obiekt1.moja_litera;  
  cout << "\n Wszystkich znakow bylo: " << obiekt2.ile;  
}   
  
Overloading "siostrzanych" operatorów ++ i -- jest bliźniaczo  
podobny.  
 

OVERLOADING OPERATORA !  

 
Z matematyki jesteśmy przyzwyczajenu do zapisu silni n! i  
wydawałoby się, że mając w C++ do dyspozycji operator ! nie  
powinniśmy mieć z tym zadaniem najmniejszego kłopotu. Operując  
znaną Ci klasą Liczba i wyposażając program w funkcję  
operatorową możemy załatwić ten problem np. tak:  
 

[P124.CPP]  

 
# include <iostream.h>  
  
class Liczba  
{  
public:  
  long wartosc;  
  Liczba(int x) { wartosc = (long) x; }  
  friend void operator!(Liczba&);  
};  
 
void operator!(Liczba& obiekt)  
{  
 long wynik = 1;  
 for(int i = 1; i <= obiekt.wartosc; i++)   
   {  
   wynik *= i;  
   }  
 cout << '\n' << wynik;  
}  
  
int x;  
  
main()  
{  
for(int k = 0; k < 5; k++)  
   {  

299

background image

    cout << "\n Podaj liczbe --> ";  
    cin >> x;  
    Liczba a(x);  
    cout << "\n Silnia wynosi:   ";  
    !a;  
    }  
return 0;  

 
Program działa, wyniki kolejnych kilku silni są poprawne. Gdy  
jednak spróbujemy zastosować operator ! zgodnie z tradycyjnym  
matematycznym zapisem:  a!;  okaże się, że C++ zacznie mieć  
wątpliwości. Komunikaty o błędzie spowodują wątpliwości  
kompilatora, czy chodzi nam o operator "!=", w którym  
zapomnięliśmy znaku "=". Jeśli w funkcji operatorowej spróbujemy 
 
zmienić operator ! na != , a zapis w programie: 
 
z    !a;        na       a!=a; 
 
C++ zarząda dwuargumentowej funkcji operatorowej (bo taki  
operator jest tradycyjnie dwuargumentowy). Możemy oczywiście  
próbować oszukać C++ przy pomocy argumentu pozornego. Jeśli  
podamy w funkcji operatorowej dwa argumenty 
 
void operator!=(Liczba& obiekt1, Liczba& obiekt2)  
{  
 long wynik = 1;  
 for(int i = 1; i <= obiekt.wartosc; i++)   
   {  
   wynik *= i;  
   }  
 cout << '\n' << wynik;  
}  
 
program uda się skompilować i kod wynikowy będzie działał  
poprawnie, C++ zaprotestuje jedynie przy pomocy ostrzeżenia  
 
Warning: obiekt2 is never used...  
 
Chcąc uniknąć ostrzeżeń należy użyć argument pozorny w dowolny  
sposób. Zwracamy na to uwagę, ponieważ C++ jest pedantem i:  
 

DZIAŁANIE OPERATORÓW MOŻE BYĆ DALECE DOWOLNE, ALE LICZBA 
ARGUMENTÓW MUSI POZOSTAĆ ZGODNA Z "TRADYCJAMI" C++.  

 
Stosowanie podczas overloadingu operatorów argumentów pozornych  
jest techniką często stosowaną przez programistów.  
 
Aby wykazać, że korzystanie z gotowych "fabrycznych" zasobów  
ułatwia życie programiście czasami w zaskakująco skuteczny  
sposób, przytoczę przykładowy program, który posługując się  
"fabryczną" klasą ofstream (obiekty - strumień danych do pliku  
wyjściowego - Output File STREAM):  
 
* zakłada w bieżącym katalogu plik dyskowy DANE.TST;  
* otwiera plik dla zapisu;  
* zapisuje do pliku tekst "to jest zawartosc pliku";  

300

background image

* zamyka plik;  
 

[P125.CPP]  

 
# include "fstream.h"  
  
void main()  
{  
  ofstream plik("dane.tst");  
  plik << "To jest zawartosc pliku";  
}  
  
I już. O wszystkie szczegóły techniczne tych (wcale przecież nie 
 
najprostszych) operacji zadbał producent w bibliotekach klas  
Wejścia/Wyjścia. Jeśli zechcemy do pliku dopisać coś jeszcze,  
wystarczy dodać:  
 

[P126.CPP]  

 
# include "fstream.h"  
  
void main()  
{  
  ofstream plik("dane.tst");  
  plik << "To jest zawartosc pliku" << " i jeszcze cosik.";  
}  
 
Urzekająca prostota, nieprawdaż? I to wszystko załatwia poddany  
overloadingowi operator << . Niedowiarek mógłby w tym momencie  
zapytać "a jeśli plik już istnieje, to chyba nie jest takie  
proste?". Rzeczywiście, należałoby tu rozbudować program w C++  
do postaci: 
 
# include "fstream.h"  
void main()  
{  
  ofstream plik("dane.tst", ios::app);  
  plik << " Dopiszemy do pliku jeszcze i to...";  
}  
 
Korzystamy tu dodatkowo z globalnej zmiennej ios::app (ang.  
append - dołącz) określającej inny niż typowy tryb dostępu do  
pliku dyskowego i w dalszym ciągu z operatora << . Tworzenie  
obiektu - pliku dyskowego jest takie proste, dzięki istnieniu  
konstruktora, który jest tu automatycznie wywoływany po  
deklaracji:  ofstream plik( ... ); 
 

Zadania

________________________________________________________________ 
1. Wykonaj samodzielnie overloading dowolnego operatora.  
_______________________________________________________________ 

301

background image

LEKCJA 35:   O ZASTOSOWANIU DZIEDZICZENIA.  

________________________________________________________________ 
Z tej lekcji dowiesz się, do czego w praktyce programowania  
szczególnie przydaje się dziedziczenie.  
________________________________________________________________ 
 
Dzięki dziedziczeniu programista może w pełni wykorzystać gotowe 
 
biblioteki klas, tworząc własne klasy i obiekty, jako klasy  
pochodne wazględem "fabrycznych" klas bazowych. Jeśli bazowy  
zestw danych i funkcji nie jest adekwatny do potrzeb, można np.  
przesłonić, rozbudować, bądź przebudować bazową metodę dzięki  
elastyczności C++. Zdecydowana większość standardowych klas  
bazowych wyposażana jest w konstruktory. Tworząc klasę pochodną  
powinniśmy pamiętać o istnieniu konstruktorów i rozumieć sposoby 
 
przekazywania argumentów obowiązujące konstruktory w przypadku  
bardziej złożonej struktury klas bazowych-pochodnych. 
 

PRZEKAZANIE PARAMETRÓW DO WIELU KONSTRUKTORÓW.  

 
Klasy bazowe mogą być wyposażone w kilka wersji konstruktora.  
Dopóki nie przekażemy konstruktorowi klasy bazowej żadnych  
argumentów - zostanie wywołany (domyślny) pusty konstruktor i  
klasa bazowa będzie utworzona z parametrami domyślnymi. Nie  
zawsze jest to dla nas najwygodniejsza sytuacja. 
 
Jeżeli wszystkie, bądź choćby niektóre z parametrów, które  
przekazujemy konstruktorowi obiektu klasy pochodnej powinny  
zostać przekazane także konstruktorowi (konstruktorom) klas  
bazowych, powinniśmy wytłumaczyć to C++. Z tego też powodu,  
jeśli konstruktor jakiejś klasy ma jeden, bądź więcej  
parametrów, to wszystkie klasy pochodne względem tej klasy  
bazowej muszą posiadać konstruktory. Dla przykładu dodajmy  
konstruktor do naszej klasy pochodnej Cpochodna:  
 
 
class CBazowa1  
{  
public:  
  CBazowa1(...);              //Konstruktor 
};  
 
class CBazowa2  
{   
public:  
  CBazowa2(...);              //Konstruktor 
};  
 
class Cpochodna : public CBazowa1, CBazowa2     //Lista klas  

public:  
  Cpochodna(...);             //Konstruktor 
};  
 
main()  
{  
  Cpochodna Obiekt(...);          //Wywolanie konstruktora 

302

background image

 ... 
 
W momencie wywołania kostruktora obiektu klasy pochodnej  
Cpochodna() przekazujemy kostruktorowi argumenty. Możemy (jeśli  
chcemy, nie koniecznie) przekazać te argumenty konstruktorom  
"wcześniejszym" - konstruktorom klas bazowych. Ta możliwość  
okazuje się bardzo przydatna (niezbędna) w środowisku obiektowym 
 
- np. OWL i TVL. Oto prosty przykład definiowania konstruktora w 
 
przypadku dziedziczenia. Rola konstruktorów będzie polegać na  
trywialnej operacji przekazania pojedynczego znaku. 
 
class CBazowa1  

public:  
  CBazowa1(char znak) { cout << znak; }  
};  
 
class CBazowa2  

public:  
  CBazowa2(char znak) { cout << znak; }  
};  
 
class Cpochodna : public CBazowa1, CBazowa2  

public:  
  Cpochodna(char c1, char c2, char c3);   
};  
 
Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2),  
 CBazowa2(c3)  
{  
     cout << c1;  
}  
 
Konstruktor klasy pochodnej pobiera trzy argumenty i dwa z nich: 
 
c2 --> przekazuje do konstruktora klasy CBazowa1  
c3 --> przekazuje do konstruktora klasy CBazowa2  
Sposób zapisu w C++ wygląda tak:  
 
Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2),  
 CBazowa2(c3)  
 
Możemy zatem przekazać parametry "w tył" do konstruktorów klas  
bazowych w taki sposób:  
 
kl_pochodna::kl_pochodna(lista):baza1(lista), baza2(lista), ...  
 
gdzie:  
lista - oznacza listę parametrów odpowiedniego konstruktora.  
 
W takiej sytuacji na liście argumentów konstruktorów klas  
bazowych mogą znajdować się także wyrażenia, przy założeniu, że  
elementy tych wyrażeń są widoczne i dostępne (np. globalne  
stałe, globalne zmienne, dynamicznie inicjowane zmienne globalne 
 
itp.). Konstruktory będą wykonywane w kolejności:  

303

background image

 
CBazowa1 --> CBazowa2 --> Cpochodna 
 
Dzięki tym mechanizmom możemy łatwo przekazywać argumenty  
"wstecz" od konstruktorów klas pochodnych do konstruktorów klas  
bazowych.  
 

FUNKCJE WIRTUALNE. 

 
Działanie funkcji wirtualnych przypomina rozbudowę funkcji  
dzięki mechanizmowi overloadingu. Jeśli, zdefiniowaliśmy w  
klasie bazowej funkcję wirtualną, to w klasie pochodnej możemy  
definicję tej funkcji zastąpić nową definicją. Przekonajmy się o 
 
tym na przykładzie. Zacznijmy od zadeklarowania funkcji  
wirtualnej (przy pomocy słowa kluczowego virtual) w klasie  
bazowej. Zadeklarujemy jako funkcję wirtualną funkcję oddychaj() 
 
w klasie CZwierzak:  
 
class CZwierzak  
{  
public:  
  void Jedz();  
  virtual void Oddychaj();  
};  
 
Wyobraźmy sobie, że chcemy zdefiniować klasę pochodną CRybka  
Rybki nie oddychają w taki sam sposób, jak inne obiekty klasy  
CZwierzak. Funkcję Oddychaj() trzeba zatem będzie napisać w dwu  
różnych wariantach. Obiekt Ciapek może tę funkcję odziedziczyć  
bez zmian i sapać spokojnie, z Sardynką gorzej: 
 
class CZwierzak  
{  
public:  
  void Jedz();  
  virtual void Oddychaj() { cout << "Sapie..."; } 
};  
 
class CPiesek : public CZwierzak  
{  
  char imie[30];  
} Ciapek; 
 
class CRybka 
  char imie[30];  
public:  
  void Oddychaj() { cout << "Nie moge sapac..."; }  
} Sardynka;  
 
 
Zwróć uwagę, że w klasie pochodnej w deklaracji funkcji słowo  
kluczowe virtual już nie występuje. W klasie pochodnej funkcja  
CRybka::Oddychaj() robi więcej niż w przypadku "zwykłego"  
overloadingu funkcji. Funkcja CZwierzak::Oddychaj() zostaje  
"przesłonięta" (ang. overwrite), mimo, że ilość i typ  
argumentów. pozostaje bez zmian. Taki proces - bardziej  
drastyczny, niż overloading nazywany jest przesłanianiem lub  

304

background image

nadpisywaniem funkcji (ang. function overriding). W programie  
przykładowym Ciapek będzie oddychał a Sardynka nie.  
 

[P127.CPP] 

 
# include <iostream.h>  
 
class CZwierzak  
{  
public:  
  void Jedz();  
  virtual void Oddychaj() {cout << "\nSapie...";} 
};  
 
class CPiesek : public CZwierzak  
{  
  char imie[30];  
} Ciapek; 
 
class CRybka 
  char imie[30];  
public:  
  void Oddychaj() {cout << "\nSardynka: A ja nie oddycham.";} 
} Sardynka;  
 
void main()  
{  
  Ciapek.Oddychaj();  
  Sardynka.Oddychaj(); 
}  
 
Funkcja CZwierzak::Oddychaj() została w obiekcie Sardynka  
przesłonięta przez funkcję CRybka::Oddychaj() - nowszą wersję  
funkcji-metody pochodzącą z klasy pochodnej.   
 
Overloading funkcji zasadzał się na "typologicznym pedantyźmie"  
C++ i na dodatkowych informacjach, które C++ dołącza przy  
kompilacji do funkcji, a które dotyczą licznby i typów  
argumentów danej wersji funkcji. W przypadku funkcji wirtualnych 
 
jest inaczej. Aby wykonać przesłanianie kolejnych wersji funkcji 
 
wirtualnej w taki sposób, funkcja we wszystkich "pokoleniach"  
musi mieć taki sam prototyp, tj. pobierać taką samą liczbę  
parametrów tych samych typów oraz zwracać wartość tego samego  
typu. Jeśli tak się nie stanie, C++ potraktuje różne prototypy  
tej samej funkcji w kolejnych pokoleniach zgodnie z zasadami  
overloadingu funkcji. Zwróćmy tu uwagę, że w przypadku funkcji  
wirtualnych o wyborze wersji funkcji decyduje to, wobec którego  
obiektu (której klasy) funkcja została wywołana. Jeśli wywołamy  
funkcję dla obiektu Ciapek, C++ wybierze wersję  
CZwierzak::Oddychaj(), natomiast wobec obiektu Sardynka zostanie 
 
zastosowana wersja CRybka::Oddychaj().  
 
W C++ wskaźnik do klasy bazowej może także wskazywać na klasy  
pochodne, więc zastosowanie funkcji wirtualnych może dać pewne  
ciekawe efekty "uboczne". Jeśli zadeklarujemy wskaźnik *p do  
obiektów klasy bazowej CZwierzak *p; a następnie zastosujemy ten 

305

background image

 
sam wskaźnik do wskazania na obiekt klasy pochodnej: 
 
p = &Ciapek;     p->Oddychaj();  
 ...  
p = &Sardynka;   p->Oddychaj(); 
 
zarządamy w taki sposób od C++ rozpoznania właściwej wersji  
wirtualnej metody Oddychaj() i jej wywołania we właściwym  
momencie. C++ może rozpoznać, którą wersję funkcji należałoby  
zastosować tylko na podstawie typu obiektu, wobec którego  
funkcja została wywołana. I tu pojawia się pewien problem.  
Kompilator wykonując kompilcję programu nie wie, co będzie  
wskazywał pointer. Ustawienie pointera na konkretny adres  
nastąpi dopiero w czasie wykonania programu (run-time).  
Kompilator "wie" zatem tylko tyle:  
 
p->Oddychaj()();    //która wersja Oddychaj() ???  
 
Aby mieć pewność, co w tym momencie będzie wskazywał pointer,  
kompilator musiałby wiedzieć w jaki sposób będzie przebiegać  
wykonanie programu. Takie wyrażenie może zostać wykonane "w  
ruchu programu" dwojako: raz, gdy pointer będzie wskazywał  
Ciapka (inaczej), a drugi raz - Sardynkę (inaczej):  
 
  CZwierzak *p;  
 ... 
  for(p = &Ciapek, int i = 0; i < 2; i++)  
     {  
       p->Oddychaj();  
       p = &Sardynka;  
     }  
 
lub inaczej:  
 
  if(p == &Ciapek) CZwierzak::Oddychaj();  
  else CRybka::Oddychaj(); 
 
Taki efekt nazywa się polimorfizmem uruchomieniowym (ang.  
run-time polymorphism).  
 
Overloading funkcji i operatorów daje efekt tzw. polimorfizmu  
kompilacji (ang. compile-time), to funkcje wirtualne dają efekt  
polimorfizmu uruchomieniowego (run-time). Ponieważ wszystkie  
wersje funkcji wirtualnej mają taki sam prototyp, nie ma innej  
metody stwierdzenia, którą wersję funkcji należy zastosować.  
Wybór właściwej wersji funkcji może być dokonany tylko na  
podstawie typu obiektu, do którego należy wersja funkcji-metody. 
 
Różnica pomiędzy polimorfizmem przejawiającym się na etapie  
kompilacji i poliformizmem przejawiającym się na etapie  
uruchomienia programu jest nazywana również wszesnym albo póżnym 
 
polimorfizmem (ang. early/late binding). W przypadku wystąpienia 
 
wczesnego polimorfizmu (compile-time, early binding) C++ wybiera 
 
wersję funkcji (poddanej overloadingowi) do zastosowania już  
tworząc plik .OBJ. W przypadku późnego polimorfizmu (run-time,  
late binding) C++ wybiera wersję funkcji (poddanej przesłanianiu 

306

background image

 
- overriding) do zastosowania po sprawdzeniu bieżącego kontekstu 
 
i zgodnie z bieżącym wskazaniem pointera.  
 
Przyjrzyjmy się dokładniej zastosowaniu wskaźników do obiektów w 
 
przykładowym programie. Utworzymy hierarchię złożoną z klasy  
bazowej i pochodnej w taki sposób, by klasa pochodna zawierała  
jakiś unikalny element - np. nie występującą w klasie bazowej  
funkcję.  
 
class CZwierzak  
{  
public:  
  void Jedz();  
  virtual void Oddychaj() {cout << "\nSapie...";} 
};  
 
class CPiesek : public CZwierzak  
{  
  char imie[20];  
  void Szczekaj() { cout << "Szczekam !!!"; } 
} Ciapek; 
 
Jeśli teraz zadeklarujemy wskaźnik do obiektów klasy bazowej:  
 
CZwierzak *p; 
 
to przy pomocy tego wskaźnika możemy odwołać się także do  
obiektów klasy pochodnej  oraz do elementów obiektu klasy  
pochodnej - np. do funkcji p->Oddychaj(). Ale pojawia się tu  
pewien problem. Jeśli zechcelibyśmy wskazać przy pomocy pointera 
 
taki element klasy pochodnej, który nie został odziedziczony i  
którego nie ma w klasie bazowej? Rozwiązanie jest proste -  
wystarczy zarządać od C++, by chwilowo zmienił typ wskaźnika z  
obiektów klasy bazowej na obiekty klasy pochodnej. W przypadku  
funkcji Szczekaj() w naszym programie wyglądałoby to tak:  
 
CZwierzak *p;  
 ...  
  p->Oddychaj(); 
  p->Szczekaj();             //ŹLE !  
  (CPiesek*)p->Szczekaj();   //Poprawnie 
 ... 
 
Dzięki funkcjom wirtualnym tworząc klasy bazowe pozwalamy  
późniejszym użytkownikom na rozbudowę funkcji-metod w  
najwłaściwszy ich zdaniem sposób. Dzięki tej "nieokreśloności"  
dziedzicząc możemy przejmować z klasy bazowej tylko to, co nam  
odpowiada. Funkcje w C++ mogą być jeszcze bardziej  
"nieokreślone" i rozbudowywalne. Nazywają się wtedy funkcjami w  
pełni wirtualnymi.  

307

background image

LEKCJA 36: FUNKCJE WIRTUALNE i KLASY ABSTRAKCYJNE.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, co mawia żona programisty, gdy 
 
nie chce być obiektem klasy abstrakcyjnej.  
________________________________________________________________ 
 

FUNKCJE W PEŁNI WIRTUALNE (PURE VIRTUAL). 

 
W skrajnych przypadkach wolno nam umieścić funkcję wirtualną w  
klasie bazowej nie definiując jej wcale. W klasie bazowej  
umieszczamy wtedy tylko deklarację-prototyp funkcji. W  
następnych pokoleniach klas pochodnych mamy wtedy pełną swobodę  
i możemy zdefiniować funkcję wirtualną w dowolny sposób -  
adekwatny dla potrzeb danej klasy pochodnej. Możemy np. do klasy 
 
bazowej (ang. generic class) dodać prototyp funkcji wirtualnej  
funkcja_eksperymentalna() nie definiując jej w (ani wobec)  
klasie bazowej. Sens umieszczenia takiej funkcji w klasie  
bazowej polege na uzyskaniu pewności, iż wszystkie klasy  
pochodne odziedziczą funkcję funkcja_eksperymentalna(), ale  
każda z klas pochodnych wyposaży tę funkcję we własną definicję. 
 
Takie postępowanie może okazać się szczególnie uzasadnione przy  
tworzeniu biblioteki klas (class library) przeznaczonej dla  
innych użytkowników. C++ w wersji instalacyjnej posiada już  
kilka gotowych bibliotek klas. Funkcje wirtuale, które nie  
zostają zdefiniowane - nie posiadają zatem ciała funkcji -  
nazywane są funkcjami w pełni wirtualnymi (ang. pure virtual  
function).  
 

O KLASACH ABSTRAKCYJNYCH.  

 
Jeśli zadeklarujemy funkcję CZwierzak::Oddychaj() jako funkcję w 
 
pełni wirtualną, oprócz słowa kluczowego virtual, trzeba tę  
informację w jakiś sposób przekazać kompilatorowi C++. Aby C++  
wiedział, że naszą intencją jest funkcja w pełni wirtalna, nie  
możemy zadeklarować jej tak:  
 
class CZwierzak  
{  
 ... 
public:  
virtual void Oddychaj();  
 ... 
};  
 
a następnie pominąć definicję (ciało) funkcji. Takie  
postępowanie C++ uznałby za błąd, a funkcję - za zwykłą funkcję  
wirtualną, tyle, że "niedorobioną" przez programistę. Naszą  
intencję musimy zaznaczyć już w definicji klasy w taki sposób:  
 
class CZwierzak  
{  
 ... 

308

background image

public:  
virtual void Oddychaj() = 0; 
 ... 
};  
 
Informacją dla kompilatora, że chodzi nam o funkcję w pełni  
wirtualną, jest dodanie po prototypie funkcji "= 0". Definiując  
klasę pochodną możemy rozbudować funkcję wirtualną np.:  
 
class CZwierzak  
{  
 ... 
public:  
  virtual void Oddychaj() = 0; 
 ... 
};  
 
class CPiesek : public CZwierzak  
{  
 ...  
public:  
  void Oddychaj() { cout << "Oddycham..."; }  
 ...  
}; 
 
Przykładem takiej funkcji jest funkcja Mów() z przedstawionego  
poniżej programu. Zostawiamy ją w pełni wirtualną, ponieważ  
różne obiekty klasy CZLOWIEK i klas pochodnych  
  
class CZLOWIEK  
{  
public:  
  void Jedz(void);  
  virtual void Mow(void) = 0;   //funkcja WIRTUALNA  
};  
  
class NIEMOWLE : public CZLOWIEK  
{  
public:  
  void Mow(void);   // Tym razem BEZ slowa virtual 
};  
/* Tu definiujemy metodę wirtualną: -------------------- */  
void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; };  
  
 
mogą mówić na różne sposoby... Obiekt Niemowle, dla przykładu,  
nie chce mówić wcale, ale z innymi obiektami może być inaczej.  
Wyobraź sobie np. obiekt klasy Żona (żona to przecież też  
człowiek !). 
 
class Zona : public CZLOWIEK  
{  
public:  
  void Mow(void);  
}  
 
W tym pokoleniu definicja wirtualnej metody Mow() mogłaby  
wyglądać np. tak:  
 
void Zona::Mow(void) 

309

background image

{  
  cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!! ";  
  cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!"; 
 
//... itd., itd., itd...  

 

[P128.CPP]  

 
#include "iostream.h"  
  
class CZLOWIEK  
{  
public:  
  void Jedz(void);  
  virtual void Mow(void) = 0;  
};  
  
void CZLOWIEK::Jedz(void) { cout << "MNIAM, MNIAM..."; };  
 
class Zona : public CZLOWIEK  
{  
  public:  
  void Mow(void);       //Zona mowi swoje 
};                      //bez wzgledu na argumenty (typ void) 
 
void Zona::Mow(void)  
{  
  cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!!";  
  cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!"; 
 

 
class NIEMOWLE : public CZLOWIEK  
{  
public:  
  void Mow(void);  
};  
  
void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; };  
 
main() 
{  
  NIEMOWLE Dziecko;  
  Zona Moja_Zona; 
  
  Dziecko.Jedz();  
  Dziecko.Mow();  
  Moja_Zona.Mow() 
 
  return 0;  

 
Przykładowa klasa CZŁOWIEK jest klasą ABSTRAKCYJNĄ. Jeśli  
spróbujesz dodać do powyższego programu np.:  
 
CZLOWIEK Facet;  
Facet.Jedz();  
 

310

background image

uzyskasz komunikat o błędzie:  
 
Cannot create a variable for abstract class "CZLOWIEK"  
(Nie mogę utworzyć zmiennych dla klasy abstrakcyjnej "CZLOWIEK"  
 

KLASY ABSTRAKCYJNE. 

________________________________________________________________ 
* Po klasach abstrakcyjnych MOŻNA dziedziczyć! 
* Obiektów klas abstrakcyjnych NIE MOŻNA stosować bezpośrednio! 
________________________________________________________________ 
 
Ponieważ wyjaśniliśmy, dlaczego klasy są nowymi typami danych,  
więc logika (i sens) innej rozpowszechnionej nazwy klas  
abstrakcyjnych - ADT - Abstract Data Type (Abstrakcyjne Typy  
Danych) jest chyba zrozumiała i oczywista. 
 

ZAGNIEŻDŻANIE KLAS I OBIEKTÓW.  

 
Może się np. zdarzyć, że klasa stanie się wewnętrznym elementem  
(ang. member) innej klasy i odpowiednio - obiekt - elementem  
innego obiektu. Nazywa się to fachowo "zagnieżdżaniem" (ang.  
nesting). Jeśli, dla przykładu klasa CB będzie zawierać obiekt  
klasy CA:  
 
class CA  
{  
  int liczba;  
public:  
  CA() { liczba = 0; }           //Konstruktor domyslny  
  CA(int x) { liczba = x; }  
  void operator=(int n) { liczba = n }  
};  
 
class CB  
{  
  CA obiekt;  
public:  
  CB() { obiekt = 1; }  
};  
 
Nasze klasy wyposażyliśmy w konstruktory i od razu poddaliśmy  
overloadingowi operator przypisania = . Aby prześledzić  
kolejność wywoływania funkcji i sposób przekazywania parametrów  
pomiędzy tak powiązanymi obiektami rozbudujemy każdą funkcję o  
zgłoszenie na ekranie.  
 
class CA  
{  
  int liczba;  
public:  
  CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }  
  CA(int x) { liczba = x; cout << "->CA(int) "; }  
  void operator=(int n) { liczba = n; cout << "->operator "; }  
};  
 
class CB  
{  

311

background image

  CA obiekt;  
public:  
  CB() { obiekt = 1; cout << "->Konstruktor CB() "; }  
};  
 
Możemy teraz sprawdzić, co stanie się w programie po  
zadeklarowaniu obiektu klasy CB:  
 

[P129.CPP]  

 
# include "iostream.h"  
 
class CA  
{  
  int liczba;  
public:  
  CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }  
  CA(int x) { liczba = x; cout << "->CA(int) "; }  
  void operator=(int n) { liczba = n; cout << "->operator "; }  
};  
 
class CB  
{  
  CA obiekt;  
public:  
  CB() { obiekt = 1; cout << "->Konstruktor CB() "; }  
};  
 
main()  
{  
  CB Obiekt;  
  return 0;  
}  
 
Po uruchomieniu programu możesz przekonać się, że kolejność  
działań będzie następująca:  
 
C:\>program  
-> CA(), CA_O::liczba = 0 ->operator ->Konstruktor CB()  
 
Skoro oprócz zainicjowania obiektu klasy pochodnej nie robimy w  
programie dokładnie nic, nie dziwmy się ostrzeżeniu  
 
Warning: Obiekt is never used...  
 
Jest to sytuacja trochę podobna do komunikacji pomiędzy  
konstruktorami klas bazowych i pochodnych. Jeśli zaprojektujemy  
prostą strukturę klas:  
 
class CBazowa  
{  
private:  
  int liczba; 
public:  
  CBazowa() { liczba = 0}  
  CBazowa(int n) { liczba = n; }  
};  
 
class CPochodna : public CBazowa 

312

background image

{  
public:  
  CPochodna() { liczba = 0; }  
  CPochodna(int x) { liczba = x; } 
};  
 
problem przekazywania parametrów między konstruktorami klas  
możemy w C++ rozstrzygnąć i tak:  
 
class CPochodna : public CBazowa 
{  
public:  
  CPochodna() : CBazowa(0) { liczba = 0; }  
  CPochodna(int x) { liczba = x; } 
};  
 
Będzie to w praktyce oznaczać wywołanie konstruktora klasy  
bazowej z przekazanym mu argumentem 0. Podobnie możemy postąpić  
w stosunku do klas zagnieżdżonych:  
 

[P130.CPP]  

 
#include "iostream.h"  
 
class CA  
{  
  int liczba;  
public:  
  CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }  
  CA(int x) { liczba = x; cout << "->CA(int) "; }  
  void operator=(int n) { liczba = n; cout << "->operator "; }  
};  
 
class CB  
{  
  CA obiekt;  
public:  
  CB() : CA(1) {}  
};  
 
main()  
{  
  CB Obiekt;  
  return 0;  
}  
 
Eksperymentując z dwoma powyższymi programami możesz przekonać  
się, jak przebiega przekazywanie parametrów pomiędzy  
konstruktorami i obiektami klas bazowych i pochodnych.  
 

JESZCZE RAZ O WSKAŹNIKU *this.  

 
Szczególnie ważnym wskaźnikiem przy tworzeniu klas pochodnych i  
funkcji operatorowych może okazać się pointer *this. Oto  
przykład listy.  
 

313

background image

[P131.CPP]  

  
# include "string.h"   
# include "iostream.h"   
  
class CLista   
{   
private:  
  char *poz_listy;   
  CLista *poprzednia;   
public:   
  CLista(char*);   
  CLista* Poprzednia() { return (poprzednia); };   
  void Pokazuj() { cout << '\n' << poz_listy; }   
  void Dodaj(CLista&);   
  ~CLista() { delete poz_listy; }   
};   
   
CLista::CLista(char *s)   
{   
  poz_listy = new char[strlen(s)+1];   
  strcpy(poz_listy, s);   
  poprzednia = NULL;   
}   
  
void CLista::Dodaj(CLista& obiekt)   
{   
  obiekt.poprzednia = this;   
}   
   
main()   
{   
CLista *ostatni = NULL;   
cout << '\n' << "Wpisanie kropki [.]+[Enter] = Quit \n";  
  for(;;)  
    {   
      cout << "\n Wpisz nazwe (bez spacji): ";   
      char TAB[70];   
      cin >> TAB;   
      if (strncmp(TAB, ".", 1) == 0) break;   
      CLista *lista = new CLista(TAB);   
      if (ostatni != NULL)   
      ostatni->Dodaj(*lista);   
      ostatni = lista;   
    }   
  
  for(; ostatni != NULL;)  
    {   
      ostatni->Pokazuj();   
      CLista *temp = ostatni;   
      ostatni = ostatni->Poprzednia();   
      delete (temp);   
    }   
  return 0;  
}   
   
Z reguły to kompilator nadaje wartość wskaźnikowi this i to on  
automatycznie dba o przyporządkowanie pamięci obiektom. Pointer  
this jest zwykle inicjowany w trakcie działania konstruktora  
obiektu.  

314

background image

LEKCJA 37: KAŹDY DYSK JEST ZA MAŁY, A KAŹDY PROCESOR 
ZBYT WOLNY...  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak komputer dysponuje swoimi  
zasobami w środowisku tekstowym (DOS).  
________________________________________________________________ 
 
Truizmy użyte w tytule mają znaczyć, że "zasoby najlepszego  
nawet komputera są ograniczone" i zwykle okazują się  
wystarczające tylko do pewnego momentu. Najbardziej newralgiczne 
 
zasoby to:  
 
* czas mikroprocesora i  
* miejsce w pamięci operacyjnej.  
 
Tworzone przez nas programy powinny wystrzegać się zatem  
najcięższych grzechów:  
 
* nie pozwalać mikroprocesorowi na słodkie nieróbstwo;  
 
Rzadko uzmysławiamy sobie, że oczekiwanie na naciśnięcie  
klawisza przez użytkownika (czasem po przeczytaniu napisu na  
ekranie) trwa sekundy (1, 2, .... czasem 20), a każda sekunda  
lenistwa PC to stracone miliony cykli mikroprocesora.  
 
* oszczędnie korzystać z pamięci dyskowej, a szczególnie  
oszczędnie z pamięci operacyjnej RAM. 
 

MODELE PAMIĘCI IBM PC.  

 
Jak zapewne wiesz, Twój PC może mieć:  
 
* pamięć ROM (tylko do odczytu),  
* konwencjonalną pamięć RAM (640 KB),  
* pamięć rozszerzoną EMS i XMS,  
* pamięć karty sterownika graficznego ekranu (np. SVGA-RAM),  
* pamięć Cache dla buforowania operacji dyskowych.  
 
Najczęściej stosowane modele pamięci to:  
 
* Small - mały,  
* Medium - średni,  
* Compact - niewielki (tu mam wątpliwość, może "taki sobie" ?),  
* Large - duży,  
* Huge - jeszcze większy, odległy.  
 
Dodatkowo może wystąpić  
 
* Tiny - najmniejszy.  
 
Taki podział został spowodowany segmentacją pamięci komputera  
przez procesory Intel 8086 i podziałem pamięci na bloki o  
wielkości 64 KB. Model Small (Tiny, jeśli jest) jest najszybszy, 
 

315

background image

ale najmniej pojemny. Model Huge - odwrotnie - najpojemniejszy,  
za to najwolniejszy. Model Tiny powoduje ustawienia wszystkich  
rejestrów segmentowych mikroprocesora na tę samą wartość  
(początek tej samej stronicy pamięci) i umieszczenie wszystkich  
zasobów programu wewnątrz wspólnego obszaru pamięci o wielkości  
nie przekraczającej 64 KB. Wszystkie skoki są wtedy "krótkie", a 
 
wszystkie pointery (adresy) 16-bitowe. Kompilacja z  
zastosowaniem modelu Tiny pozwala uzyskać program wykonywalny w  
wersji *.COM (a nie *.EXE). Ale niestety nie wszystkie programy  
mieszczą się w 64 KB. W modelu Small segment kodu jest jeden  
(kod max. 64 K) i segment danych też tylko jeden (dane max. 64  
K), ale są to już dwa różne segmenty. Zestawienia  
najważniejszych parametrów poszczególnych modeli pamięci  
przedstawia tabelka poniżej:  
 
Modele pamięci komputera IBM PC.  
________________________________________________________________ 
 
Model    Segment kodu     Segment danych   *dp        *cp  
________________________________________________________________ 
 
Tiny           1             1  (CS = DS)   16 bit     16 bit  
Small          1             1              16 bit     16 bit  
Medium      wiele            1              16 bit     32 bit  
Compact        1           wiele            32 bit     16 bit  
Large       wiele          wiele            32 bit     32 bit  
Huge        wiele          wiele            32 bit     32 bit  
________________________________________________________________ 
 
*dp - data pointer - wskaźnik do danych (near/far)  
*cp - code pointer - wskaźnik do kodu.  
Large - kod + dane = max. 1 MB.  
Huge - kod = max. 1 MB, wiele segmentów danych po 64 K każdy.  
 
Wynikające z takich modeli pamięci kwalifikatory near, far, huge 
 
dotyczące pointerów w C++ nie są akceptowane przez standard ANSI 
 
C (ponieważ odnoszą się tylko do IBM PC i nie mają charakteru  
uniwersalnego). Trzeba tu zaznaczyć, że typ wskaźnika jest przez 
 
kompilator przyjmowany domyślnie (automatycznie) zgodnie z  
wybranym do kompilacji modelem pamięci. Jeśli poruszamy się  
wewnątrz niewielkiego obszaru pamięci, możesz "forsować" bliższy 
 
typ pointera, przyspieszając tym samym działanie programów:  
 
huge *p;  
 ... 
near *ptr;                   //Bliski pointer 
 ...  
 near int Funkcja(...)       //Bliska funkcja 
{  
 ...  
}  
#define ILE  (1024*640)  
 
near unsigned int Funkcja(void)  
{  

316

background image

  huge char *ptr;          // tu długi pointer jest niezbędny 
  long suma = 0;  
  for (p = 0; p < ILE; p++) suma += *p;  
  return (suma);  
}  
 
Zarówno zadeklarowanie funkcji jako bliskiej (near), jak i jako  
statycznej (static) powoduje wygenerowanie uproszczonej  
sekwencji wywołania funkcji przez kompilator. Daje to w efekcie  
mniejszy i szybszy kod wynikowy.  
 

IDENTYFIKACJA KLAWISZY.  

 
Znane Ci z pliku <stdio.h> i <conio.h> "klasyczne" funkcje  
obsługi konsoli mają pewne zalety. Korzystanie z klasycznych,  
nieobiektowych mechanizmów powoduje z reguły wygenerowanie  
znacznie krótszego kodu wynikowego. Funkcje scanf() i gets()  
wymagają wciśnięcia klawisza [Enter]. Dla szybkiego dialogu z  
komputerem znacznie bardziej nadają się szybsze getch() i  
kbhit(). Ponieważ klawiatura zawiera także klawisze specjalne  
(F1 ... F10, [Shift], [Del], itp.), pełną informację o stanie  
klawiatury można uzyskać za pośrednictwem funkcji bioskey(),  
korzystającej z przerywania BIOS Nr 16. Oto krótki przykład  
zastosowania funkcji bioskey():  
 
#include "bios.h"  
#include "ctype.h"   
#include "stdio.h"   
#include "conio.h"  
  
# define CTRL   0x04   
# define ALT    0x08   
# define RIGHT  0x01   
# define LEFT   0x02   
  
   int klawisz, modyfikatory;   
void main()   
{                                        
  clrscr();  
  printf("Funkcja zwraca : %d", bioskey(1));   
  printf("\n Nacisnij klawisz ! \n");   
  while (!bioskey(1));  
  printf("Funkcja zwrocila: %c", bioskey(1));  
  printf("\nKod: %d", (char)bioskey(1));  
 ... 
 
A to jeszcze inny sposób korzystania z tej bardzo przydatnej  
funkcji, tym razem z innymi parametrami:  
 
/* Funkcja z parametrem (0) zwraca kod klawisza: ------ */  
  
  klawisz = bioskey(0);   
   
/* Funkcja sprawdza stan klawiszy specjalnych --------- */   
  
   modyfikatory = bioskey(2);   
   if (modyfikatory)   
   {   
      printf("\n");   

317

background image

      if (modyfikatory & RIGHT) printf("RIGHT");   
      if (modyfikatory & LEFT)  printf("LEFT");   
      if (modyfikatory & CTRL)  printf("CTRL");   
      if (modyfikatory & ALT)   printf("ALT");   
      printf("\n");   
   }   
   /* drukujemy pobrany klawisz */   
   if (isalnum(klawisz & 0xFF))   
      printf("'%c'\n", klawisz);   
   else   
      printf("%#02x\n", klawisz);   
}   
  
Należy tu zwrócić uwagę, że funkcje kbhit() i bioskey() nie  
dokonują czyszczenia bufora klawiatury. Identyfikują znak  
(znaki) w buforze, ale pozostawiają bufor w stanie niezmienionym 
 
do dalszej obróbki. Zwróć uwagę, że funkcja getch() może  
oczekiwać na klawisz w nieskończoność. Sprawdzić szybciej, czy  
użytkownik nacisnął już cokolwiek możesz np. tak:  
 
if (kbhit()) ...;          if (!kbhit()) ...;  
 
while (!bioskey(1)) ...     if (bioskey(1)) ...; 
 
Inną wielce przydatną "szybką" funkcją jest getch(). Oto  
praktyczny przykład pobierania i testowania naciśniętych  
klawiszy klawiatury.  
 
[P131.CPP]  
  
# include "stdio.h"   
# include "conio.h"  
  
char z1, z2;  
  
void Odczyt(void)   
{   
  z2 = '\0';   
  z1 = getch();   
  if (z1 == '\0') z2 = getch();   
}  
  
main()   
{   
  clrscr();   
  printf("\nKropka [.] = Quit");  
  printf("\nRozpoznaje klawisze [F1] ... [F3] \n\n");  
  
  for (;;)   
   {   
    while(!kbhit());  
    Odczyt();   
    if (z1 == '.') break;  
    if (z1 != '\0') printf("\nZnak: %c", z1);   
    else   
    switch (z2)   
            {   
            case ';' : printf("\n  F1"); break;   
            case '<' : printf("\n  F2"); break;   

318

background image

            case '=' : printf("\n  F3"); break;   
    default  : printf("\n Inny klawisz specjalny!");  
             }   
   }  
  return 0;  
}  
  
Klawisze specjalne powodują wygenerowanie dwubajtowego kodu  
(widzianego w powyższym przykładowym programie jako dwa  
jednobajtowe znaki z1 i z2). Funkcja getch() pobiera te bajty z  
bufora klawiatury kolejno jednocześnie czyszcząc bufor. W  
przypadku klawiszy specjalnych pierwszy bajt jest zerowy (NULL,  
'\0', 00h), co jest sprawdzane w programie. A oto tabela kodów  
poszczególnych klawiszy:  
 

Kody klawiszy klawiatury IBM PC.  

________________________________________________________________ 
 
Klawisze           Kody             ASCII (dec)  
________________________________________________________________ 
 
Home                G                71   (00:47h)   '\0', 'G' 
End                 O                79   (00:4Fh)   '\0', 'O' 
PgUp                I                73 
PgDn                Q                81 
Ins                 R                82 
Del                 S                83 
F1                  ;                59 
F2 ... F10        <, ... D           60, ... 68 
Shift + F1          T                84 
 ... 
Shift + F10         ]                93  
Ctrl + F1           ^                94  
 ...  
Ctrl + F10          f                103  
Alt + F1...F10    h, ... q           104, ... 113  
Alt + 1...9       x, ... Ą (?)       120, ... 128  
Alt + 0           Ć (?)              129  
 
Strzałki kursora:  
LeftArrow         K                  75  
RightArrow        M                  77  
UpArrow           H                  72  
DownArrow         P                  80  
 
Ctrl + PgDn       v                  118  
Ctrl + PgUp       Ń (?)              132  
Ctrl + Home       w                  119  
Ctrl + End        u                  117  
________________________________________________________________ 
 
 
Wyprowadzanie znaków na ekran można przeprowadzić szybciej  
posługując się przerywaniem DOS INT 29H. Drukowanie na ekranie w 
 
trybie tekstowym przebiega wtedy szybciej niż robią to  
standardowe funkcje <stdio.h>, <conio.h>, czy <iostream.h>.  
Poniżej prosty przykład praktyczny wykorzystania przerywania  
29H:  

319

background image

 

[P132.CPP]  

 
# include <stdlib.h>  
# include <conio.h>  
# pragma inline  
  
void SpeedBox(int, int, int, int, char);  
  
main()  
{  
  clrscr();  
  for (; !kbhit(); )  
  {  
    int x = rand() % 40;  
    int y = rand() % 12;  
    SpeedBox(x, y, (80 - x), (24 - y), ('€' + x % 50));    
  }  
return 0;                                            
}  
  
void SpeedBox(int x1, int y1, int x2, int y2, char znak)  
{  
  int k;  
    
  for (; y1 < y2; y1++) { gotoxy(x1, y1);  
       for (k = x1; k < x2; k++)   
    {  
      asm MOV AL, znak  
      asm INT 29H  
    }  
  }  
}         
 

Zadanie  

________________________________________________________________ 
1. Opracuj program pozwalający porównać szybkość wyprowadzania  
danych na ekran monitora różnymi technikami (cout, puts(),  
printf(), asm).  
2. Porównaj wielkość plików wynikowych .EXE powstających w  
różnych wariantach z poprzedniego zadania.  
______________________________________________________________ 

LEKCJA 38: O C++, Windows i małym Chińczyku. czyli: KTO 
POWIEDZIAŁ, ŻE PROGRAMOWANIE DLA WINDOWS JEST 
TRUDNE?!!!  

 
Jak świat światem ludzie przekazują sobie sądy, opinie,  
poglądy... W ciągu naszej nowożytnej ery wymyślono już wiele  
opinii, które krążyły przez dziesięcio- i stulecia gwarantując  
jednym komfort psychiczny (- Ja przecież mam swoje zdanie na ten 
 
temat!), innym dając pozory wiedzy (- Tak, ja coś o tym wiem,  
słyszałem, że...). Żywotność takich ćwierćprawd, uproszczeń,  

320

background image

uogólnień, czy wręcz kompletnie bzdurnych mitów była i jest  
zadziwiająca.  
 
Podejmę tu próbę obalenia funkcjonującego powszechnie przesądu,  
że  
 
    - Programowanie dla Windows jest trudne.       (BZDURA!!!)  
 
Aby nie zostać całkowicie posądzonym o herezję, przyznaję na  
wstępie dwa bezsporne fakty.  
Po pierwsze, wielu powszechnie szanowanych ludzi zrobiło wiele,  
by już pierwszymi przykładami (zwykle na co najmniej dwie  
strony) skutecznie odstraszyć adeptów programowania dla Windows. 
 
No bo jak tu nie stracić zapału, gdy program piszący tradycyjne  
"Hello World." w okienku ma 2 - 3 stronice i jeszcze zawiera  
kilkadziesiąt zupełnie nieznanych i niezrozumiałych słów  
(skrótów? szyfrów?).  
Po drugie, wszystko jest trudne, gdy brak odpowiednich narzędzi. 
 
Nawet odkręcenie małej śrubki bywa niezwykle trudne, gdy do  
dyspozycji mamy tylko młotek... Napisanie aplikacji okienkowej  
przy pomocy Turbo Pascal 6, Turbo C, Quick C, czy QBASIC  
rzeczywiście BYŁO nadwyraz trudne. 
 
I tu właśnie dochodzimy do sedna sprawy:  
 
(!!!)       Programowanie dla Windows BYŁO trudne          (!!!) 
 
 

UWAGA!  

Pierwsza typowa aplikacja dla Windows napisana w BORLAND C++ 3/4 
 
może wyglądać np. tak:  
 
#include <iostream.h>  
 
void main()  
{  
   cout <<"Pierwsza Aplikacja dla Windows";  
}  
 
I już!  
Niedowiarek zapyta: - I TAKIE COŚ CHODZI POD Windows???  
TAK!.  
 
W BORLAND C++ 3+ ... 4+ wystarczy dobrać parametry pracy  
kompilatora i zamiast aplikacji DOS-owskiej otrzymamy program  
wyposażony we własne okienko, paski przewijania w okienku,  
klawisze, menu, ikonkę, itp., itd.!   
 

O MAŁYM CHIŃCZYKU, czyli - NAJLEPIEJ ZACZĄĆ OD POCZĄTKU...  

 
Istnieje jedyny sprawdzony sposób rozwiązywania zagadnień  
takiego typu - tzw. METODA MAŁEGO CHIŃCZYKA.  
WSZYSCY DOSKONALE WIEDZĄ, że język chiński jest szalenie trudny. 
 

321

background image

Dlatego też mimo ogromnego wysiłku prawie NIKOMU nie udaje się  
biegle nauczyć chińskiego - z jednym wyjątkiem - wyjątkiem  
małego Chińczyka. Dlaczego? To proste. Mały Chińczyk po prostu o 
 
tym nie wie! I dlatego już po kilku latach doskonale swobodnie  
włada tym bodaj najtrudniejszym językiem świata!  
 
Jeśli zatem komuś udało się przekonać Cię, szanowny Czytelniku,  
że programowanie dla Windows jest trudne, namawiam Cię na  
dokonanie pewnego eksperymentu intelektualnego. Spróbuj  
zapomnieć, że masz już na ten temat jakieś zdanie i wczuj się w  
rolę małego Chińczyka. Co roku udaje się to wielu milionom  
przyszłych ekspertów od wszystkich możliwych języków świata (C++ 
 
jest chyba znacznie łatwiejszy do chińskiego).  
 
BORLAND C++ aby dopomóc programiście w jego ciężkiej pracy  
tworzy (często automatycznie) wiele plików pomocniczych. Krótkie 
 
zestawienie plików pomocniczych zawiera tabela poniżej.  
 

Najważniejsze pliki pomocnicze w kompilatorach Borland/Turbo   C++.  

________________________________________________________________ 
 
Rozszerzenie       Przeznaczenie         Gdzie/Uwagi  
________________________________________________________________ 
 
.C .CPP            Teksty żródłowe     \EXAMPLES     \SOURCE 
                   (ASCII)             (przykłady)  (kod żródł.) 
.H .HPP .CAS       Pliki nagłówkowe    \INCLUDE 
                   (ASCII)  
.PRJ .DPR .IDE     Projekty            \EXAMPLES  \SOURCE  
 
.TAH .TCH .TDH     Help  
.TFH .HLP .HPJ 
.RTF  
 
.DSK .TC .CFG      Konfiguracyjne  
.DSW .BCW  
 
.DEF .RC .RES      Zasoby i definicje  
.RH .ICO .BMP 
 
.BGI .CHR .RTF     Grafika DOS, fonty  
 
.MAK .NMK .GEN     Pliki instruktażowe dla  
MAKEFILE           MAKE.EXE  
 
.ASM .INC .ASI     Do asemblacji (ASCII)  
 
.RSP               Instruktażowy dla TLINK  
 
.LIB .DLL          Biblioteki 
 
.TOK               Lista słów zastrzeżonych    (reserved words)  
                   (ASCII) 
.DRV               Sterowniki (drivery)  
 
.OVL               Nakładki (overlay) 

322

background image

 
.SYM               Plik ze skompilowanymi (Pre - compiled)  
                    plikami nagłówkowymi. 
________________________________________________________________ 
 
 
Świadome i umiejętne wykorzystanie tych plików może znacznie  
ułatwić i przyspieszyć pracę.  
 
Po wprowadzeniu na rynek polskiej wersji Windows 3.1 okienka  
zaczęły coraz częściej pojawiać się w biurach i domach, i  
stanowią coraz częściej naturalne (właśnie tak, jak chiński dla  
Chińczyków) środowisko pracy dla polskich użytkowników PC. Nie  
pozostaje nam nic innego, jak po prostu zauważyć i uznać ten  
fakt.  
 
Po uruchomieniu Borland C++ (2 * klik myszką, lub rozkaz Uruchom 
 
z menu Plik) zobaczymy tradycyjny pulpit (desktop)  
zintegrowanego środowiska IDE - podobny do Turbo Pascala, z  
tradycyjnym układem głównego menu i okien roboczych.  
 
Skoro mamy zająć się tworzeniem aplikacji dla Windows- zaczynamy 
 
od rozwinięcia menu Options i wybieramy z menu rozkaz  
Application... . Rozwinie się okienko dialogowe. Przy pomocy  
klawiszy możemy wybrać sposób generowania aplikacji - dla DOS,  
dla Windows lub tworzenie bibliotek statycznych .LIB, czy też  
dynamicznych .DLL. Wybieramy oczywiście wariant [Windows EXE].  
 

UWAGA!  

________________________________________________________________ 
 
Struktura podkatalogów i wewnętrzna organizacja pakietów 3.0,  
3.1, 4 i 4.5  ZNACZNIE SIĘ RÓŻNI.  
________________________________________________________________ 
 
 
Skoro ustawiliśmy już poprawnie najważniejsze dla nas parametry  
konfiguracyjne - możemy przystąpić do uruchomienia pierwszej  
aplikacji dla Windows.  
 

PIERWSZA APLIKACJA "specjalnie dla Windows".  

 
Tryb postępowania z kompilatorem BORLAND C++ 3.0/3.1 będzie w  
tym przypadku dokładnie taki sam, jak np. z Turbo Pascalem.  
Wszystkich niezbędnych zmian w konfiguracji kompilatora już  
dokonaliśmy. Kompilator "wie" już, że chcemy uzyskać w efekcie  
aplikację dla Windows w postaci programu .EXE. Możemy zatem  
 
* Wydać rozkaz File | New 
 
Pojawi się nowe okienko robocze. Zwróć uwagę, że domyślne  
rozszerzenie jest .CPP, co powoduje domyślne zastosowanie  
kompilatora C++ (a nie kompilatora C - jak w przypadku plików z  
rozszerzeniem .C). Możesz to oczywiście zmienić, jeśli zechcesz, 
 

323

background image

posługując się menu Options | Compiler | C++ options... (Opcje | 
 
Kompilator | Kompilator C albo C++). W tym okienku dialogowym  
masz sekcję:  
 
   Use C++ Compiler:                Zastosuj Kompilator C++ 
                                    (zamiast kompilatora C) 
       (.) CPP extention          - tylko dla rozszerzenia .CPP 
       ( ) C++ always             - zawsze 
 
* Wybierz rozkaz Save as... z menu File  
 
Pojawi się okienko dialogowe "Save File As" (zapis pliku pod  
wybraną nazwą i w wybranym miejscu).   
 
* Do okienka edycyjnego wpisz nazwę pliku i pełną ścieżkę  
  dostępu - np. A:\WIN1.CPP lub C:\C-BELFER\WIN1.CPP 
 
Zmieni się tytuł roboczego okna z NONAME00 na wybraną nazwę  
 
Możemy wpisać tekst pierwszego programu:  
 

[P133.CPP] 

 
#include <iostream.h>  
  
void main()  
{  
  cout << "   Pierwsza Aplikacja  " << " Dla MS Windows ";  
}  
  
Po wpisaniu tekstu dokonujemy kompilacji.  
 
* Wybierz rozkaz Compile to OBJ z menu Compile.  
* Wybierz rozkaz Link lub Make z menu Compile.  
W okienku komunikatów (Messages) powinien pojawić się w trakcie  
konsolidacji komunikat ostrzegawczy:  
 
*Linker Warning: No module definition file specified:  
                 using defaults  
 
Oznacza to: Konsolidator ostrzega, że brak specjalnego  
stowarzyszonego z plikiem .CPP tzw. pliku definicji sposobu  
wykorzystania zasobów Windows - .DEF. Program linkujący  
zastosuje wartości domyślne. 
 
Jeśli w IDE wersji kompilatora przeznaczonej dla środowiska DOS  
spróbujesz uruchomić program WIN1.EXE w tradycyjny sposób -  
rozkazem Run z menu Run - na ekranie pojawi się okienko z  
komunikatem o błędzie (Error message box):  
 
                     Can't run a Windows EXE file  
                           D:\WIN1.EXE  
 
                                [ OK ]  
 
czyli: "Nie mogę uruchomić pliku EXE dla Windows".  
 
Jak już napisałem wcześniej, kompilatory C++ w pakietach 3.0/3.1 

324

background image

 
mają swoje ulubione specjalności:  
 
Borland C++       - jest zorientowany na współpracę z DOS  
Turbo C++         - jest zorientowany na współpracę z Windows  
 
w wersji 3.1:  
 
BCW -              dla Windows  
BC  -              dla DOS 
 
nie oznacza to jednak, że będą kłopoty z pracą naszego programu! 
 
Wyjdź z IDE BC/BCW. 
 
Z poziomu Menedżera Programów możesz uruchomić swój program  
rozkazem Plik | Uruchom. Do okienka musisz oczywiście wpisać  
poprawną ścieżkę do pliku WIN1.EXE (czyli katalog wyjściowy  
kompilatora Borland C++).  
 
*** Wybierz z menu głównego Menedżera Programów (pasek w górnej  
     części ekranu) rozkaz Plik. Rozwinie się menu Plik.  
*** Wybierz z menu Plik rozkaz Uruchom. Pojawi się okienko  
     dialogowe uruchamiania programów. Wpisz pełną ścieżkę  
     dostępu do programu - np.:  
 
                     D:\KATALOG\WIN1.EXE  
 
   i "kliknij" myszką na klawiszu [OK] w okienku.  
 
Na ekranie pojawi się okno naszej aplikacji. Okno jest  
wyposażone w:  
 
- Pasek z tytułem (Caption) - np.: A:\WIN1.EXE ;  
- Klawisz zamykania okna i rozwinięcia standardowego menu (tzw.  
   menu systemowego Windows) - [-] ;  
- Paski przewijania poziomego i pionowego;  
- Klawisze MINIMIZE i MAXIMIZE (zmniejsz do ikonki | powiększ na 
 
  cały ekran) w prawym górnym narożniku okna;  
 
Program znajduje się w wersji .EXE na dyskietce dołączonej do  
książki. Możesz uruchomić go z poziomu Menedżera Plików (Windows 
 
File Manager), Menedżera Programów (Windows Program Manager) lub 
 
z DOS-owskiego wiersza rozkazów (DOS Command Line): 
 
C\>WIN A:\WIN1.EXE[Enter] 
 
Co może nasza pierwsza aplikacja?  
 
- Typową dla Windows techniką drag and drop - pociągnij i upuść  
możesz przy pomocy myszki przesuwać okno naszej pierwszej  
aplikacji po ekranie ("ciągnąc" okno za pasek tytułowy).  
 
- Ciągnąc ramki bądź narożniki możesz zmieniać wymiary okna w  
  sposób dowolny.  
 
- Posługując się paskami przewijania możesz przewijać tekst w  

325

background image

  oknie w pionie i w poziomie.  
 
- Miżesz zredukować okno do ikonki.  
 
- Możesz uruchomić naszą aplikację wielokrotnie i mieć na  
  ekranie kilka okien programu WIN1.EXE.  
 
- Nasza aplikacja wyposażona jest w menu systemowe. Możesz  
  rozwinąć menu i wybrać z menu jeden z kilku rozkazów.  
 
Jeśli nie pisałeś jeszcze programów dla Windows - możesz być  
trochę zaskoczony. Gdzie w naszym programie jest napisane np. -  
co powinno znaleść się w menu??? Odpowiedź jest prosta -  
nigdzie. Podobnie jak programy tworzone dla DOS korzystają w  
niejawny sposób z zasobów systemu - standardowych funkcji DOS,  
standardowych funkcji BIOS, przerywań, itp - tak programy  
tworzone dla Windows mogą w niejawny sposób korzystać z zasobów  
środowiska Windows - standardowego menu, standardowych okien,  
standardowych klawiszy, itp.. Takie zasoby udostępniane przez  
środowisko programom aplikacyjnym nazywają się interfejsem API  
(Application Program Interface). Poznałeś już API DOS'a - jego  
przerywania i funkcje. Interfejs Windows nazywa się "Windows  
API" i to z jego gotowych funkcji właśnie korzystamy.  
 
Uruchom program wielokrotnie (min. 4 razy). Wykonaj 4 - 6 razy  
czynności oznaczone powyżej trzema gwiazdkami *** . Ponieważ nie 
 
zażądaliśmy, by okno programu było zawsze "na wierzchu" (on top) 
 
- po każdym kolejnym uruchomieniu (nie musisz nic robić -  
nastąpi to automatycznie - zadba o to Menedżer Windows)  
poprzednie okno programu zniknie. Jeśli po czwartym (piątym)  
uruchomieniu programu zredukujesz okno Menedżera Programów do  
ikony (np. [-] i "do ikony" z menu systemowego) - okaże się, że  
"pod spodem" stale widać kaskadę okien naszej aplikacji WIN1.EXE 
 
(patrz rys. poniżej). Na rysunkach poniżej przedstawiono kolejne 
 
stadia pracy z naszą PIERWSZĄ APLIKACJĄ.  
 
Aplikacja WIN1.EXE została wyposażona w ikonkę systemową (znane  
Ci okienko). Ikonka jest transparentna (półprzezroczysta) i  
możemy ją także metodą drag and drop przenieść w dowolne miejsce 
 
- np. do roboczego okna naszej aplikacji. Zwróć uwagę także na  
towarzyszący nazwie programu napis "inactive" (nieaktywna).  
Chodzi o to, że program zrobił już wszystko, co miał do  
zrobienia i zakończył się. DOS dołożyłby standardowo funkcję  
zwolnienia pamięci i zakończył program. W Windows niestety  
okienko nie zamknie się samo w sposób standardowy. W Windows,  
jak wiesz, możemy mieć otwarte jednocześnie wiele okien  
programów a aktywne jest (tylko jedno) zawsze to okno, do  
którego przekażemy tzw. focus. Okno to można rozpoznać po  
ciemnym pasku tytułowym. Właśnie z przyjęcia takiego sposobu  
podziału zasobów Windows pomiędzy aplikacje wynika skutek  
praktyczny - okno nie zamknie się automatycznie po zakończeniu  
programu - lecz wyłącznie na wyrażne życzenie użytkownika. API  
Windows zawiera wiele gotowych funkcji (np. CloseWindow() -  
zamknij okno, DestroyWindow() - skasuj okno i in.), z których  
może skorzystać programista pisząc aplikację. Nie jesteśmy więc  

326

background image

całkiem bezradni. 
 
Spróbuj skompilować w opisany wyżej sposób i uruchomić pierwszą  
aplikację w takiej wersji:  
 
#include <stdio.h>  
  
void main()  
{  
  printf("   Pierwsza Aplikacja \n Dla MS Windows ");  
}  
 
Jak łatwo się przekonać, całkowicie klasyczny, w pełni  
nieobiektowy program WIN1.C będzie w Windows działać dokładnie  
tak samo. Nasze aplikacje nie muszą bynajmniej być całkowicie  
obiektowe, chociaż zastosowanie obiektowej techniki  
programowania pozwala zmusić nasz komputer do zdecydowanie  
wydajniejszego działania. 
 

PODSUMUJMY:  

 
* Jeśli korzystamy wyłącznie ze standardowych zasobów środowiska 
 
  Windows, tworzenie aplikacji dla Windows nie musi być wcale  
  trudniejsze od tworzenia aplikacji DOS'owskich.  
* Czy aplikacja ma być przeznaczona dla DOS, czy dla Windows  
  możemy zdecydować "w ostatniej chwili" ustawiając odpowiednio  
  robocze parametry kompilatora C++:  
  Options | Applications... | DOS standard      
    albo  
  Options | Applications... | Windows EXE 
* Aplikacje skompilowane do wersji DOS'owskiej możemy uruchamiać 
 
   wewnątrz kompilatora DOS'owskiego rozkazem Run | Run.  
*  Aplikacje skompilowane (ściślej - skonsolidowane) do wersji  
   okienkowej możemy uruchamiać wewnątrz Windows z poziomu  
   Menedżera Plików bądź Menedżera Programów rozkazem Uruchom z  
   menu Plik.  
* Dodatkowe pliki nagłówkowe .H i biblioteki .LIB .DLL znajdują  
   się w katalogach  
   \BORLANDC\INCLUDE 
   \BORLANDC\OWL\INCLUDE   
   \BORLANDC\LIB 
   \BORLANDC\OWL\LIB  
   Ścieżki dostępu do tych katalogów należy dodać do roboczych  
   katalogów kompilatora w okienku Options | Directories...  
* Aplikacje nie korzystające z funkcji Windows API nie muszą  
   dołączać okienkowych plików nagłówkowych. Jeśli jednak  
   zechcemy zastosować funkcje i dane (stałe, struktury,  
   obiekty, itp.) wchodzące w skład:  
   - Windows API  
   - Windows Stock Objects - obiekty "ze składu Windows"  
   - biblioteki klas Object Windows Library  
   należy dołączyć odpowiedni plik nagłówkowy:  
   #include <windows.h>  
   #include <windowsx.h>  
   #include <owl.h>  
 

327

background image

TYPOWE BŁĘDY I KŁOPOTLIWE SYTUACJE:  

 
* Należy pamiętać o ustawieniu właściwych katalogów roboczych  
   kompilatora Options | Directories...  
* Przy bardziej skomplikowanych aplikacjach może wystąpić  
   potrzeba dobrania innego (zwykle większego) modelu pamięci.  
   Modelem domyślnym jest model Small. Inne parametry pracy  
   kompilatora ustawia się podobnie za pomocą menu Options.  
_______________________________________________________________ 

LEKCJA 39:  KORZYSTAMY ZE STANDARDOWYCH ZASOBÓW 
Windows.  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak korzystać z zasobów  
Windows bez potrzeby wnikania w wiele szczególów technicznych  
interfejsu aplikacji - Windows API.  
________________________________________________________________ 
 
Ponieważ nasze programy mogą korzystać ze standardowych zasobów  
Windows, na początku będziemy posługiwać się okienkami  
standardowymi. Począwszy od aplikacji WIN3.EXE "rozszerzymy  
ofertę" do dwu podstawowych typów:  
 
* Standardowe główne okno programu (Default main window). 
  To takie właśnie okno, jakie dostały nasze pierwsze aplikacje  
  WIN1.EXE. 
* Okienkiem dialogowym (Dialog box), 
  a dokładniej najprostszym rodzajem okienek dialogowych - tzw.  
  okienkami komunikatów - Message Box.  
 
Zastosowanie okienka dialogowego pozwoli nam na wprowadzenie do  
akcji klawiszy (buttons).  
 
________________________________________________________________ 
 

UWAGA:  

Niestety nie wszystkie tradycyjne funkcje typu printf(),  
scanf(), gets() itp. zostały zaimplementowane dla Windows!  
Pisząc własne programy możesz przekonać się o tym dzięki opisowi 
 
funkcji w Help. Funkcję należy odszukać w Help | Index. Oprócz  
przykładu zastosowania znajdziesz tam tabelkę typu:  
 
         DOS    Unix    Windows   ANSI C    C++ Only 
cscanf   Yes                                            
fscanf   Yes    Yes     Yes        Yes                  
scanf    Yes    Yes                Yes                  
sscanf   Yes    Yes     Yes        Yes                  
 
[Yes] oznacza "zaimplementowana". Dlatego właśnie w dalszych  
programach przykładowych dla wersji 3.0 należy np. stosować np.  
makro getchar() zamiast tradycyjnego getch() zaimplementowane  
dla Windows już w wersji BC++ 3.0. 
________________________________________________________________ 
 

328

background image

 
Dla przykładu spróbujmy skompilować i uruchomić w środowisku  
Windows jeden z wcześniejszych programów - tabliczkę mnożenia. 
Zwróć uwagę na dołączony dodatkowy plik WINDOWS.H i nowy typ  
wskaźnika. Zamiast zwykłego  
 
char *p ...  
LPSTR p ...  
 
LPSTR - to Long Pointer to STRing - daleki wskaźnik do łańcucha  
tekstowego. Jest to jeden z "ulubionych" typów Windows. 
 
/* WIN2.CPP: */  
/*               - Tablica dwuwymiarowa  
 - Wskazniki do elementów tablicy           */  
  
#include <windows.h>  
#include <iostream.h>  
#include <stdio.h>  
  
int T[10][10], *pT, i, j, k;  
char spacja = ' ';  
  
LPSTR p1 = "  TABLICZKA MNOZENIA (ineksy)\n";  
LPSTR p2 = "  Inicjujemy i INKREMENTUJEMY wskaznik:\n";  
LPSTR p3 = "... nacisnij cokolwiek (koniec)...";  
  
void main()  
{  
  printf(p1);  
  for (i = 0; i < 10; i++)  
    {  
    for (j = 0; j < 10; j++)  
      { T[i][j] = (i + 1)*(j + 1);  
if (T[i][j] < 10) cout << T[i][j] << spacja << spacja;  
  else  
     cout <<  T[i][j] << spacja;  
      }  
  cout << '\n';  
    }  
  printf(p2);  
  pT = &T[0][0];  
  for (k = 0; k < 10*10; k++)  
    {  
      if (*(pT+k) < 10) cout << *(pT + k) << spacja << spacja;  
      else  
cout << *(pT + k) << spacja;  
  if ((k + 1)%10 == 0) cout << '\n';  
    }  
  printf(p3);  
  getchar();   
}  
  
Wybraliśmy dla aplikacji standardowe główne okno (Main Window),  
ponieważ istnieje potrzeba pionowego przewijania okna w celu  
przejrzenia pełnego wydruku obu tablic.  
 

Dlaczego ten tekst jest nierówny???  

________________________________________________________________ 

329

background image

Niestety, znaki w trybie graficznym Windows nie mają stałej  
szerokości (jak było to w trybie tekstowym DOS). Niektóre  
aplikacje przeniesione ze środowiska DOS będą sprawiać kłopoty. 
________________________________________________________________ 
 

APLIKACJE DWUPOZIOMOWE.  

 
Zastosujemy teraz najprostszy typ okienka dialogowego - okienko  
kamunikatów (Message Box), nasze następne aplikacje mogą być już 
 
nie jedno- a dwupoziomowe. Typowe postępowanie okienkowych  
aplikacji bywa takie:  
 
* program wyświetla w głównym oknie to, co ma do powiedzenia;  
* aby zadawać pytania stosuje okienka dialogowe, bądź okienka  
  komunikatów;  
* funkcja okienkowa (u nas MessageBox()) zwraca do programu  
  decyzję użytkownika;  
* program główny analizuje odpowiedź i podejmuje w głównym oknie 
 
  stosowne działania.  
 
Prześledźmy ewolucję powstającej w taki sposób aplikacji.  
 
STADIUM 1. Tekst w głównym oknie.  
 
Zaczniemy tworzenie naszej aplikacji tak:  
 
/* WINR1.CPP:                                    */  
/* Stadium 1: Dwa okienka w jednym programie      */  
  
# include <stdio.h>  
# include <windows.h>  
  
char *p1 = "Teraz dziala \n funkcja \n MessageBox()";  
char *p2 = "START";  
int wynik;  
  
void main()  
{  
   printf("   Start: Piszemy w glownym oknie \n");  
   printf("   ...nacisnij cosik...");  
   getchar();  
   MessageBox(0, p1, p2, 0);  
   printf("\n\n\n     Hello World dla WINDOWS!");  
   printf("\n\t...dowolny klawisz... ");  
   getchar();  
}  
  
Moglibyśmy zrezygnować z metod typowych dla aplikacji DOSowskich 
 
i zatrzymania (i zapytania) makrem getchar() (odpowiednik  
getch() dla Windows). To działanie możemy z powodzeniem  
powierzyć funkcji okienkowej MessageBox(). Funkcja MessageBox()  
pobiera cztery parametry:  
 
int Message Box(hwndParent, lpszText, lpszTitle, Style)  
 
HWND hwndParent - identyfikator macieżystego okna (głównego okna 

330

background image

 
aplikacji). Ponieważ nie wiemy póki co pod jakim numerem  
(identyfikatorem) Windows zarejestrują naszą aplikację -  
wpisujemy 0  
LPCSTR lpszText - daleki wskaźnik do łańcucha tekstowego  
wewnątrz okienka.  
LPCSTR lpszTitle - daleki wskażnik do łańcucha tekstowego -  
tytułu okienka komunikatu.  
UINT Style - UINT = unsigned int; numer określający zawartość  
okienka.  
int Return Value - identyfikator klawisza, który wybrał  
użytkownik w okienku komunikatu.  
 

UWAGA 

________________________________________________________________ 
Deklaracje wskaźników do tekstów powinny wyglądać tak:  
LPCSTR p1 = "Napis1", p2 = "Tekst2";  
ale C++ może samodzielnie dokonać forsowania typów i zamienić  
typ char* na typ LPCSTR (lub LPSTR).  
________________________________________________________________ 
 
/* WINR2.CPP:                                       */  
/* Stadium 2: Dwa okienka ze zmienną zawarością      */  
 
# include <windows.h>  
# include <stdio.h>  
  
char *p2, *p1 = "Dopisywanie:";  
char napisy[4][20] = { "Borland ", "C++ ", "dla ", "Windows" };  
  
void main()  
{  
   printf("\n\n\n Hello World dla WINDOWS!");  
   printf("\n AUTOR: ...................");  
  
   for( int i = 0; i < 4; i++)  
   {  
     p2 = &napisy[i][0];  
     MessageBox(0, p2, p1, MB_OK);  
     printf("\n %s", napisy[i]);  
   }  
   MessageBox(0, "I to juz \n wszystko...", "KONIEC", MB_OK);  
}  
  
W tym stadium stosujemy:  
 
- główne okno aplikacji  
- dwa okienka komunikatów (Dopisywanie i KONIEC)  
- jeden klawisz - [OK]  
 
Łańcuchy tekstowe przeznaczone do pola tekstowego okienka  
pobierane są z tablicy napisy[4][20] (cztery napisy po max. 20  
znaków) przy pomocy wskaźnika p2. MB_OK to predefiniowana stała  
(Message Box OK - key identifier - identyfikator klawisza [OK]  
dla okienek komunikatów).  
 
/* WINR3.CPP:                                       */  
/* Stadium 3: Dwa okienka sterują pętlą      */  
 

331

background image

# include <windows.h>  
# include <stdio.h>  
  
char *p2, *p1 = "Dopisywanie:";  
char napisy[4][20] = { "Borland ", "C++ ", "dla ", "Windows" };  
  
void main()  
{  
   printf("\n\n\n Hello World dla WINDOWS!");  
   printf("\n AUTOR: ...................");  
  
   for( int i = 0; i < 4; i++)  
   {  
     p2 = &napisy[i][0];  
if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)  
 printf("\n %s", napisy[i]);  
     else  
 printf("\n ...?");  
   }  
   MessageBox(0, "I to juz \n wszystko...", "KONIEC", MB_OK);  
}  
  
 
W tym stadium stosujemy:  
 
- główne okno aplikacji  
- dwa okienka komunikatów (Dopisywanie i KONIEC)  
- dwa klawisze - [OK] i [Anuluj]  (OK/Cancel)  
- jedną ikonę [STOP]  
 
Zwróć uwagę, że tym razem sprawdzamy, który klawisz wybrał  
użytkownik w okienku. Odbywa się to tak:  
 
if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)  
 
IDOK jest predefiniowaną stałą - kodem klawisza [OK] (ang.  
OK-key IDentifier - identyfikator klawisza OK). Identyfikatory  
różnych zasobów Windows są liczbami całkowitymi. Jeśli jesteś  
dociekliwy Czytelniku, możesz sprawdzić - jaki numer ma klawisz  
[OK] rozbudowując tekst aplikacji np. tak:  
 
int Numer;  
 ... 
Numer = MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL);  
printf("\nKlawisz [OK] ma numer: %d", Numer);  
if(Numer == IDOK) ... 
 
Zwróć uwagę na sposób wykorzystania zasobów w funkcji  
MessageBox(). Identyfikatory zasobów, które chcemy umieścić w  
okienku są wpisywane jako ostatni czwarty argument funkcji i  
mogą być sumowane przy pomocy znaku | (ang. ORing), np.:  
 
MessageBox(0,..,.., MB_ICONSTOP | MB_OKCANCEL);  
 
oznacza umieszczenie ikony STOP i klawiszy [OK] i [Anuluj]. Kod  
zwracany przez funkcję może być wykorzystywany we wszelkich  
konstrukcjach warunkowych (switch, case, for, while, if-else,  
itp.).  
 
/* WINR4.CPP:                                             */  

332

background image

/* Stadium 4: Okienka sterują 2 pętlami, przybywa zasobów. */  
 
# include <windows.h>  
# include <stdio.h>  
  
char *p2, *p1 = "Dopisywanie:";  
char *p3 = "I to by bylo na tyle...\n Konczymy ???";  
char *p4 = "UWAGA: KONIEC ?";  
char napisy[5][20] = { "Borland ", "C++ ", "dla ", "Microsoft",  
                        "Windows" };  
  
main()  
{  
   printf("\n\n\n Grafoman dla WINDOWS!");  
   printf("\n AUTOR: (jak wyzej)");  
   puts("_____________________________\n");  
  
   do  
   {  
     for( int i = 0; i < 5; i++)  
       {  
 p2 = &napisy[i][0];  
 if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)  
   printf("\n %s", napisy[i]);  
 else  
   printf("\n ...?");  
}  
    } while  
(MessageBox(0,p3,p4,MB_ICONQUESTION | MB_OKCANCEL)==IDCANCEL);  
  
  return 0;  
}  
  
W tym stadium stosujemy:  
 
- główne okno aplikacji  
- dwa okienka komunikatów (Dopisywanie i KONIEC)  
- dwa klawisze - [OK] i [Anuluj]  (OK/Cancel)  
- dwie ikonki [STOP] i [PYTAJNIK]  
 
Tekst jest przewijany w głównym oknie programu i po zakończeniu  
roboczej części programu i przejściu w stan nieaktywny  
(inactive) możesz przy pomocy paska przewijania pionowego  
obejrzeć napisy - historię Twoich zmagań z programem. Zwróć  
uwagę, że pojemność głównego okna jest ograniczona. Jeśli  
napisów będzie zbyt dużo, tekst przewinięty poza okno może  
ulegać obcięciu (ang clip on). Zwróć również uwagę na  
naprzemienne przekazywanie aktywności (focus) pomiędzy oknami  
aplikacji:  
 
   MainWindow  <----->  MessageBox  
 
Warto w tym momencie zwrócić uwagę na kilka typowych dla  
okienkowych aplikacji mechanizmów.  
 
* Jeśli naciśniemy klawisz na klawiaturze, bądź klawisz myszki,  
obsługa takiego zdarzenia może następować na dwa sposoby.  
Najpierw Windows pobierają kod klawisza i dokonują  
"kolejkowania" (podobnie jak DOS-owski bufor klawiatury).  
Następnie przekazują kod klawisza aplikacji do obsługi. Jeśli  

333

background image

aplikacja czeka na klawisz i potrafi obsłużyć takie zdarzenie  
(np. funkcja MessageBox(), bądź makro getchar(), czy operator  
cin >> w programie głównym), obsługa zdarzenia zostaje  
zakończona. Jeśli aplikacja nie potrafi obsłużyć zdarzenia -  
obsługa przekazywaba jest stadardowym funkcjom obsługi (Event  
Handler) Windows. 
* Kiedy na ekranie pojawia się okienko dialogowe (tu:  
komunikatów) zostaje mu przekazany tzw. focus - czyli aktywność. 
 
Naciśnięcie [Entera] spowoduje zadziałanie tego klawisza w  
okienku, który właśnie ten focus otrzymał (tu zwykle pierwszego  
z lewej).  
* jeśli naciśniemy klawisz specjalny, którego obsługę w sposób  
standardowy powinny załatwiać Windows - obsługa takiego  
zdarzenia zostaje przekazana domyślnej funkcji Windows (ang.  
Default Event Handler). Tak jest w przypadku klawiszy ze  
strzałkami (przewijanie w oknie), [Tab], [Alt]+[F4], itp.  
 
/* WINR5.CPP:                                             */  
/* Stadium 5: Zmiana wielkości i nazwy okienka.            */  
  
# include <windows.h>  
# include <iostream.h>  
# include <string.h>  
  
char tytul[80] = "Dopisywanie:  ";  
char *p0, *p2;  
char *p1 = "UWAGA: Ponawianie proby \n oznacza: WYDRUKUJE I  
ZAPYTAM";  
char *p3 = "I to by bylo na tyle...\n Konczymy ???";  
char *p4 = "UWAGA: KONIEC ?";  
char napisy[5][20] = { "Borland ", "C++ ", "dla ", "Microsoft",  
                        "Windows" };  
  
main()  
{  
   cout << "\n\n\n Grafoman dla WINDOWS!";  
   cout << "\n AUTOR: (jak wyzej)";  
   cout << "\n_____________________________\n";  
  
   p0 = &tytul[0];  
   do  
   {  
     for( int i = 0; i < 5; i++)  
      {  
       p2 = &napisy[i][0];  
       strcat(p0, p2);  
       int decyzja = MessageBox(0, p1, p0, MB_ICONHAND |  
                                     MB_ABORTRETRYIGNORE);  
 if (decyzja == IDABORT) break;  
 else  
 if (decyzja == IDRETRY)  
    {  
    cout << "\n " << napisy[i];  
    i--;  
    }  
 else  
 if (decyzja == IDIGNORE)  
   {  
     cout << "\n ...?";  

334

background image

     continue;  
   }  
}  
    } while  
      (MessageBox(0, p3, p4, MB_ICONQUESTION | MB_OKCANCEL) ==  
       IDCANCEL);  
   return 0;  
}  
  
W Stadium 5 zmienia się (rośnie) nagłówek okienka komunikatów.  
 
UWAGA: Po wyjściu za ekran nastąpi załamanie programu. Program  
        nie zawiera handlera obsługującego przekroczenia  
        dopuszczalnej długości.  
 
Rysunek poniżej przedstawia różne stadia działania opisanych  
powyżej aplikacji.  
 
Jeśli postanowisz napisać praktyczną aplikację dla Windows, jest 
 
to zwykle program znacznie dłuższy, w którym trzeba przemyśleć  
sposób organizacji pętli, wyrażeń warunkowych i sposoby  
wykorzystania zasobów.  
 

UWAGA  

________________________________________________________________ 
Okienka mogą być "modalne" i "nie-modlane". Okienko "modalne" to 
 
takie okienko, które do momentu jego zamknięcia uniemożliwia  
użytkownikowi działania w innych oknach (tej samej aplikacji,  
bądź innych aplikacji) znajdujących się na ekranie. W ramach  
parametru Styl możesz stosować predefiniowane stałe  
MB_APPMODAL  
MB_TASKMODAL  
itp.  
określające stopień "modalności" okienka (na poziomie zadania -  
TASK, aplikacji - APP, itp.).  
_______________________________________________________________ 

LEKCJA 40: STRUKTURA PROGRAMU PROCEDURALNO – 
ZDARZENIOWEGO PRZEZNACZONEGO DLA WINDOWS.  

________________________________________________________________ 
W trakcie tej lekcji poznasz ogólną budowę interfejsu API  
Windows i dowiesz się, co z tego wynika dla nas - autorów  
programów przeznaczonych dla Windows.  
________________________________________________________________ 
 
W przeciwieństwie do długich liniowych programów przeznaczonych  
dla DOS, w naszych programach dla Windows będziemy pisać coś  
na kształt krótkich odcinków programu i przekazywać sterowanie  
Windows. Jest to bardzo ważna cecha - kod programu jest zwykle  
silnie związany z Windows w taki sposób, że użytkownik może w  
dużym stopniu decydować o sposobie (kolejności) wykonywania  
programu. Praktycznie robi to poprzez wybór opcji-klawiszy w  
dowolnej kolejności. Przy takiej filozofii w dowolnym momencie  
powinniśmy mieć możliwość przełączenia się do innego programu  
(innego okna) i nasz program powinien (bez zauważalnej zwłoki)  
przekazać sterowanie, nie zagarniając i nie marnując czasu CPU.  

335

background image

Z tego powodu kod programu powinien być bardzo  
"zmodularyzowany". Każda sekcja kodu powinna być odseparowana i  
każda, po wykonaniu powinna przekazywać sterowanie do Windows.  
 

NOTACJA WĘGIERSKA I NOWE TYPY DANYCH.  

 
Tworzenie zdarzeniowych programów dla Windows wymaga kilku  
wstępnych uwag na temat nowych typów danych. Okienkowe typy są  
definiowane w plikach nagłówkowych (WINDOWS.H, WINDOWSX.H, OWL.H 
 
itp) i mają postać najczęściej struktury, bądź klasy. Typowe  
sposoby deklaracji w programach okienkowych są następujące:  
 
HWND hWnd  - WiNDow Handle - identyfikator okna  
HWND hWnd  - typ (predefiniowany), hWnd - zmienna   
HINSTANCE hInstance  - Instance Handle - identyfikator danego  
                       wystąpienia (uruchomienia) programu  
PAINTSTRUCT  - struktura graficzna typu PAINTSTRUCT  
               ps - nasza robocza struktura (zmienna)  
WNDCLASS - struktura (a nie klasa wbrew mylącej nazwie) 
POINT - struktura (współrzędne punktu - piksela na ekranie) 
RECT  - struktura (współrzędne prostokąta) 
BOOL  - typ signed int wykorzystywany jako flaga (TRUE/FALSE)  
WORD -  unsigned int  
DWORD - unsigned long int  
LONG - long int  
HANDLE, HWND, HINSTANCE - unsigned int (jako nr - identyfikator) 
 
UINT - j. w. - unsigned int.  
 
W programach okienkowych stosuje się wiele predefiniowanych  
stałych, których znaczenie sugeruje przedrostek i nazwa, np:  
 
WM_CREATE - Windows Message: Create! - Komunikat Windows:  
              Utworzyć! (np. okno) 
WS_VISIBLE - Window Style: Visible - Styl Okna: Widoczne  
ID_...     - IDentifier - IDentyfikator  
MB_...     - Message Box - elementy okienka komunikatów  
 
W środowisku Windows stosuje się specjalną notację nazwaną od  
narodowości swojego wynalazcy Karoja Szimoni - notacją  
węgierską. Sens notacji węgierskiej polega na dodaniu do nazwy  
zmiennej określonych liter jako przedrostka (prefix).  
Litery-przedrostki stosowane w notacji węgierskiej zebrano w  
Tabeli poniżej. Pomiędzy nazewnictwem Microsofta a Borlanda  
istnieją wprawdzie drobne rozbieżności, ale ogólne zasady można  
odnieść zarówno do BORLAND C++ 3+...4+, jak i Microsoft C++  
6...7, czy Visual C++. 
 
Notacja węgierska  
________________________________________________________________ 
Prefix     Skrót ang.               Znaczenie  
________________________________________________________________ 
 
a          array                    tablica  
b          bool                     zmienna logiczna (0 lub 1) 
by         unsigned char            znak (bajt)  
c          char                     znak  
cb         count of bytes           liczba bajtów  

336

background image

cr         color reference value    określenie koloru  
cx, cy     short (count x, y len.)  x-ilość, y-długość (short) 
dw         unsigned long            liczba długa bez znaku 
           double word              podwójne słowo  
fn         function                 funkcja  
pfn        pointer to function      wsk. do funkcji 
h          handle                   "uchwyt" - identyfikator 
i          integer                  całkowity  
id         identifier               identyfikator 
n          short or int             krótki lub całkowity  
np         near pointer             wskaźnik bliski  
p          pointer                  wskaźnik  
l          long                     długi  
lp         long pointer             wskaźnik typu long int  
lpfn       l. p. to function        daleki wskaźn. do funkcji 
s          string                   łańcuch znaków  
sz         string terminated '\0'   łańcuch ASCIIZ  
tm         text metric              miara tekstowa  
w          unsigned int (word)      słowo  
x,y        short x,y coordinate     współrzędne x,y (typ: short) 
________________________________________________________________ 
 

O PROGRAMOWANIU PROCEDURALNO - ZDARZENIOWYM DLA WINDOWS.  

 
W proceduralno-sekwencyjnych programach DOS'owskich sterowanie  
jest przekazywane mniej lub bardziej kolejno kolejnym  
instrukcjom w taki sposób, jak życzył sobie tego programista. W  
Windows program-aplikacja prezentuje użytkownikowi wszystkie  
dostępne opcje w formie widocznych na ekranie obiektów (visual  
objects) do wyboru przez użytkownika. Program funkcjonuje zatem  
według zupełnie innej koncepcji nazywanej "programowaniem  
zdarzeniowym" (ang. event-driven programming). Można powiedzieć, 
 
że za przebieg wykonania programu nie jest odpowiedzialny tylko  
programista lecz część tej odpowiedzialności przejmuje  
użytkownik i to on decyduje w jaki sposób przebiega wykonanie  
programu. Użytkownik może wybrać w dowolnym momencie dowolną  
spośród wszystkich oferowanych mu opcji a program powinien  
zawsze zareagować poprawnie i równie szybko. Jest oczywiste, że  
pisząc program nie możemy przewidzieć w jakiej kolejności  
użytkownik będzie wybierał opcje/rozkazy z menu. Przeciwnie  
powiniśmy napisać program w taki sposób by dla każdego rozkazu  
istniał oddzielny kod. Jest to ogólna koncepcja, na której  
opiera się programowanie zdarzeniowe.  
 
W przeciwieństwie do programów proceduralno - sekwencyjnych,  
które należy czytać od początku do końca, programy dla Windows  
muszą zostać pocięte na na mniejsze fragmenty - sekcje - na  
zasadzie jedna sekcja - obsługa jednego zdarzenia. Jeśli  
zechcesz wyświetlić napis "Hello, World", sekcja zdarzeniowego  
programu obsługująca takie zdarzenie może wyglądać np. tak:  
 
Funkcja_Obsługi_Komunikatów_o_Zdarzeniach(komunikat) 

  switch (komunikat_od_Windows)  
    {  
case WM_CREATE:      
    ...  
    TextOut(0, 0, "Napis: np. Hello world.", dlugosc_tekstu);  

337

background image

    break; 
 
   ...  
case WM_CLOSE:            // CLOSE - zamknąć okno 
     ....  break;  
 ..................... itd.  

 
a w przypadku obiektowego stylu programowania - metoda  
obsługująca to zdarzenie (należąca np. do obiektu  
Obiekt_Główne_Okno - TMainWindow) może wyglądać np. tak:  
 
void TMainWindow::RysujOkno()  
{  
    TString Obiekt_napis = "Hello, World";  
    int dlugosc_tekstu = sizeof(Obiekt_napis); 
    TextOut(DC, 10, 10, Obiekt-napis, dlugosc_tekstu);  
}  
 
Taki fragment kodu programu jest specjalnie przeznaczony do  
obsługi jednego zdarzenia (ewent-ualności). W okienku wykonuje  
się operacja PAINT (maluj). "Malowanie" okna może się odbywać  
albo po raz pierwszy, albo na skutek przesunięcia. Programy  
zdarzeniowe tworzone w C++ dla Windows będą zbiorem podobnych  
"kawałków" następujących w tekście programu sekcja za sekcją.  
Oto jak działa program zdarzeniowy: kod programu, podzielony na  
sekcje obsługujące poszczególne zdarzenia koncentruje się wokół  
interfejsu.  
 

FUNKCJE WinMain() i WindowProc().  

 
W programach pisanych w standardowym C dla Windows używane są  
dwie najważniejsze funkcje: WinMain() i WindowProc().  
 
________________________________________________________________ 
 

UWAGA:  

Funkcji WindowProc() można nadać dowolną nazwę, ale WinMain()  
musi się zawsze nazywać WinMain(). Jest to nazwa zastrzeżona  
podobnie jak main() dla aplikacji DOSowskich. 
________________________________________________________________ 
 
 
Funkcja WinMain() powoduje utworzenie okna programu umożliwiając 
 
zdefiniowanie i zarejestrowanie struktury "okno" (struct  
WNDCLASS) a następnie powoduje wyświetlenie okna na ekranie. Od  
tego momentu zarządzanie przejmuje funkcja WindowProc(). W  
typowej proceduralno - zdarzeniowej aplikacji dla Windows to  
właśnie funkcja WindowProc() obsługuje pobieranie informacji od  
użytkownika (np. naciśnięcie klawisza lub wybór z menu). Funkcja 
 
WindowProc() robi to dzięki otrzymywaniu tzw. komunikatów (ang.  
Windows message).  
 
W Windows zawsze po wystąpieniu jakiegoś zdarzenia (event)  
następuje przesłanie komunikatu (message) o tym zdarzeniu do  

338

background image

bieżącego aktywnego w danym momencie programu w celu  
poinformowania go, co się stało. Jeśli został naciśnięty  
klawisz, komunikat o tym zdarzeniu zostanie przesłany do funkcji 
 
WindowProc(). Tak funkcjonuje interfejs pomiędzy aplikacją a  
Windows. W programach tworzonych w C prototyp funkcji  
WindowProc() wygląda następująco:  
 
LONG FAR PASCAL WindowProc(HWND hWnd,  WORD Message, 
                           WORD wParam,  LONG lParam);  
 
Słowa FAR i PASCAL oznaczają, że:  
 
FAR - kod funkcji znajduje się w innym segmencie niż kod  
        programu;  
PASCAL - kolejność odkładania argumentów na stos - odwrotna (jak 
 
         w Pascalu).  
 
________________________________________________________________ 
 

UWAGA:  

Prototyp funkcji może zostać podany również tak:  
 
LONG FAR PASCAL WndProc(HWND, unsigned, WORD, LONG);  
________________________________________________________________ 
 
 
Pierwszy parametr hWnd jest to tzw. identyfikator okna (ang.  
window handle). Ten parametr zawiera informację, dla którego  
okna przeznaczony jest komunikat. Zastosowanie takiego  
identyfikatora jest celowe, ponieważ funkcje typu WindowProc()  
mogą obsługiwać przesyłanie komunikatów do wielu okien. Jeśli  
okien jest wiele, okno jest identyfikowane przy pomocy tego  
właśnie identyfikatora (numeru).  
 
Następny parametr to sam komunikat o długości jednego słowa  
(word). Ten parametr przechowuje wartość z zakresu  
zdefiniowanego w pliku nagłówkowym WINDOWS.H. W zależności od  
tego co się zdarzyło, Windows mogą nam przekazać ok. 150 różnych 
 
komunikatów a w tym np.:  
 
WM_CREATE      Utworzono okno  
WM_KEYDOWN     Naciśnięto klawisz  
WM_SIZE        Zostały zmienione wymiary okna  
WM_MOVE        Okno zostało przesunięte  
WM_PAINT       Okno należy narysować (powtórnie) - (re)draw  
WM_QUIT        Koniec pracy aplikacji 
        itp.  
 
Przedrostek WM_ to skrót od Windows Message - komunikat Windows. 
 
Wymiana komunikatów w środowisku Windows może przebiegać w różny 
 
sposób - zależnie od źródła wywołującego generację komunikatu i  
od charakteru zdarzenia. Ze względu na źródło można komuniakty  
umownie podzielić na następujące grupy:  
 

339

background image

1. Działanie użytkownika (np. naciśnięcie klawisza) powoduje  
wygenerowanie komunikatu.  
2. Program - aplikacja wywołuje funkcję Windows i powoduje  
przesłanie komunikatu do aplikacji.  
3. Środowisko Windows przesyła komunikat do programu.  
4. Dwie aplikacje związane mechanizmem dynamicznej wymiany  
danych (Dinamic Data Exchange - DDE) wymieniają komunikaty.  
 
Komunikaty Windows można także podzielić umownie na następujące  
kategorie:  
 
1. Komunikaty dotyczące zarządzania oknami (Windows Managenent  
Msg.):  
WM_ACTIVATE (zaktywizuj lub zdezaktywizuj okno), WM_PAINT,  
WM_MOVE, WM_SIZE, WM_CLOSE, WM_QUIT.  
 
Bardzo istotnym szczegółem technicznym jest problem  
przekazywania aktywności pomiędzy oknami. Szczególnie często  
występuje potrzeba przekazania aktywności do elementu  
sterującego. Jeśli hEditWnd będzie identyfikatorem (window  
handle) okienka edycyjnego: 
 
     case WM_SETFOCUS:   
              SetFocus(hEditWnd);   
      break;   
 
funkcja SetFocus() spowoduje, że wszystkie komunikaty dotyczące  
zdarzeń klawiatury będą kierowane do okna sterującego, jeżeli  
okno macieżyste jest aktywne. Ponieważ zmiana rozmiaru okna  
głównego nie pociąga za sobą automatycznej zmiany rozmiaru okna  
sterującego, potrzebna jest dodatkowo obsługa wiadomości   
WM_SIZE wobec okna elementu sterującego.  
 
2. Komunikaty inicjacyjne dotyczące konstrukcji np. menu  
aplikacji:  
WM_INITMENU - zainicjuj menu (wysyłany przed zainicjowaniem),  
WM_INITDIALOG - zainicjuj okienko dialogowe.  
 
3. Komunikaty generowane przez Windows w odpowiedzi na wybór  
rozkazu z menu, zegar, bądź naciśnięcie klawisza:  
WM_COMMAND - wybrano rozkaz z menu,  
WM_KEYDOWN - naciśnięto klawisz,  
WM_MOUSEMOVE - przesunięto myszkę,  
WM_TIMER - czas minął.  
 
4. Komunikaty systemowe. Aplikacja nie musi odpowiadać na  
rozkazy obsługiwane przez domyślną procedurę Windows -  
DefWindowProc(). Szczególnie dotyczy to rozkazów nie odnoszących 
 
się do roboczego obszaru okna - Non Client Area Messages. 
 
5. Komunikaty schowka (Clipborad Messages).  
 
Sens działania funkcji WindowProc() w C/C++ polega na  
przeprowadzeniu analizy, co się stało i podjęciu stosownej  
akcji. Można to realizować przy pomocy drabinki if-else-if, ale  
najwygodniejsze jest stosowanie instrukcji switch. 
 
LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message, 
                           WORD wParam, LONG lParam)  

340

background image

{  
switch (Message)  
    {  
case WM_CREATE:  
      .....  
    break;            /* Koniec obsługi komunikatu WM_CREATE */ 
case WM_MOVE:  
      ....            /*  Kod obsługi komunikatu WM_MOVE     */ 
    break;            /*  Koniec obsługi WM_MOVE.            */ 
case WM_SIZE:  
      ....            /*  Kod obsługi sytuacji WM_SIZE       */ 
    break;            /*  Koniec obsługi WM_SIZE             */ 
 
     ..........       /*  Inne, pozostałe możliwe sytuacje   */ 
 
case WM_CLOSE:        /*  Zamknięcie okna                    */ 
     ....  
   break;  
default:             /* wariant domyślny: standardowa obsługa  
        ....            przez standardową funkcję Windows */ 
    }  
}  
________________________________________________________________ 

UWAGA: 

Ponieważ komunikatów "interesujących" daną aplikację może być  
ponad 100 a sposobów reakcji użytkownika jeszcze więcej, w  
"poważnych" aplikacjach tworzone są często struktury decyzyjne o 
 
większym stopniu złożoności. Jeśli istnieje potrzeba  
optymalizacji działania programów stosuje się struktury dwu  
typów:  
* hierarchia wartości (Value Tree) i  
* drzewo analizy zdarzeń (Event Tree). 
Utworzone w taki sposób tzw. "Drzewo decyzyjne" nazywane także  
"Drzewem analizy zdarzeń" może być wielopoziomowe. Widoczny  
powyżej pierwszy poziom drzewa (pierwszy przesiew) realizowany  
jest zwykle przy pomocy instrukcji switch a następne przy pomocy 
 
drabinek typu if-else-if-break. Schemat if-else-if-break często  
bywa zastępowany okienkami dialogowymi. 
________________________________________________________________ 
 
 
Parametry wParam i lParam przechowują parametry istotne dla  
danego komunikatu. wParam ma długość pojedynczego słowa (word) a 
 
lParam ma długość podwójnego słowa (long). Jeśli, dla przykładu, 
 
okno zostało przesunięte, te parametry zawierają nowe  
współrzędne okna.  
 
Jeżeli program ma być programem zdarzeniowym, powinniśmy przed  
podjęciem jakiejkolwiek akcji zaczekać aż Windows przyślą nam  
komunikat o tym, jakie zdarzenie nastąpiło. Wewnątrz Windows  
tworzona jest dla komunikatów kolejka (ang message queue).  
Dzięki istnieniu kolejkowania otrzymujemy komunikaty pobierane z 
 
kolejki pojedynczo. Jeśli użytkownik przesunie okno a następnie  
przyciśnie klawisz, to Windows wywołają funkcję WindowProc()  

341

background image

najpierw z parametrem WM_MOVE a następnie z parametrem  
WM_KEYDOWN.  
 
Jednym z najważniejszych zadań funkcji WinMain() jest utworzenie 
 
kolejki dla komunikatów i poinformowanie Windows, że komunikaty  
do naszego programu należy kierować pod adresem funkcji  
WindowProc(). W tym celu stosuje się daleki wskaźnik do  
procedury okienkowej lpfn (Long Pointer to Function). Poza tym  
funkcja WinMain() tworzy okno (okna) i wyświetla je na ekranie w 
 
pozycji początkowej. Kiedy program zostaje po raz pierwszy  
załadowany i uruchomiony - Windows najpierw wywołują funkcję  
WinMain().  
 
Windows manipulują komunikatami posługując się strukturą MSG (od 
 
messages - komunikaty). Struktura MSG jest zdefiniowana w pliku  
WINDOWS.H w następujący sposób:  
 
typedef struct tagMSG  
{  
    HWND        hwnd;  
    WORD        message;  
    WORD        wParam;  
    LONG        lParam;  
    DWORD       time;  
    POINT       pt;  
} MSG;  
 
Na pierwszym polu tej struktury znajduje się "identyfikator"  
(kod) okna, dla którego przeznaczony jest komunikat (każdy  
komunikat może być przesłany tylko do jednego okna). Na drugim  
polu struktury przechowywany jest sam komunikat. Komunikat jest  
zakodowany przy pomocy predefiniowanych stałych w rodzaju  
WM_SIZE, WM_PAINT czy WM_MOUSEMOVE. Kolejne dwa pola służą do  
przechowania danych-parametrów towarzyszących każdemu  
komunikatowi: wParam i lParam. Na następnym polu przechowywany  
jest w zakodowanej postaci czas - moment, w którym wystąpiło  
zdarzenie. Na polu pt przechowywane są współrzędne kursora  
myszki na ekranie w momencie w którym został wygenerowany  
komunikat o wystąpieniu zdarzenia. Należy zwrócić tu uwagę, że  
typ POINT oznacza strukturę. Struktura POINT (punkt) w Windows  
wygląda tak:  
 
typedef struct tagPOINT  
{  
    int x;  
    int y;  
} POINT;  
 
Aby mieć pewność, że otrzymaliśmy wszystkie komunikaty, które  
zostały do nas skierowane, w programie wykonywana jest pętla  
pobierania komunikatów (message loop) wewnątrz funkcji  
WinMain(). Na początek wywoływana jest zwykle okienkowa (czyli  
należącą do Windows API) funkcja GetMessage(). Ta funkcja  
wypełnia strukturę komunikatów i zwraca wartość. Zwracana przez  
funkcję wartość jest różna od zera, jeżeli otrzymany właśnie  
komunikat był czymkolwiek za wyjątkiem WM_QUIT. Komunikat  
WM_QUIT jest komunikatem kończącym pracę każdej aplikacji dla  

342

background image

Windows. Jeśli otrzymamy komunikat WM_QUIT powinniśmy przerwać  
pętlę pobierania komunikatów i zakończyć pracę funkcji  
WinMain(). Taka sytuacja oznacza, że więcej komunikatów nie  
będzie. Po uwzględnieniu tych warunków pętla może wyglądać tak:  
 
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \  
                   LPSTR lpszCmdLine, int nCmdShow) 
....  
while(GetMessage(&msg,NULL,0,0))   //Poki nie otrzymamy WM_QUIT  
    {  
      .... 
    }  
 
Po naciśnięciu przez użytkownika klawisza generowany jest  
komunikat WM_KEYDOWN. Jednakże z faktu otrzymania komunikatu  
WM_KEYDOWN nie wynika, który klawisz został przyciśnięty, czy  
była to duża, czy mała litera. Funkcję TranslateMessage()  
(PrzetłumaczKomunikat) stosuje się do przetłumaczenia komunikatu 
 
WM_KEYDOWN na komunikat WM_CHAR. Komunikat WM_CHAR przekazuje  
przy pomocy parametru wParam kod ASCII naciśniętego klawisza.  
Funkcję TranslateMessage() stosujemy w pętli pobierania  
komunikatów tak:  
 
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \  
                   LPSTR lpszCmdLine, int nCmdShow)  
....  
while(GetMessage(&msg, 0, 0, 0))  
    {  
     TranslateMessage(&msg);  
.... 
    }  
 
W tym stadium program jest gotów do przesłania komunikatu do  
funkcji - procedury okienkowej WindowProc(). Posłużymy się w tym 
 
celu funkcją DispatchMessage() (ang. dispatch - odpraw, przekaż, 
 
DispatchMessage = OtprawKomunikat). Funkcja WinMain()  
poinformowała wcześniej Windows, że odprawiane komunikaty  
powinny trafić właśnie do WindowProc().  
 
while(GetMessage(&msg, NULL, NULL, NULL))  
    {  
     TranslateMessage(&msg);  
     DispatchMessage(&msg); 
    }  
 
Tak funkcjonuje pętla pobierająca komunikaty od Windows i  
przekazująca je funkcji WindowProc(). Pętla działa do momentu  
pobrania komunikatu WM_QUIT (Koniec!). Otrzymanie komunikatu  
WM_QUIT powoduje przerwanie pętli i zakończenie pracy programu.  
Komunikaty systemowe (system messages), które są kierowane do  
Windows także trafiają do tej pętli i są przekazywane do  
WindowProc(), ale ich obsługą powinna się zająć specjalna  
funkcja DefWindowProc() - Default Window Procedure, umieszczona  
na końcu (wariant default).  
Jest to standardowa dla aplikacji okienkowych postać pętli  
pobierania komunikatów.  
 

343

background image

Jak widać, wymiana informacji pomiędzy użytkownikiem,  
środowiskiem a aplikacją przebiega tu trochę inaczej niż w DOS.  
Program pracujący w środowisku tekstowym DOS nie musi np.  
rysować własnego okna.  
 

Zadania  

________________________________________________________________ 
1. Uruchom Windows i popatrz świadomym, fachowym okiem, jak  
przebiega przekazywanie aktywności (focus) między okienkami  
aplikacji.  
_______________________________________________________________ 

LEKCJA 41: JAK TWORZY SIĘ APLIKACJĘ DLA Windows?  

________________________________________________________________ 
W trakcie tej lekcji dowiesz się, jak "poskładać" aplikację dla  
Windows z podstawowych funkcji interfejsu API i jakie komunikaty 
są najważniejsze dla naszych aplikacji. 
________________________________________________________________ 
 
Przy tworzeniu programu zwróćmy szczególną uwagę na to, co  
dzieje się w programie po otrzymaniu komunikatu WM_PAINT (należy 
narysować okno). Jest to żądanie ze strony Windows, by program  
narysował obszar roboczy (client area) swojego okna. Program  
otrzyma komunikat WM_PAINT zawsze na początku, kiedy powinien  
narysować swoje okno po raz pierwszy i później powtórnie, za  
każdym razem, gdy trzeba będzie odtworzyć okno na ekranie. Jeśli 
inne okno przesuwane po ekranie przysłoni okno naszego programu, 
po odsłonięciu naszego okna Windows prześlą do programu  
komunikat WM_PAINT - odtwórz swoje okno - narysuj go powtórnie  
(redraw, repaint). Jeśli zechcemy wyprowadzić na ekran napis  
"Hello World" także będziemy musieli narysować okno od nowa. Nie 
zawsze "odświeżenia" wymaga całe okno. W każdej z sytuacji:  
 
- całe okno zostało przysłonięte i odsłonięte  
- część okna wymaga odświeżenia  
- okno jest rysowane po raz pierwszy  
 
Windows prześlą do programu ten sam komunikat - WM_PAINT.  
Jeśli odtworzenia wymaga tylko część okna, taka część okna  
nazywa się nieważną-nieaktualną (ang. invalid). W Windows takie  
nieaktualne fragmenty okna zawsze mają kształt prostokątów.  
Wyobraźmy sobie, że jakieś inne okno przesłoniło narożnik okna  
naszego programu. Jeśli użytkownik usunie to przesłaniające  
okno, odsłonięty obszar będzie potraktowany przez Windows jako  
nieaktualny. Windows prześlą do aplikacji komunikat WM_PAINT  
żądający odtworzenia okna. Żądając odtworzenia okna Windows  
powinny nas poinformować która część naszego okna została na  
ekranie "zepsuta". Współrzędne prostokąta na ekranie Windows  
przekażą przy pomocy specjalnej struktury nazywanej strukturą  
rysunku (ang. paint structure - PAINTSTRUCT).  
 
Strukturę rysunku możemy nazwać w programie np.:  
 

344

background image

  PAINSTRUCT ps;  

 
W funkcji WindowProc() obsługa komunikatu WM_PAINT rozpoczyna  
się od wyczyszczenia pól struktury rysunku ps. Struktura  
predefiniowanego typu  PAINTSTRUCT (w WINDOWS.H) zawiera  
informacje o rysunku.  
 
     PAINTSTRUCT ps;        

  switch (Message)  
    {  
case WM_CREATE:  
      .....  break; 
case WM_MOVE:  
      ....  break;  
case WM_SIZE:  
      ....  break; 
 
case WM_PAINT:      /*  Obsługa rysowania okna                */ 
     memset(&ps, 0x00, sizeof(PAINTSTRUCT);  
      ....  
      break;              //Koniec obsługi WM_PAINT 
 
case WM_CLOSE:    
     ....  break;  
 
default:  ..... 
    }  
}  
 
Następnie pola struktury rysunku zostają wypełnione poprzez  
okienkową funkcją BeginPaint() - RozpocznijRysowanie. Zwróć  
uwagę, że do poprawnego działania funkcji potrzebne są  
informacje o tym, które okno trzeba odświeżyć (Windows powinny  
wiedzieć wobec którego okna żądamy informacji o "zepsutym"  
prostokącie) i adres naszej struktury rysunku. Aby przekazać te  
informacje postępujemy tak:  
 
case WM_PAINT:  
     memset(&ps, 0x00, sizeof(PAINTSTRUCT));  
     hDC = BeginPaint(hWnd, &ps);  
....  
 
Teraz funkcja BeginPaint() może wypełnić naszą strukturę rysunku 
 
ps danymi. Pola struktury typu PAINTSTRUCT wyglądają  
następująco:  
 
typedef struct tagPAINTSTRUCT  
{  
     HDC      hdc;  
     BOOL     fErase;  
     RECT     rcPaint;  
     BOOL     fRestore;  
     BYTE     rgbReserved[16];  
} PAINTSTRUCT;  
 
Przy pomocy pola typu RECT (ang. rectangle - prostokąt) Windows  
przekazują do programu współrzędne wymiary (ang. dimensions)  
"zepsutego" na ekranie prostokąta. Typ RECT oznacza następującą  

345

background image

strukturę:  
 
typedef struct tagRECT  
{  
     int left;      //współrzędna lewa - x 
     int top;       //współrzędna górna - y 
     int right;     //współrzędna prawa - x 
     int bottom;    //współrzędna dolna - y 
} RECT;  
 
Górny lewy róg nieaktualnego prostokąta (invalid rectangle) ma  
dwie współrzędne (left, top) a dolny prawy róg prostokąta ma  
współrzędne (right, bottom). Te współrzędne ekranowe mierzone są 
 
w pikselach i są to współrzędne względne - względem lewego  
górnego narożnika okna aplikacji. Lewy górny narożnik okna  
aplikacji ma więc współrzędne (0,0). 
 
Zwróćmy uwagę na wartość zwracaną przez funkcję BeginPaint() -  
zmienną hDC:  
 
case WM_PAINT:  
     memset(&ps, 0x00, sizeof(PAINTSTRUCT));  
     hDC = BeginPaint(hWnd, &ps);  
....  
 
Wszystnie operacje graficzne będą wymagać nie kodu okna hWnd a  
właśnie kodu-identyfikatora kontekstowego hDC.  
 
Na początku pracy programu, gdy okno jest rysowane po raz  
pierwszy, Windows generują komunikat WM_PAINT i cały obszar  
roboczy okna jest uznawany za nieaktualny. Kiedy program otrzyma 
 
ten pierwszy komunikat, możemy wykorzystać to do umieszczenia w  
oknie np. napisu. Jeśli tekst ma rozpoczynać się od lewego  
górnego narożnika okna aplikacji, funkcja TextOut() używana w  
Windows do wykreślania tekstu (w trybie graficznym) powinna  
rozpoczynać wyprowadzanie tekstu od punktu o (pikselowych)  
współrzędnych (0,0).  
 
case WM_PAINT:  
 ... 
     TextOut(hDC, 0, 0, (LPSTR) "Tekst", strlen("Tekst"));  
     EndPaint(hWnd, &ps);  
     break;   
 
Funkcja TextOut() (wyprowadź tekst) pobiera pięć parametrów:  
 
hDC - identyfikator-kod prostokąta, który należy narysować  
x - współrzędna pozioma (w pikselach)  
y - współrzędna pionowa początku naszego napisu  
W tym przypadku współrzędne wynoszą (0,0).  
LPSTR - wskaźnik do łańcucha znaków "Hello world."  
LPSTR = long pointer to string (wskaźnik typu far).  
 
Wskaźnk ten przekazujemy do funkcji poprzez forsowanie typu:  
 ... (LPSTR) "Tekst";  
Zgodnie z definicją typu w pliku WINDOWS.H spowoduje to zamianę  
wskaźnika do łańcucha typu near char* (bliski) na wskaźnik typu  
far (daleki). Ostatni parametr funkcji to długość wyprowadzanego 

346

background image

 
tekstu - tu obliczana przez funkcję strlen().  
 
Prześledźmy etapy powstawania aplikacji.  
 
Funkcja MainWin() rejestruje i tworzy główne okno programu oraz  
inicjuje globalne zmienne i struktury. Funkcja WinMain() zawiera 
 
pętlę pobierania komunikatów. Każdy komunikat przeznaczony dla  
głównego okna (lub ewentualnych nastepnych okien potomnych) jest 
 
pobierany, ewentualnie poddawany translacji i przekazywany do  
funkcji obsługującej dialog z Windows. Przed zakończeniem  
programu funkcja WinMain() kasuje utworzone wcześniej obiekty,  
zwalnia pamięć i pozostałe zasoby. 
 
UWAGA: "Obiekty" nie są tu użyte w sensie stosowanym w OOP.  
           "Obiekt" oznacza tu np. strukturę.  
 
int PASCAL WinMain(HANDLE hInstance, hPrevInstance,  
                   LPSTR lpszCmLine, int nCmdShow)  
{ ... 
 
HANDLE hInstance - identyfikator bieżącego pojawienia się danej  
aplikacji. Ponieważ w Windows program może być uruchamiany  
wielokrotnie, stosuje sie pojecie tzw. "Instancji" - wystąpienia 
 
- uruchomienia programu.  
 
HANDLE hPrevInstance -  identyfikator  poprzedniego  wystąpienia 
 
                        danej aplikacji    
LPSTR lpszCmdLine -     daleki wskaźnik do parametrów wywołania  
                        programu z linii rozkazu               
int nCmdShow -          sposób  początkowego  wyświetlenia  okna 
 
                        (pełne okno, bądź ikona)   
 
Deklaracja struktury typu MSG (Message) do przechowywania  
komunikatów.  
 
MSG       msg;   
 
Nadanie nazwy aplikacji:  
 
strcpy(szAppName, "Nazwa Aplikacji");  
 
Rejestrujemy struktury okien jeśli jest to pierwsze uruchomienie 
 
danej aplikacji i sprawdzamy, czy rejestracja powiodła się: 
 
if(!PrevInstance)  
   {  
    if((int nRc = RegisterClass() ... 
 
Utworzenie głównego okna programu (może się nie udać): 
 
hWndMain = CreateWindow(....);  
if(hWndMain == NULL)  
  {  

347

background image

   MessageBox(0, "Klops", "Koniec", MB_OK);  
   return (FALSE); 
  }  
 
Wyświetlenie głównego okna na ekranie:  
 
ShowWindow(hWndMain, nCmdShow);   
 
Pętla komunikatów wykrywająca komunikat WM_QUIT: 
 
while(GetMessage(&msg, 0, 0, 0))   
  {  
   TranslateMessage(&msg);  
   DispatchMessage(&msg);  
  }  
 

Główna procedura obsługi okna WindowProc(). 

 
Instrukcja switch przełącza do odpowiedniego wariantu działania  
- obsługi odpowiedniego komunikatu. Muszą tu znajdować sie  
procedury obsługi wszystkich interesujacych nas działań  
uzytkownika i ogólnych komunikatow Windows (np. WM_CLOSE). Jeśli 
 
wystąpi taki komunikat, którego obsługa nie została  
przewidziana, obsługa jest przekazywana, do funkcji okienkowej  
DefWindowProc() - obsługę przejmują Windows.  
Komunikaty inicjowane przez użytkownika są rozpatrywane  
zasadniczo jako WM_COMMAND. Rozkaz wybrany z menu lub  
odpowiadająca mu kombinacja klawiszy jest przekazywana przy  
pomocy pierwszego parametru komunikatu - wParam. Kod  
odpowiadający rozkazowi z menu nazywa sie "control menu ID", a  
identyfikator kombinacji klawiszy - "accelerator ID". Procedura  
obsługi komunikatów powinna zawierać  
 
case (WM_COMMAND): ..... break;  
 
Wewnątrz przy pomocy instrukcji switch{...} należałoby  
rozpatrywać kolejne warianty, wykorzystując identyfikator  
wybranego z menu rozkazu - ID. Obsługa komunikatow świadczących  
o wyborze przez użytkownika rozkazu z menu stanowi zwykle główną 
 
roboczą cześć programu.  
 
LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message, WORD wParam, 
 
                           LONG lParam)  
{  
 HMENU        hMenu=0;         /* Identyfikator menu */ 
 HBITMAP      hBitmap=0;       /* Identyfikator mapy bitowej */  
 HDC          hDC;             /* Identyfikator kontekstowy */  
 PAINSTRUCT   ps;              /* Struktura rysunku */  
 int          nRc=0;           /* Zwrot kodu przez funkcje */  
 
switch (message)  
   {  
    case WM_CREATE:  
 
Gdy okno jest tworzone Windows przesyłają jeden raz komunikat  
WM_CREATE do okna. Procedura obsługi nowego okna (new window  

348

background image

procedure) otrzymuje ten komunikat po utworzeniu okna, ale  
jeszcze zanim okno pojawi sie na ekranie. 
 
lParam - Wskaźnik do struktury CREATESTRUCT o postaci:  
 
typedef struct {  
LPSTR      lpCreateParams;  
HANDLE     hInst;  
HANDLE     hMenu;  
HWND       hwndParent;  
int        cy;  
int        cx;  
int        y;  
int        x;  
LONG       style;  
LPSTR      lpszName;  
LPSTR      lpszClass;  
DWORD      dwExStyle;  
} CREATESTRUCT;                                         */ 
 
 

Kod obsługi powiekszania/zmniejszania case WM_SIZE.  

 
wParam  zawiera kod operacji - zmniejsz/powiększ  
lParam  zawiera nową wysokość i szerokość okna  
 
    case WM_PAINT:      
 
Pobranie kontekstowego identyfikatora urządzenia. Funkcja  
BeginPaint() spowoduje w razie potrzeby wysłanie komunikatu  
WM_ERASEBKGND (Erase Background - skasuj tło).  
 
    memset(&ps, 0x00, sizeof(PAINTSTRUCT));  
    hDC = BeginPaint(hWnd, &ps);  
 
Set Background Mode - ustaw rodzaj tła (tu: przezroczyste): 
 
    SetBkMode(hDC, TRANSPARENT);  
 
Aplikacja powinna wykreślić obszar roboczy okna posługując sie  
grafiką GDI i (Graficzny Interfejs Urządzenia - analogia do  
graficznego standardu BGI w środowisku DOS). Struktura ps typu  
PAINSTRUCT zwrócona przez BeginPaint() wskazuje prostokąt do  
zamalowania.  
 

Wypisanie tekstu w głównym oknie aplikacji: 

 
TextOut(hDC, 0, 0, (LPSTR) "Hello, world.", strlen("Hello,  
world."));  
 
Funkcja TextOut() pracuje w trybie graficznym, więc (podobnie  
jak inne funkcje graficzne Windows API) otrzymuje jako argument  
tzw. "kontekst urządzenia" - hDC.  
 
Zamykanie okna: 
 
    case WM_CLOSE:       

349

background image

    DestroyWindow(hWnd);  
    if (hWnd == hWndMain)  
       PostQuitMessage(0);      
 
Jeśli zamknięte zostało główne okno aplikacji, funkcja  
PostQuitMessage() wysyła do Windows komunikat, że aplikacja  
zakończyła działanie i okno aplikacji zostało usunięte. W tym  
stadium stosuje się funkcje PostQuitMessage() i  
PostAppMessage(). Pozostale przypadki są obsługiwane przez  
wariant domyślny - default. Przekazanie komunikatu do obsługi  
przez Windows.  
 
default: 
    return (DefWindowProc(hWnd, Message, wParam, lParam));  
 
Funkcja rejestrująca wszystkie klasy wszystkich okien związanych 
 
z bieżącą  aplikacja (nazwiemy ją roboczo FRegisterClasses()).  
Jesli operacja sie powiodła - funkcja zwraca kod błędu.  
 
int FRegisterClasses(void)  
{  
WNDCLASS wndclass;  /* Struktura do definiowania klas okien. */  
memset(&wndclass, 0x00, sizeof(WNDCLASS));  
 
Ustawienie parametrów okna w strukturze: 
 
wndclass.style = CS_HRDRAW | CS_VRDRAW;  
wndclass.lpfnWindowProc = WindowProc;  
 
Dodatkowa pamięć dla klasy Window i obiektów klasy Window. 
Dołączanie innych zasobów odbywa się przy pomocy funkcji:  
 
LoadBitmap() - załaduj mapę bitową  
LoadIcon()   - załaduj ikonkę  
LoadCurcor(), LoadMenu(), itp. ... 
 
wndclass.cbClsExtra = 0;  
wndclass.cbWndExtra = 0;  
wndclass.hInstance = hInst;  
wndclass.hIcon = LoadIcon(NULL, ID_ICON);  
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);  
 
Utworzenie pędzla (brush) dla tła: 
 
wndclass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);  
wndclass.lpszMenuName = szAppName;    
wndclass.lpszClassName = szAppName;   
 
if (!RegisterClass(&wndclass)) return -1;  

 
Typowe obiekty ze składu Windows to  
 
HBRUSH Pędzel;   i  
HPEN Ołówek;  
 
Należy tu zwrócić uwagę jeszcze na dwa szczegóły techniczne. DC  
i GDI - Device Context, Graphics Device Interface - to tzw.  
kontekst urządzenia i graficzny interfejs urządzenia. Pozwala to 

350

background image

Windows działać skutecznie w trybie "Device Independent"  
(niezależnym od sprzętu).  

LEKCJA 42: KOMPILATORY "SPECJALNIE DLA Windows". 

________________________________________________________________ 
Z tej lekcji dowiesz się, czym różnią się kompilatory  
przeznaczone dla pracy w środowisku Windows.  
________________________________________________________________ 
W IDE i w sposobie zachowania zaszły istotne zmiany. Posługując  się Turbo C++ z pakietu BORLAND C++ 
3.0 lub BCW z pakietu 3.1  możemy korzystać z uroków i usług Windows szerzej niż do tej  pory. Możemy 
otwierać wiele okien i uruchamiać bezpośrednio z  poziomu IDE okienkowe aplikacje. W głównym menu 
kompilatora  zaszły pewne zmiany (sygnalizujące obiektowo- i okienkowo -  zorientowaną ewolucję pakietów 
Borlanda), na które warto zwrócić uwagę.  Zniknęło menu Debug (co wcale nie oznacza, że nie możemy 
korzystać z Debuggera), pojawiło się natomiast nowe menu Browse  (przeglądanie). Rozkazy, których 
tradycyjnie szukaliśmy w menu  Debug zostały rozrzucone do innych menu. I tak:  

Menu Compile zawiera

:  

Compile           (kompilacja do *.OBJ),  
Make              (kompilacja i konsolidacja do *.EXE),  
Link              (konsolidacja bez powtórnej kompilacji),  
Build all         (konsolidacja wszystkich modułów),  
Information...    (informacja o przebiegu kompilacji),  
Remove messages   (usuwanie komunikatów z pliku wynikowego)  

Menu Run zawiera:  

Run               (uruchomienie i ewentualna rekompilcja),  
Arguments...      (argumenty uruchomieniowe z wiersza rozkazu),  
Debugger          (zamiast w Debug - TU!)  
Debugger arguments...  (argumenty dla Debuggera)  

Menu Project zawiera:   

Open project       - otwórz (nowy lub istniejący) plik projektu, 
 
Close project      - zamknij projekt,  
Add item...        - dodaj element (plik) do projektu,  
Delete item        - usuń element (plik) z projektu,  
Include ˙˙files...  ˙˙- ˙˙podaj ˙katalog ˙zawierający ˙dodatkowe 
 
                    dołączane do programu pliki nagłówkowe *.H  

 W menu Options (zestaw znany już z Borland C++) warto zwrócić  uwagę na pewną dodatkową możliwość. Jak 
wiemy z doświadczenia,  uruchamiając program często dokonujemy zmian i korekt w pliku  żródłowym *.C, czy 
*.CPP. Znacznie rzadziej jednak zmieniamy  zestaw dołączanych do programu plików nagłówkowych *.H. 
Wiemy  również, że kompilacja tych właśnie plików nagłówkowych zajmuje  często lwią część czasu całej 
kompilacji i konsolidacji  programu. Borland zauważył to i w okienku dialogowym:  
 
Options | Compiler | Code generation --> Code Generation Options 
 
 umieścił opcję Pre-compiled headers (pliki nagłówkowe wstępnie  skompilowane wcześniej - i tylko jeden raz). 
Szczególnie w  przypadku aplikacji okienkowych może to znacznie przyspieszyć  proces uruchamiania i 
"szlifowania" naszych programów. Nie ma  jednak nic za darmo. Borland/Turbo C++ po skompilowaniu plików 
nagłówkowych tworzy na dysku roboczy plik *.SYM nadając mu nazwę zgodną z nazwą bieżącego projektu 
(jest to zwykle nazwa głównego modułu *.CPP) i do poprawnego działania wymaga kilkadziesiąt lub nawet 
kilkaset kilobajtów dodatkowej przestrzeni na dysku.  

UWAGA  

Jeśli przenosisz projekt na dyskietkę i tam kontynuujesz pracę  nad projektem, pamiętaj, że może zabraknąć 
miejsca na  prekompilowany plik .SYM.  

351

background image

Czytelnik zechce sam sprawdzić w jakim stopniu przyspieszy to  kompilację naszego własnego programu 
proceduralno -  zdarzeniowego WINPZ1.CPP:  WINZ1.CPP. Jednomodułowa aplikacja proceduralno - 
zdarzeniowa  dla Windows. 

#include <windows.h>  
#pragma argused  
 
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;  
  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpszCmdLine, int nCmdShow )  
{  
  WNDCLASS Okno1;  
  MSG komunikaty;  
  HWND NrOkna;  
  LPSTR     LongPtr1 = "Okno 1";  
  LPSTR     lp2      = "AM: BC++ 3..4/Reczne sterowanie (1)";  
  
    if (hPrevInstance == 0)  
    {  
Okno1.style= CS_HREDRAW | CS_VREDRAW ;  
Okno1.lpfnWndProc= WndProc;  
Okno1.cbClsExtra = 0;  
Okno1.cbWndExtra= 0;  
Okno1.hInstance = hInstance;  
Okno1.hCursor        = LoadCursor(0, IDC_CROSS );  
Okno1.hbrBackground= GetStockObject(WHITE_BRUSH );  
Okno1.lpszMenuName= 0;  
Okno1.lpszClassName= LongPtr1;  
  
    if (!RegisterClass(&Okno1))  
return 0;  
    }  
  
    NrOkna = CreateWindow(LongPtr1, lp2,  WS_VISIBLE |  
                              WS_SYSMENU | 
                WS_MINIMIZEBOX | WS_VSCROLL | WS_MAXIMIZEBOX,  
                CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,  
                0, 0, hInstance, 0);  
  
    ShowWindow(NrOkna, nCmdShow);  
    UpdateWindow(NrOkna);  
  
    while (GetMessage(&komunikaty, 0, 0, 0))  
    {  
TranslateMessage(&komunikaty );  
DispatchMessage(&komunikaty );  
    }  
    return 0;  
}  
  
long FAR PASCAL WndProc (HWND NrOkna, unsigned KomunikatWindows, 
 
 WORD wParam, LONG lParam)  
{  
HDC          NrKontekstu;  
PAINTSTRUCT  struktura_graficzna;  
RECT         prostokat;  
  
    switch(KomunikatWindows)  

352

background image

    {  
      case WM_PAINT:  
   {  
    NrKontekstu = BeginPaint(NrOkna, &struktura_graficzna);  
    GetClientRect(NrOkna, &prostokat);  
    TextOut(NrKontekstu,80,50,  ": Reczne sterowanie:", 20 );  
    TextOut(NrKontekstu, 5,70,  "Tu -->", 6);  
    TextOut(NrKontekstu, 5, 85, "Blad:", 5);  
    TextOut(NrKontekstu,75,70,  "-----------------------------", 
 
40);  
    TextOut(NrKontekstu,30,110, "Programowanie proceduralno -  
zdarzeniowe.", 41 );  
    TextOut(NrKontekstu,30,135, "Szablon moze zostac rozbudowany 
 
o inne funkcje.", 47 );  
    TextOut(NrKontekstu,30,180, "RECZNIE panujemy np. nad:", 25  
);  
    TextOut(NrKontekstu,20,220, "paskiem tytulowym okna, tytulem 
 
ikonki...", 41);  
    TextOut(NrKontekstu, 100, 250, "!KONIEC - [Alt]+[F4]", 20);  
  
    EndPaint(NrOkna,&struktura_graficzna);  
    break;  
    }  
      case WM_DESTROY:  
    {  
    PostQuitMessage(0);  
    break;  
    }  
      default:  
    return DefWindowProc(NrOkna,KomunikatWindows,wParam,lParam); 
 
    }  
    return 0;  
}  
 Program demonstruje opisane wyżej mechanizmy, może być  uruchamiany wielokrotnie i sprowadzony do 
ikony. Z uwagi na brak zdefiniowanych dodatkowych zasobów (brak w projekcie plików:  
.RC - resources - zasoby  
.ICO - ikona  
.DEF - definicji  
.PRJ lub .IDE - projektu  
.DSK - konfiguracyjnego  
itp.)  
 podczas kompilacji programu wystąpią dwa komunikaty ostrzegawcze. Komunikaty te można zignorować. A 
oto druga przykładowa aplikacja w tym samym stylu. Tym razem  funkcja okienkowa reaguje na naciśnięcie 
lewego klawisza myszki, co powoduje wygenerowanie komunikatu WM_LEFTBUTTONDOWN.  

Program WINZ-2.CPP  

#include <windows.h>  
#include <string.h>  
#pragma argused  
 
char napis[10];  
int X, Y;  
  
LONG FAR PASCAL WndProc (HWND, WORD, WORD, LONG);  
  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  

353

background image

   LPSTR lpszCmdLine, int nCmdShow )  
{  
    WNDCLASSwndClass;  
    MSGmsg;  
    HWNDhWnd;  
    LPSTR       Lp1 = "Mysza1";  
    LPSTR       lp2 = "WINPZ2: Wykrywanie Lewego Klawisza  
Myszki";  
  
    if (!hPrevInstance)  
    {  
wndClass.style= CS_HREDRAW | CS_VREDRAW ;  
wndClass.lpfnWndProc= WndProc;  
wndClass.cbClsExtra = 0;  
wndClass.cbWndExtra= 0;  
wndClass.hInstance = hInstance;  
wndClass.hIcon = 0;  
wndClass.hCursor= LoadCursor(0, IDC_ARROW );  
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );  
wndClass.lpszMenuName= 0;  
wndClass.lpszClassName= Lp1;  
  
    if (!RegisterClass(&wndClass))  
exit(1);  
    }  
  
hWnd = CreateWindow(Lp1, lp2, WS_OVERLAPPEDWINDOW,  
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,  
0, 0, hInstance, 0);  
  
    ShowWindow(hWnd, nCmdShow);  
    UpdateWindow(hWnd);  
  
    while (GetMessage(&msg, 0, 0, 0))  
    {  
TranslateMessage(&msg );  
DispatchMessage(&msg );  
    }  
    return 0;  
}  
  
LONG FAR PASCAL WndProc (HWND hWnd, WORD Message,  
 WORD wParam, LONG lParam)  
{  
HDC hDC;  
PAINTSTRUCT ps;  
RECT rect;  
    switch(Message)  
    {  
case WM_SIZE:  
    hDC = GetDC( hWnd );  
    TextOut(hDC, 50, 100, "Wykrywanie nacisniecia", 22);  
    TextOut(hDC, 50, 120, "lewego klawisza myszki.", 23);  
    TextOut(hDC, 20, 140, "Komunikat o zdarzeniu: ", 22);  
    TextOut(hDC, 20, 156, "Left Button Down - LBUTTONDOWN", 31); 
 
    TextOut(hDC, 50, 170, "Po wcisnieciu klawisza,", 23);  
    TextOut(hDC, 50, 190,"w biezacej pozycji kursora, pojawi sie 
 
napis <-- Tu!.", 52);  

354

background image

    ReleaseDC(hWnd, hDC);  
    break;  
  
case WM_PAINT:  
    hDC = BeginPaint(hWnd, &ps);  
    TextOut(hDC, X,Y, napis, strlen(napis));  
    EndPaint(hWnd, &ps);  
    break;  
  
case WM_LBUTTONDOWN:  
    strcpy(napis,"<-- Tu !");  
    X = LOWORD(lParam);  
    Y = HIWORD(lParam);  
    InvalidateRect(hWnd, 0, TRUE);  
    UpdateWindow(hWnd);  
    break;  
  
case WM_DESTROY:  
    PostQuitMessage(0);  break;  
  
default:  
return DefWindowProc(hWnd, Message, wParam, lParam);  
    }  
    return 0;  
}  
 Plik nagłówkowy STRING.H pojawia się ze względu na obecność  funkcji strlen() wyznaczającej długość 
napisu. Zmienne X i Y to  bieżące (względne) współrzędne kursora myszki w momencie  naciśnięcia klawisza. 
Program demonstruje następujące efekty:  
 
    X = LOWORD(lParam);  
 
- przekazanie współrzędnej X przy pomocy parametru lParam  
(LOWORD to LOw WORD of the double word - młodsze słowo podójnego 
 
słowa).  
 
    Y = HIWORD(lParam);  
 Analogicznie - przekazanie współrzędnej Y (HIgh WORD of the  double word). Funkcja InvalidateRect() 
powoduje uznanie  prostokąnego pola za nieaktualne. Funkcja UpdateWindow()  "odświeża" okno. Dzięki temu 
tandemowi napis znika i pojawia się w nowym miejscu. 

PROJEKT.  

Aby skompilować powyższe programy przykładowe należy:  
1. Uruchomić kompilator C++.  
2. Załadować do okienka edycyjnego (File | Open) plik z tekstem  żródłowym programu.  
3. Wybrać rozkaz Compile z menu Compile. Przed kompilacją i konsolidacją (jeśli był inny) ustawić sposób 
tworzenia kodu wynikowego [Windows EXE]. 
 
Kompilacja przebiegnie poprawnie (pamiętaj o Opcjach i  Katalogach), mimo to pojawią się jednak dwa 
komunikaty  ostrzegawcze. W okienku "Compile Status" (stan/przebieg  kompilacji) pojawi się zawartość:  
 
  Lines     3832   (znakomita większość to WINDOWS.H,  
                         prekompilacja byłaby celowa)  
  Warnings:    1  
  Errors:      0  
 
Jeśli wybierzesz klawisz [OK] w okienku "focus" (aktywność)  zostanie przekazana do okienka komunikatów 
"Message" a tam  pojawi się napis:  
 
Warning: Parameter 'lspzCmdLine' is never used.  

355

background image

 
Wskaźnik do parametrów uruchomieniowych programu (Arguments)  pobieranych z wiersza rozkazu nie został 
ani raz użyty w  programie. Na to nic nie możemy poradzić. Po prostu argumenty  uruchomieniowe nie są nam 
potrzebne. Wykonujemy więc "klik"  (przekazanie "focusa") w okienku edycyjnym i możemy przejść do 
następnej czynności:  
 
4. Konsolidacja: Compile | Link.  
 
W okienku "Message" znów pojawi się ostrzeżenie:  
 
Linker Warning: No module definition file specified:  
                using defaults.  
(brak wyspecyfikowanego pliku definicji .DEF; stosuję wartości  domyślne) I tu już możemy coś zaradzić. 
Możemy zatem pokusić się o stworzenie naszego pierwszego pliku definicji (nazwa jest trochę myląca - chodzi o 
zdefiniowanie sposobu wykorzystania zasobów  środowiska Windows).  Aby utworzyć plik .DEF (jest to plik 
ASCII) należy:  
 1. Otworzyć nowe okienko edycyjne (nie wychodząc z IDE): File | New Otworzy się okienko 
NONAMExx.CPP. Ta nazwa nie jest oczywiście  najodpowiedniejsza, więc umieszczamy plik we właściwym 
katalogu  (tym samym, co główny program *.CPP) przy pomocy rozkazu File |  Save as...  i nadajemy plikowi 
stosowną nazwę i rozszerzenie *.DEF. Okieno  pozostaje puste, ma jednak "focus" i nową nazwę, np. 
C:\..\PR.DEF.  
 
3. Redagujemy nasz pierwszy plik definicji, np. tak:  
 
NAME JAKAKOLWIEK             // <-- nazwa aplikacji  
DESCRIPTION 'Opis: A. MAJCZAK, BC C++ 3...4'  
EXETYPE WINDOWS              // <-- EXE dla Windows 
CODE PRELOAD MOVEABLE DISCARDABLE  
DATA PRELOAD MOVEABLE MULTIPLE  
HEAPSIZE 4096                // <-- sterta 4 KB 
STACKSIZE 5120               // <-- stos 5 KB_______________________________________________________ 

UWAGA:  

W przypadku tworzenia bibliotek .DLL dane muszą mieć status  SINGLE (pojedyncze) zamiast MULTIPLE 
(wielokrotne). Użycie tu  słowa MULTIPLE pozwoli nam na wielokrotne uruchamianie aplikacji.  
Możnaby tu zapytać - po co to robić, skoro używamy standardowych wartości i obecność tego pliku nie wnosi 
nic nowego do sposobu  działania naszego programu?  Odpowiedź jest prosta. Mając taki plik będziemy mogli 
prześledzić stadia tworzenia tzw. projektu (w BC++ 4 bez tego  ani rusz). Zapisujemy zatem plik na dysk:  

4. File | Save. (plik .DEF zostaje zapisany na dysku).  
Ponieważ pracujemy w środowisku Windows, okno edycji pliku *.DEF możemy traktować podobnie jak każde 
inne okno. Najwygodniej  zatem przejść do okna edycji głównego pliku żródłowego *.CPP  przy pomocy 
własnego menu systemowego tegoż okna.  
 
5. Menu Systemowe [-] | Zamknij.  
 I możemy przystąpić do tworzenia projektu składającego się z dwu plików: *.CPP i *.DEF. Jeśli, dla przykładu, 
przyjmiemy w tym miejscu, że nasze dwa moduły nazywają się: WINZ2.CPP i WINZ2.DEF i są 
przechowywane w katalogu głównym dysku C:\ , kolejność  czynności powinna być następująca:   
1. Rozwijamy menu Project  ([Alt]+[P] lub myszką). 
2. Wybieramy z menu rozkaz Open Project... (Utwórz projekt). Pojawia się okienko dialogowe Open Project File 
z domyślnym  rozszerzeniem *.PRJ (w BC 4+ - *.IDE).  
3. Do okienka File Name:  wpisujemy nazwę pliku z opisem projektu: np. WINZ2.PRJ. W dolnej części ekranu 
otwiera się okienko Project: WINZ2  
4. Wybieramy z menu Project rozkaz Add item... (dodaj element do projektu). Pojawia się okienko dialogowe 
"Add to Project List" (dodawanie do listy elementów projektu).  
5. Do okienka File Name: wpisujemy nazwę głównego pliku  projektu: WINZ2.CPP (*.cpp jest domyślnym 
rozszerzeniem). Plik możemy wybrać także z listy w okienku Files: .  
6. Wybieramy w okienku dialogowym klawisz [+Add] (dodaj do  projektu).  
7. Wpisujemy nazwę kolejnego pliku wchodzącego w skład projektu  (w tym przypadku WINZ2.DEF).  
8. Wybieramy klawisz [+Add] w okienku.  

356

background image

UWAGA: Czynności 7) i 8) w przypadku bardziej złożonych  projektów będą powtarzane wielokrotnie.  
9. Wybieramy klawisz [Done] w okienku (zrobione/gotowe).  Konfigurowanie projektu zostało zakończone.  
10. Przy pomocy rozkazów Compile, Link, Make, Build all, Run  możemy teraz skompilować, skonsolidować i 
uruchomić nasz program w postaci projektu. Ostrzeżenie Linkera zniknie.  

UWAGA  

W dolnej części ekranu w stadium tworzenia projektów ( i póżniej po załadowaniu pliku projektu [Open Project] 
pojawi się lista  plików. Do trybu edycji pliku możesz przjść poprzez dwukrotne  klinięcie pliku na tej liście. 
Zwróć uwagę, że pliki projektów .PRJ ( w Borland 4+ .IDE)  przechowują również informacje o konfiguracji. 
Najważniejsza z  nich to informacja o katalogach, z których korzysta kompilator:  
 
Options | Directories... | Include  
Options | Directories... | Library  
Options | Directories... | Output  
Najwygodniej przechowywać wszystkie pliki wchodzące w skład  jednego projektu w odrębnym katalogu 
dyskowym. Dla wprawy załóż  odrębny katalog i zapisz tam pliki:  
 
*.CPP  
*.DEF  
*.PRJ          (lub *.IDE)  
 
dla swoich pierwszych dwóch projektów, które właśnie powstały.  
UWAGA 
 Ten sam plik definicji możesz wykorzystywać do tworzenia  następnych przykładowych aplikacji typu Windows 
EXE.  
_______________________________________________________________ 

LEKCJA 43:  Elementy sterujące i zarządzanie programem.  

________________________________________________________________ 
Jak sterować pracą aplikacji. Jak umieszczać elementy  graficzne-sterujące w oknie aplikacji. Najczęściej 
stosowane  funkcje API Windows.  
Elementy sterujące pracą aplikacji w Windows (ang. controls) są  również swoistymi okienkami (tyle, że 
potomnymi - Child Window  wobec głównego okna aplikacji - Parent Window).  Do utworzenia takiego 
specjalnego okna również można użyć  funkcji CreateWindow(). Jeśli okno ma stać się nie głównym oknem 
aplikacji, lecz oknem sterującym przebiegiem programu, funkcja  wymaga podania następujących argumentów:  
 
- rodzaj klasy sterującej (ang. control class)  
- rodzaj elementu sterującego (ang. control style)  

Typowe rodzaje elementów (obiektów) starujących w środowisku  

Windows: 

BUTTON                - klawisz rozkazu, prostokątne okno typu  
                        Child, reprezentujące przycisk, który  
                        użytkownik może włączyć; przycisk może  
                        być opatrzony etykietą (text label).  
COMBOBOX              - okienko dialogowe kombinowane. Jest  
                         złożeniem klasy EDIT i LISTBOX;  
LISTBOX                - oknienko z listą (zwykle element  
                            składowy okienka dialogowego typu  
                            Combo Box. 
STATIC                  - pole statyczne (bez prawa edycji).  
                           Niewielkie okno zawierające tekst lub 
                           grafikę; służy z reguły do oznaczania 
                           innych okien sterujących. 
SCROLLBAR                - pasek przewijania (pionowy - Vertical 
                           Scroll Bar; poziomy - Horizontal  
                           Scroll Bar). 

357

background image

              Style klawiszy sterujących (Button Styles): 
BS_PUSHBUTTON              - Klawisz. Okno sterujące wysyła, po  
                              każdym wyborze klawisza  
                              (kliknięcie), wiadomość do okna  
                              macieżystego (Parent Window). 
BS_RADIOBUTTON              - Okrągły przełącznik działający  
                                zwykle na zasadzie @tylko jeden  
                                z grupy". 
BS_CHECKBOX -                - prostokątny przełącznik [X]  
                                  włączający (aktywna) lub  
                                  wyłączający (nieaktywna)  
                                  opcję. Działa niezależnie od  
                                  pozostałych. 
Inne style określają np. sposób edycji tekstu (ES_LEFT,  
ES_MULTILINE, itp.) Szczegóły - patrz system Help - Windows API. 
Oto przykład utworzenia okna elementu sterującego typu "Klawisz" 
 
(BUTTON to nazwa typu): 
 
hControlWnd = CreateWindow ("BUTTON", " Napis_na_Klawiszu ",  
              BS_PUSHBUTTON |WS_CHILD | WS_VISIBLE,  
              10, 20, 30, 40,   
              hWnd, ID_Elem, hInstance, 0);  
 
Identyfikator ID_Elem jest potrzebny, gdy w jednym oknie znajduje się kilka elementów sterujących - pozwala 
na ich  rozpoznawanie w programie. Sposób przekazywania informacji o  kliknięciu klawisza przypomnę na 
przykładzie okienka  komunikatów:  
 
if(IDOK==MessageBox(0, "", "", MB_OK)) ...  
 
IDOK to predefiniowany w Windows identyfikator klawisza [OK].  
Oto krótkie wyjaśnienie pozostałych elementów:  
 
10, 10, 30, 20,  - współrzędne. x, y, szerokość, wysokość  
hWnd,  - oznacznik okna macieżystego   
 
Przesuwanie i zmiana wielkości elementu sterującego. 
 
Funkcja MoveWindow() przesuwa okno we wskazane miejsce: 
 
MoveWindow(hKlawisz, 10, 10, 20, 30, TRUE);  
 
Ponieważ okno elementu sterującego ma zadane względne  współrzędne w oknie macieżystym, gdy okno 
macierzyste zostanie  przesunięte - element sterujący będzie przesunięty  automatycznie. Również po zmianie 
rozmiarów okna macieżystego  okno elementu sterującego zmienia położenie, zawsze jednakowe  względem 
lewego górnego rogu.  
 
Usuwanie okna sterującego  
 
Okienko elementu sterującego możemy usunąć (jak i każde inne  
okna) przy pomocy funkcji: 
 
DestroyWindow(hKlawisz);  
 
Przekazywanie informacji do- i z- okna elementu sterującego  Zdarzenie w oknie elementu sterującego - np. 
kliknięcie klawisza - powoduje wygenerowanie komunikatu WM_COMMAND. Towarzyszące  komunikatowi 
parametry przenoszą istotne informacje: 
 
wParam          - identyfikator elementu sterującego,  
lParam          - dla wciśniętego klawisza będzie to BN_CLICKED. 

358

background image

 Niektóre komunikaty Windows mogą być kierowane do okna elementu  sterującego i wymuszać pewne 
operacje. Dla przykładu komunikat  WM_GETTEXTLENGTH przesłany do okienka edycyjnego typu Text Edit 
Box (element sterujący klasy EDIT) jest żądaniem podania  długości tekstu wpisanego właśnie do okienka. Aby 
Windows  wygenerowały komunikat i przesłały go do naszego elementu  sterującego - musimy "poprosić" przy 
pomocy funkcji  
SendMessage() (WyślijKomunikat): 
 
DlugTekstu = SendMessage(hEditWnd, WM_GETTEXTLENGHT, 0, 0); 
gdzie:  
hEditWnd jest identyfikatorem elementu - okienka edycyjnego  

Robi na "szaro'?  

Podobnie jak opcje w menu - klawisze także mogą zostać udostępnione (ang. enable), bądź zablokowane (ang. 
disable). Jeśli hKlawisz będzie identyfikatorem elementu sterującego, można go udostępnić (1), bądź 
zablokować (0) przy pomocy  funkcji: 
 
EnableWindow(hKlawisz, 0);   
EnableWindow(hKlawisz, 1);   
Typowy projekt dla środowiska Windows składa się z kilku (czasem kilkunastu) plików: .H, .MNU, .DLG, .RC, 
.DEF, .PRJ, .ICO, .BMP, itp. Kompilator zasobów generuje na podstawie tego "składu" końcowy plik aplikacji.  
 
------------------Plik MEDYT-01.H------------------------------- 
#define szAppName "MEDYT-01"  
#define ID_EDIT 200  
 
------------------Plik główny: MEDYT-01.CPP--------------------- 
 
#include <windows.h>  
#include "EDIT.H" 
#pragma argused  
 
HWND hEditWnd; 
  
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;  
  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpszCmdLine, int nCmdShow)  
{  
    WNDCLASS wndClass;  
    MSG msg;  
    HWND hWnd;  
    RECT rect; 
    
    if ( !hPrevInstance )  
    {  
wndClass.style= CS_HREDRAW | CS_VREDRAW ;  
wndClass.lpfnWndProc= WndProc;  
wndClass.cbClsExtra = 0;  
wndClass.cbWndExtra= 0;  
wndClass.hInstance = hInstance;  
wndClass.hIcon = LoadIcon(NULL, szAppName);  
wndClass.hCursor= LoadCursor(NULL, IDC_CROSS);  
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );  
wndClass.lpszMenuName= NULL; 
wndClass.lpszClassName= szAppName;  
  
    if (!RegisterClass(&wndClass))  
return 0;  
    }  
  

359

background image

    hWnd = CreateWindow(szAppName,  
                 "MEDYT-01", WS_OVERLAPPEDWINDOW,  
                  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,  
                  0, 0, hInstance, 0);  
 
GetClientRect(hWnd, (LPRECT) &rect);  
hEditWnd = CreateWindow ("Edit",NULL, WS_CHILD | WS_VISIBLE |  
                            ES_MULTILINE | WS_VSCROLL |  
                            WS_HSCROLL | ES_AUTOHSCROLL |  
                            ES_AUTOVSCROLL, 0, 0,(rect. right -  
                            rect. left),  
                            (rect. bottom - rect.  
                            top),hWnd,IDC_EDIT, hIstance,NULL);  
if( ! hEditWnd )  
  {  
  DestroyWindow(hWnd);  
  return 0;  
  }  
ShowWindow(hWnd, nCmdShow); 
UpdateWindow(hWnd);  
 while (GetMessage(&msg, NULL, 0, 0))  
    {  
  TranslateMessage(&msg );  
  DispatchMessage(&msg );  
    }  
    return 0;  
}  
  
long FAR PASCAL WndProc (HWND hWnd, unsigned Message,  
 WORD wParam, LONG lParam)  
{  
 switch(Message)  
    {  
     case ID_EDIT: 
       if(HIWORD(lParam)==EN_ERRSPACE)  
/* starsze słowo lParam zawiera właściwe dla okna edycyjnego  wiadomości, jeżeli jest to EN_ERRSPACE - 
okno sterujące nie może alokować dodatkowego obszaru pamięci                 */  
      {  
        MessageBox (GetFocus(), "Brak pamieci", "MEDYT-01",  
                       MB_ICONSTOP | MB_OK);  
      }  
      break;  
     case WM_SETFOCUS:  
      SetFocus(hEditWnd);  
      break;  
/* Pierwsze dwa parametry funkcji MoveWindow są ustawione na zero, dzięki temu po zastosowaniu tej funkcji 
nie zmieni się wzajemne położenie obu okien, a jedynie uaktualnianiu ulegnie okno sterujące. */  
     case WM_SIZE:  
      MoveWindows(hEditWnd, 0, 0, LOWORD(lParam));  
      HIWORD(lParam), TRUE);  
      break;  
case WM_DESTROY:  
    PostQuitMessage(0);  
    break;  
  
default:  
    return (DefWindowProc(hWnd,Message,wParam,lParam));  
    }  
    return 0;  
}  

360

background image

 Jak sterować pracą aplikacji. Jak umieszczać elementy  graficzne-sterujące w oknie aplikacji. Najczęściej 
stosowane  funkcje API Windows.  
Elementy sterujące pracą aplikacji w Windows (ang. controls) są  również swoistymi okienkami (tyle, że 
potomnymi - Child Window wobec głównego okna aplikacji - Parent Window). Do utworzenia takiego 
specjalnego okna również można użyć funkcji CreateWindow(). Jeśli okno ma stać się nie głównym oknem 
aplikacji, lecz oknem sterującym przebiegiem programu, funkcja wymaga podania następujących argumentów:  
- rodzaj klasy sterującej (ang. control class)  
- rodzaj elementu sterującego (ang. control style)  

Typowe rodzaje elementów (obiektów) starujących w środowisku  

Windows: 

BUTTON                - klawisz rozkazu, prostokątne okno typu  
                        Child, reprezentujące przycisk, który  
                        użytkownik może włączyć; przycisk może  
                        być opatrzony etykietą (text label).  
COMBOBOX              - okienko dialogowe kombinowane. Jest  
                         złożeniem klasy EDIT i LISTBOX;  
LISTBOX                - oknienko z listą (zwykle element  
                            składowy okienka dialogowego typu  
                            Combo Box. 
STATIC                  - pole statyczne (bez prawa edycji).  
                           Niewielkie okno zawierające tekst lub 
                           grafikę; służy z reguły do oznaczania 
                           innych okien sterujących. 
SCROLLBAR                - pasek przewijania (pionowy - Vertical 
                           Scroll Bar; poziomy - Horizontal  
                           Scroll Bar). 
Style klawiszy sterujących (Button Styles): 
BS_PUSHBUTTON              - Klawisz. Okno sterujące wysyła, po  
                              każdym wyborze klawisza  
                              (kliknięcie), wiadomość do okna  
                              macieżystego (Parent Window). 
BS_RADIOBUTTON              - Okrągły przełącznik działający  
                                zwykle na zasadzie @tylko jeden  
                                z grupy". 
BS_CHECKBOX -                - prostokątny przełącznik [X]  
                                  włączający (aktywna) lub  
                                  wyłączający (nieaktywna)  
                                  opcję. Działa niezależnie od  
                                  pozostałych. 
 Inne style określają np. sposób edycji tekstu (ES_LEFT, ES_MULTILINE, itp.) Szczegóły - patrz system Help - 
Windows API. Oto przykład utworzenia okna elementu sterującego typu "Klawisz" 
 
(BUTTON to nazwa typu): 
 
hControlWnd = CreateWindow ("BUTTON", " Napis_na_Klawiszu ",  
              BS_PUSHBUTTON |WS_CHILD | WS_VISIBLE,  
              10, 20, 30, 40,   
              hWnd, ID_Elem, hInstance, 0);  
 
Identyfikator ID_Elem jest potrzebny, gdy w jednym oknie znajduje się kilka elementów sterujących - pozwala 
na ich  rozpoznawanie w programie. Sposób przekazywania informacji o  kliknięciu klawisza przypomnę na 
przykładzie okienka  komunikatów:  
 
if(IDOK==MessageBox(0, "", "", MB_OK)) ...  
 
IDOK to predefiniowany w Windows identyfikator klawisza [OK]. Oto krótkie wyjaśnienie pozostałych 
elementów:  

361

background image

 
10, 10, 30, 20,  - współrzędne. x, y, szerokość, wysokość  
hWnd,  - oznacznik okna macieżystego   
 
Przesuwanie i zmiana wielkości elementu sterującego. 
 
Funkcja MoveWindow() przesuwa okno we wskazane miejsce: 
 
MoveWindow(hKlawisz, 10, 10, 20, 30, TRUE);  

 Ponieważ okno elementu sterującego ma zadane względne współrzędne w oknie macieżystym, gdy okno 
macierzyste zostanie  przesunięte - element sterujący będzie przesunięty  automatycznie. Również po zmianie 
rozmiarów okna macieżystego okno elementu sterującego zmienia położenie, zawsze jednakowe względem 
lewego górnego rogu.  

Usuwanie okna sterującego  

 Okienko elementu sterującego możemy usunąć (jak i każde inne okna) przy pomocy funkcji: 
 
DestroyWindow(hKlawisz);  
Przekazywanie informacji do- i z- okna elementu sterującego   Zdarzenie w oknie elementu sterującego - np. 
kliknięcie klawisza  - powoduje wygenerowanie komunikatu WM_COMMAND. Towarzyszące  komunikatowi 
parametry przenoszą istotne informacje: 
wParam          - identyfikator elementu sterującego,  
lParam          - dla wciśniętego klawisza będzie to BN_CLICKED. Niektóre komunikaty Windows mogą być 
kierowane do okna elementu  sterującego i wymuszać pewne operacje. Dla przykładu komunikat 
WM_GETTEXTLENGTH przesłany do okienka edycyjnego typu Text Edit Box (element sterujący klasy EDIT) 
jest żądaniem podania  długości tekstu wpisanego właśnie do okienka. Aby Windows  wygenerowały komunikat 
i przesłały go do naszego elementu  sterującego - musimy "poprosić" przy pomocy funkcji  SendMessage() 
(WyślijKomunikat): DlugTekstu = SendMessage(hEditWnd, WM_GETTEXTLENGHT, 0, 0); gdzie:  hEditWnd 
jest identyfikatorem elementu - okienka edycyjnego  

Robi na "szaro'?  

Podobnie jak opcje w menu - klawisze także mogą zostać  udostępnione (ang. enable), bądź zablokowane (ang. 
disable).  Jeśli hKlawisz będzie identyfikatorem elementu sterującego,  można go udostępnić (1), bądź 
zablokować (0) przy pomocy  funkcji: 
 
EnableWindow(hKlawisz, 0);   
EnableWindow(hKlawisz, 1);   
Typowy projekt dla środowiska Windows składa się z kilku (czasem kilkunastu) plików: .H, .MNU, .DLG, .RC, 
.DEF, .PRJ, .ICO, .BMP, itp. Kompilator zasobów generuje na podstawie tego "składu"  końcowy plik aplikacji.  
------------------Plik MEDYT-01.H------------------------------- 
#define szAppName "MEDYT-01"  
#define ID_EDIT 200  
------------------Plik główny: MEDYT-01.CPP--------------------- 
#include <windows.h>  
#include "EDIT.H" 
#pragma argused  
 
HWND hEditWnd; 
  
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;  
  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpszCmdLine, int nCmdShow)  
{  
    WNDCLASS wndClass;  
    MSG msg;  
    HWND hWnd;  
    RECT rect; 

362

background image

    
    if ( !hPrevInstance )  
    {  
wndClass.style= CS_HREDRAW | CS_VREDRAW ;  
wndClass.lpfnWndProc= WndProc;  
wndClass.cbClsExtra = 0;  
wndClass.cbWndExtra= 0;  
wndClass.hInstance = hInstance;  
wndClass.hIcon = LoadIcon(NULL, szAppName);  
wndClass.hCursor= LoadCursor(NULL, IDC_CROSS);  
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );  
wndClass.lpszMenuName= NULL; 
wndClass.lpszClassName= szAppName;  
  
    if (!RegisterClass(&wndClass))  
return 0;  
    }  
  
    hWnd = CreateWindow(szAppName,  
                 "MEDYT-01", WS_OVERLAPPEDWINDOW,  
                  CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,  
                  0, 0, hInstance, 0);  
 
GetClientRect(hWnd, (LPRECT) &rect);  
hEditWnd = CreateWindow ("Edit",NULL, WS_CHILD | WS_VISIBLE |  
                            ES_MULTILINE | WS_VSCROLL |  
                            WS_HSCROLL | ES_AUTOHSCROLL |  
                            ES_AUTOVSCROLL, 0, 0,(rect. right -  
                            rect. left),  
                            (rect. bottom - rect.  
                            top),hWnd,IDC_EDIT, hIstance,NULL);  
if( ! hEditWnd )  
  {  
  DestroyWindow(hWnd);  
  return 0;  
  }  
ShowWindow(hWnd, nCmdShow); 
UpdateWindow(hWnd);  
 while (GetMessage(&msg, NULL, 0, 0))  
    {  
  TranslateMessage(&msg );  
  DispatchMessage(&msg );  
    }  
    return 0;  
}  
  
long FAR PASCAL WndProc (HWND hWnd, unsigned Message,  
 WORD wParam, LONG lParam)  
{  
 switch(Message)  
    {  
     case ID_EDIT: 
       if(HIWORD(lParam)==EN_ERRSPACE) 
 /* starsze słowo lParam zawiera właściwe dla okna edycyjnego  wiadomości, jeżeli jest to EN_ERRSPACE - 
okno sterujące nie może alokować dodatkowego obszaru pamięci                 */  
      {  
        MessageBox (GetFocus(), "Brak pamieci", "MEDYT-01",  
                       MB_ICONSTOP | MB_OK);  
      }  
      break;  

363

background image

     case WM_SETFOCUS:  
      SetFocus(hEditWnd);  
      break;  
/* Pierwsze dwa parametry funkcji MoveWindow są ustawione na  zero, dzięki temu po zastosowaniu tej funkcji 
nie zmieni się wzajemne położenie obu okien, a jedynie uaktualnianiu ulegnie okno sterujące. */  
 
    case WM_SIZE:  
      MoveWindows(hEditWnd, 0, 0, LOWORD(lParam));  
      HIWORD(lParam), TRUE);  
      break;  
case WM_DESTROY:  
    PostQuitMessage(0);  
    break;  
  
default:  
    return (DefWindowProc(hWnd,Message,wParam,lParam));  
    }  
    return 0;  
}  

LEKCJA 44: O Okienkach dialogowych.  

________________________________________________________________ 

O tym, jak konstruuje się okienka dialogowe.  

Do wyświetlania okienek dialogowych w Windows API służy funkcja  DialogBox(), a do zakończenia ich "życia 
na ekranie" -  EndDialog(). Podobnie jak każde okno, również okno dialogowe  musi mieć swoją funkcję, 
obsługi komunikatów Windows. Zamiast  WindowProc() nazywa się ją tradycyjnie DlgProc(): 
BOOL FAR PASCAL DlgProc(HWND hDLG, unsigned Message, WORD  
                           wParam, LONG lParam);  
{  
switch (message)  
  {  
  ...  
  default: return (0);  
  }  
}  
 Za wyjątkiem braku domyślnego handlera Windows - DefWindowProc(), który jest zbędny, w związku z 
wewnętrznie  przyjmowanymi wartościami domyślnymi, funkcja podobna jest  bardzo w swojej konstrukcji do 
WindowProc(). Funkcja zwraca  wartość FALSE (czyli 0), jeśli przesłany komunikat nie został  obsłużony. 
Typowymi komunikatami, które rozpatruje większość  okienek dialogowych, są WM_INITDIALOG oraz 
WM_COMMAND. 

Przykład okienka dialogowego: 

------------------Plik: DLGBOX1.H------------------------------- 
 
#define szAppName "DLGBOX1" 
#define IDM_DLG1 100 
 
------------------Plik zasobów: DLGBOX1.RC---------------------- 
  
#include "DLGBOX1.H"  
#include <windows.h>  
  
IDI_ICON ICON CONTROL.ICO  
  
DLGBOX1 MENU  
BEGIN  
  MENUITEM "&O DlgBox" IDM_DLG1  

364

background image

/* to menu pojawi się w oknie macieżystym */ 
END  
DLGBOX1 DIALOG 30,30,200,100 
/* Pierwsze liczby to współrzędne lewego-górnego rogu okna, dwie następne - to szerokość i długość. 
Współrzędne są względne. Punkt (0,0) to narożnik okna macieżystego                  */ 
 
STYLE WS_POPUP | WS_DLGFRAME  
 
BEGIN  
LTEXT "Przyklad"                  -1, 0, 12, 160, 8  
CTEXT "DLGBOX1 - Przyklad"        -1, 0, 36, 160, 8  
DEFPUSHBUTTON "OK"           IDOK, 64, 60,  32,14, WS_GROUP  
END  
Pomiędzy parą słów kluczowych BEGIN-END można umieszczać różne  instrukcje sterujące. Definiują one, jaki 
rodzaj okna  sterującego ukaże się w okienku dialogowym. Instrukcje te można  stosować w następującym 
formacie:  
 
typ_okna "tekst" ID, x, y, szerokość, wysokość [styl]  
 
Parametr styl jest opcjonalny. Styl okna określają identyfikatory predefiniowane w API Windows (WS_...). 
Parametr ID jest odpowiednikiem identyfikatora dla okien potomnych typu Child Window; dla okien 
sterujących, które nie zwracają komunikatów do okna macierzystego, ma wartość -1. IDOK  wykorzystaliśmy 
jako identyfikator dla okna sterującego typu  BUTTON. Zostanie on wysłany do funkcji okienkowej jako 
wartość parametru wParam, gdy użytkownik kliknie klawisz.

------------------Plik główny: DLGBOX1.CPP---------------------- 

 #include <windows.h>  
#include <stdio.h>  
#include <string.h>  
#include "DLGBOX1.H" 
#pragma argused  
 
HANDLE    hInst;  
  
long FAR PASCAL  WndProc (HWND, unsigned, WORD, LONG) ;  
BOOL FAR PASCAL  ControlProc (HWND, unsigned, WORD, LONG) ;  
  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpszCmdLine, int nCmdShow )  
{  
    WNDCLASS wndClass;  
    MSG msg;  
    HWND hWnd;  
    if ( !hPrevInstance )  
    {  
wndClass.style= CS_HREDRAW | CS_VREDRAW ;  
wndClass.lpfnWndProc= WndProc;  
wndClass.cbClsExtra = 0;  
wndClass.cbWndExtra= 0;  
wndClass.hInstance = hInstance;  
wndClass.hIcon = LoadIcon(NULL, szAppName);  
wndClass.hCursor= LoadCursor(NULL, IDC_ARROW );  
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );  
wndClass.lpszMenuName= szAppName;  
wndClass.lpszClassName= szAppName;  
  
    if (!RegisterClass(&wndClass))  
return 0;  
    }  
  

365

background image

hInst = hInstance;  
hWnd = CreateWindow(szAppName, "DLGBOX1", WS_OVERLAPPEDWINDOW,  
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hInstance, 0);  
  
    ShowWindow(hWnd, nCmdShow);  
    UpdateWindow(hWnd);  
     while (GetMessage(&msg, 0, 0, 0))  
    {  
  TranslateMessage(&msg );  
  DispatchMessage(&msg );  
    }  
    return 0;  
}  
BOOL FAR PASCAL ControlProc (HWND hDlg, unsigned Message,  
 WORD wParam, LONG lParam)  
{  
    switch(msg)  
    {  
case WM_INITDIALOG:  
     return TRUE; 
    break;  
case WM_COMMAND:  
     switch(wParam)  
     {  
     case IDOK:  
     case IDCANCEL:  
  EndDialog(hDlg,0);  
  return TRUE;  
     }  
     break;  
    }  
    return (0);  
}  
  long FAR PASCAL WndProc (HWND hWnd, unsigned msg,  
 WORD wParam, LONG lParam)  
{  
FARPROC lpControlProc;  
    switch(Message)  
    {  
case WM_COMMAND:  
    switch(wParam)  
    {  
    case IDM_ABOUT:  
 lpControlProc = MakeProcInstance((FARPROC) ControlProc, hInst); 
 DialogBox(hInst, "DLGBOX1", hWnd, lpControlProc);  
 return 0;  
    }  
    break;  
case WM_DESTROY:  
    hDC = BeginPaint(hWnd , &ps);  
    TextOut(hDC, 30, 50,"Demo okienka dialogowego", 25);  
    TextOut(hDC, 30, 70,"Zastosuj menu...", 17);  
    EndPaint(hWnd, &ps);  
    break;  
case WM_DESTROY:  
    PostQuitMessage(0);  
    break;  
default:  
    return (DefWindowProc(hWnd,Message,wParam,lParam));  
    }  

366

background image

    return 0;  
}   

Stosując okienko edycyjne możemy użyć następujących  

predefiniowanych parametrów: 

 CONTROL         - określa okno elementu sterującego zdefiniowane przez użytkownika.  
CONTROL "tekst", klasa, styl, x, y, szerokość, wysokość  
LTEXT  - element sterujący: okienko tekstowe Wyrównywanie tesktu: do lewej.  
RTEXT  - j. w. Wyrównywanie tesktu: do prawej 
CTEXT  - j. w. Wyrównywanie tesktu: centrowanie w okienku 
CHECKBOX - pole tekstowe po prawej stronie przełącznika typu  Check Box. 
PUSHBUTTON  - Klawisz z napisem. 
LISTBOX  - okienko z listą 
GROUPBOX  - grupa elementów sterujących typu BUTTON; zgrupowanie kilku elementów sterujących i 
otoczenie ramką. Tekst zostanie umieszczony w lewym górnym rogu.  
DEFPUSHBUTTON    - Klawisz domyślny w stylu BS_DEFPUSHBUTTON.  
RADIOBUTTON     - analogicznie jak dla stylu BS_RADIOBUTTON. 
EDITTEXT         - tworzy okno oparte na klasie EDIT. 
COMBOBOX         - tworz okno oparte na klasie COMBOBOX. 
ICON             - definiuje ikonę opartą na klasie STATIC; w  
                    okienku dialogowym.  
SCROLLBAR       - tworzy okno oparte na klasie SCROLLBAR.   

UWAGA  

W niektórych przypadkach okienko dialogowe może być głównym  oknem aplikacji.  

LEKCJA 45: Dołączanie zasobów - menu i okienka dialogowe. 

________________________________________________________________  

Jak dodać menu i okienka dialogowe do aplikacji.  

Aby dodać do aplikacji menu należy utworzyć plik (ASCII) zasobów *.RC, który zostanie użyty w projekcie. 
Pierwszą instrukcją jest MENU, "NazwaMenu",  MENU i para słów kluczowych (znanych z Pascala) BEGIN 
oraz END,  między którymi znajdzie się kombinacja instrukcji MENUITEM oraz  POPUP.  
 MENUITEM definiuje pozycję na głównym pasku menu - określa - jak będzie wyglądać i jaki identyfikator 
będzie ją reprezentował.  Instrukcja POPUP pozwala, rozwinąć listę pozycji wchodzących w  skład danego 
menu. Nazwa menu może być użyta podczas rejestracji klasy danego okna jako wpisana w odpowiednie pole 
struktury na  której oparte jest okno. W ten sposób uzyskamy menu dla  wszystkich okien danej klasy.  
BEGIN  
  POPUP "Rozkaz"  
    BEGIN  
       MENUITEM "Rozkaz 1", IDM_R1  
       MENUITEM "Rozkaz 2", IDM_R2  
       MENUITEM "Rozkaz 3", IDM_R3  
    END  
  POPUP "Kolor"  
    BEGIN  
       MENUITEM "Czarny", IDM_BLACK  
       MENUITEM "Niebieski", IDM_BLUE  
       MENUITEM "Zielony", IDM_GREEN  
  END  
  MENUITEM "Koniec", IDM_EXIT  
END  
 Każda pozycja ma własny identyfikator, np. IDM_EXIT, IDM_BLUE, który Windows przekazują do aplikacji, 
gdy zostaje ona wybrana  przez użytkownika z systemu menu. Ponieważ każdy identyfikator  powinien być 
unikalny, najlepiej jest go zdefiniować w pliku  zasobów .RC lub własnym pliku nagłówkowym .H: 
#define IDM_EXIT 100  

367

background image

#define IDM_BLUE 101  
#define IDM_R1   102  
 ... 
 Mamy już zdefiniowane menu w pliku zasobów, należy je teraz  dołączyć do aplikacji na jeden z dwóch 
sposobów:  
  - Można określić menu jako menu danej klasy okien, gdy klasa ta jest rejestrowana. W ten sposób dołączymy 
menu do każdego okna opartego na tej klasie. Aby to wykonać, wystarczy przypisać odpowiedniemu polu 
struktury nazwę naszego menu. Jeżeli obiekt klasy WNDCLASS nazwiemy Window1, to:  
 
Window1.lpszMenuName = "NazwaMenu";  
 Gdy klasa zostanie zarejestrowana, każde okno tej klasy będzie miało to samo menu, chyba że dostarczymy 
odpowiedni  identyfikator menu w momencie tworzenia okna funkcją  
CreateWindow().  
  - Drugim sposobem jest dołączenie menu w momencie tworzenia okna, wtedy tylko tworzone okno będzie 
miało dane menu.  
 Należy najpierw załadować menu przy użyciu funkcji LoadMenu(), która zwraca jego identyfikator:  
HMENU  hMenu = LoadMenu(hInstance, "NazwaMenu");  
hWnd = CreateWindow(szAppName,  
                    "Nazwa Aplikacji",  
                    WS_OVERLAPPEDWINDOW,  
                    CW_USEDEFAULT,  
                    CW_USEDEFAULT,  
                    CW_USEDEFAULT,  
                    CW_USEDEFAULT,  
                    NULL,  
                    hMenu,                 <-- tu 
                    hIstance,  
                    NULL );  
 Typową praktyką jest dołączenie pozycji menu do instrukcji switch w funkcji okienkowej. Ponieważ Windows 
wysyła komunikat WM_COMMAND do odpowiedniej funkcji okienkowej w odpowiedzi na wybór pozycji 
przez użytkownika, a parametr wParam zawiera identyfikator tejże pozycji - można napisać tak: 
case WM_COMMAND:  
switch (wParam)  
{  
case IDM_R1:     
            ... obsługa ...; break; 
case IDM_R2:    
            ... obsługa ...; break 
case IDM_QUIT:     
            ...DestroyWindow(...);  
}  
Jak rozbudowuje się menu. API Windows zawiera funkcje, umożliwiające rozbudowę menu nawet w ruchu 
aplikacji (run-time). Rozbudowa menu w konkretnym oknie nie pociąga za sobą zmian w innych, opartych na tej 
samej klasie oknach. Jest to możliwe, ponieważ w chwili tworzenia okna otrzymuje ono swoją kopię menu 
(tradycyjne w C/C++ przekazywanie kopii zmiennej do funkcji).Nie wszystkie pozycje w menu są w danym 
stadium pracy aplikacji  sensowne (możliwe do wykonania). Zaraz przekonasz się, jak to  się dzieje, że niektóre 
pozycje "robi się na szaro". W API  Windows służy do tego funkcja: 
 
EnableMenuItem (hMenu, IDM_R1, MF_DISABLED);  
EnableMenuItem (hMenu, IDM_R1, MF_GRAYED);  
EnableMenuItem (hMenu, IDM_R1, MF_ENABLED);  
 Rozkaz R1 skojarzony z identyfikatorem IDM_R1 i znajdujący się w systemie menu o oznaczniku hMenu 
stanie się kolejno zablokowany, widoczny-lecz-niedostępny, dostępny. 

Dodawanie i usuwanie pozycji w menu  

 Dodawanie pozycji do menu może być wykonane dwoma sposobami:  przez wstawienie pomiędzy istniejące 
pozycje lub na końcu listy.  W pierwszym przypadku należy użyć funkcji InsertMenu(). Funkcja ta pozwala 
jednocześnie określić status pozycji, między innymi czy będzie umieszczone nowe pole można określić dwoma 
sposobami: przez identyfikator pozycji mającej być przed nową lub przez  numerację poszczególnych, licząc id 

368

background image

lewej skrajnej pozycji (C++  tradycyjnie liczy od zera). Sposób "odliczania" pozycji w  systemie menu określa 
tryb (BYCOMMAND lub BYPOSITION - rozkaz,  bądź pozycja):  
 
InsertMenu(hMenu, IDM_R1, MF_BYCOMMAND |MF_DISABLED, IDM_R5,  
"Rozkaz 5");  
InsertMenu(hMenu, 1, MF_ENABLED, IDM_R5, "Rozkaz 5");  
 
Funkcja wstawi za pozycją "Rozkaz 1" nową pozycję "Rozkaz 5",  jednocześnie ustawia jej status. Drugą 
funkcją dodającą pozycję  do utworzonego systemu menu jest: 
 
AppendMenu(hMenu, MF_ENABLED, IDM_R4, "Rozkaz 4");  
 
Poniżej przykład zdefiniowania menu aplikacji w taki właśnie  sposób: 

case WM_CREATE:  
hMenu = CreateMenu();     //Utworzenie menu 
AppendMenu(hMenu, MF_ENABLED, IDM_R1, "Rozkaz 1");  
AppendMenu(hMenu, MF_ENABLED, IDM_R2, "Rozkaz 2");  
AppendMenu(hMenu, MF_ENABLED, IDM_R3, "Rozkaz 3");  
SetMenu(hWnd, hMenu);     //Wyświetlenie menu  
 ...  
break;  
 Usuwanie pozycji z menu można przeprowadzić dwoma sposobami:  
  - poprzez wskazanie numeru pozycji w systemie menu: 
DeleteMenu(hMenu, 1, MF_BYPOSITION); //usunięta zostanie druga  
                                     //pozycja z systemu menu 
  - przez wyszczególnienie identyfikatorem pozycji   
DeleteMenu(hMenu, IDM_R3, MF_BYCOMMAND);  
 Po usunięciu pozycji z menu Window usunie również wszystkie  związane z nią submenu.  

Zaznaczanie pozycji w menu (mark). 

 Obok pozycji w menu można umieścić znak markujący ("ptaszek").  Znak markujący można zainicjować w 
pliku zasobów .RC. Dzięki  temu, użytkownik w momencie otwarcia okna dowie się z wyglądu  menu o 
początkowym ustawieniu opcji.  
 MENUITEM "Rozkaz 2", IDM_R2, CHECKED  
 W trakcie pracy aplikacji należy posłużyć się funkcją  CheckMenuItem(). Zwykle najpierw kasujemy "ptaszka" 
przy  poprzedniej pozycji:  
 
CheckMenuItem( hMenu, IDM_R2, MF_UNCHECKED);  
CheckMenuItem(hMenu, IDM_R3, MF_CHECKED);  

Zmiany pozycji menu  

Funkcja ModyfyMenu() pozwala na zmianę nazwy pozycji i jej atrybutów. Oto przykłady użycia tej funkcji:  
 ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND, IDM_R2, "Polecenie 2");  
 Identyfikator pozycji nie ulegnie zmianie, jedynie nazwa pola z  "Rozkaz 2" na "Polecenie 2". Możemy zmienić 
jednocześnie i  identyfikator, by nie pomylić się w programie:  
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND, IDM_P2, "Polecenie 2");  
Dodatkowo można ustawić za jednym zamachem i atrybuty:  
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND | MF_CHECKED | MF_GRAYED, 
 
           IDM_R2, "Polecenie 2");  

Użycie grafiki w systemie menu.  

W systemie menu aplikacji możemy zamiast łańcucha znaków "Rozkaz 
2" umieścić element graficzny - np. w postaci mapy bitowej.  Zamiast pola o nazwie "Pole", wprowadza mapę 
bitową:  
HMENU hMenu = GetMenu(hWnd);  
HBITMAP hBitmap = LoadBitmap (hIstance, "Pole");  
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND | MF_BITMAP, IDM_R2,  

369

background image

(LPSTR) MAKELONG (hBitmap, 0));  
 GetMenu() zwraca oznacznik aktualnego menu, potrzebny jako pierwszy parametr funkcji ModifyMenu(). 
Drugim parametrem tej funkcji jest identyfikator pozycji, którą chcemy zmienić. Trzecia określa, że zmiana ma 
być wykonana przez wyszukanie pozycji za pośrednictwem jej identyfikatora oraz że nową pozycję ma 
reprezentować mapa bitowa. Czwarty parametr określa  identyfikator nowej pozycji. Ponieważ ostatnim 
parametrem nie  jest już wskaźnik do łańcucha znakowego, należy przesłać  oznacznik mapy bitowej jako mniej 
znaczące słowo tego parametru. W tym celu 16-bitowy oznacznik jest łączony z 16-bitową stałą, a następnie 
poddawany konwersji do typu Long Pointer to STRing.  

Zmiana menu aplikacji na kolejne.  

 Aplikacja w różnych stadiach pracy może mieć na ekranie różne  (kilka czasem kilkanaście) menu. Wymiany 
menu w oknie aplikacji  można dokonać, załadowując nowe menu funkcją LoadMenu() i  ustawiając je jako 
aktualne funkcją SetMenu(): 
...  
hMenu2 = LoadMenu (hIstance, "Menu2");  
SetMenu (hWnd, hMenu2);  
DrawMenuBar(...); 
...  
 Menu i Menu2 powinny być zdefiniowane w pliku zasobów *.RC.  Po każdej zmianie menu należy użyć funkcji 
DrawMenuBar(), aby  wprowadzone zmiany pojawiły się na ekranie. Oto przykład  stosownego pliku zasobów: 
 
Menu1 MENU  
BEGIN  
  POPUP "&File"  
  BEGIN  
    MENUITEM "&New" , IDM_NEW  
    MENUITEM "&Save", IDM_SAVE  
    MENUITEM "E&xit", IDM_EXIT  
  END  
  POPUP "&Options"  
  BEGIN  
    MENUITEM "Menu&1", IDM_M1,CHECKED  
    MENUITEM "Menu&2" , IDM_M2  
  END  
END  
 
Menu2 MENU  
BEGIN  
  POPUP "&File"  
  BEGIN 
    MENUITEM "&Open", IDM_OPEN  
    MENUITEM "&New" , IDM_NEW  
    MENUITEM "&Save", IDM_SAVE  
    MENUITEM "Save &As", IDM_SAVEAS  
    MENUITEM "&DOS shell", IDM_DOSSHELL  
    MENUITEM "E&xit", IDM_EXIT  
  END  
  POPUP "&Options"  
  BEGIN  
    MENUITEM "Menu&1", IDM_M1,  
    MENUITEM "Menu&2" , IDM_M2, CHECKED  
  END  
END  

ZASTOSOWANIE Resource Worshop 

 Takie pliki zasobów w Borland C++ mało kto tworzy dziś "na  
piechotę". BORLAND C++ oferuje do tego celu dwa  
narzędzia: Edytor zasobów - Resource Workshop  Automatyczny generator - DialogExpert (wersje 4+) 
Najwygodniejszym sposobem jest zastosowanie edytora zasobów  Resource Workshop. Jest to tym 

370

background image

wygodniejsze, że Resource  Workshop pozwala jednocześnie obserwować i źródłowy plik *.RC  (ASCII) i efekt 
- menu w ruchu.  W środowisku Borland C++ okienka dialogowe tworzy się także  zwykle przy pomocy 
Resource Worshop.  Tworzenie okienek dialogowych przy pomocy Resource Workshop  przypomina składanie 
budowli z gotowych klocków. Kolejne elementy sterujące możemy umieszczać w okienku  dialogowym poprzez 
wybranie ich z palety narzędzi i  przeniesienie do projektowanego okienka techniką "pociągnij i  upuść" (drag & 
drop).  Po skróconym omówieniu najważniejszych funkcji z API Windows  przejdźmy to niemniej krótkiej 
prezentacji zasad tworzenia  aplikacji przy pomocy biblioteki obiektów OWL.  

Zadania  

1. Przeanalizuj program w pełnej wersji (na dyskietce).  
2. Zmodyfikuj dowolną aplikację przykładową tak, by dołączyć do  niej inną ikonę.  
3. Opracuj własne menu i własną ikonę przy pomocy Resource  Workshop.  

Krótka instrukcja do Resource  Workshop. 

 1. Uruchomienie: Ikonka Worshop w oknie grupowym Borland C++. 
2. Początek pracy: File | New Project...  
3. Rodzaje zasobów do wyboru w okienku dialogowym "New project":  
[ ] RC - plik zasobów  
[ ] CUR - kursor  
[ ] BMP - mapa bitowa  
[ ] RES - plik zasobów w formie skompilowanej  
[ ] ICO - ikonka  
[ ] FNT - czcionki (Fonts)  
Wybieramy odpowiednio: RC  
4. Zmieni się menu w głównym oknie Resource Workshop. Z menu  wybieramy Resource | New  W okienku 
dialogowym z listy Resource Type (rodzaj zasobów):  ACCELERATORS, BITMAP, CURSOR, DIALOG, 
FONT, ICON, MENU, RCDATA,  STRINGTABLE, VERSINFO wybieramy odpowiednio MENU lub DILOG. 
Kolejny raz zmieni się menu. W przypadku menu wybieramy:  Menu:  New pop-up - nowa pozycja POPUP 
New menu item - nowa pozycja MENUITEM  Zwróć uwagę, że typowe menu File, Edit, Help jesy już gotowe 
do  wstawienia (ukryte pod pozycjami New file pop-up, New edit  pop-up...). .W przypadku okienka 
dialogowego najważniejsze jest menu Control. Są tam wszyskie rodzaje podstawowych elementów sterujących 
(Push button, Radio button, scroll bar, List box, Combo box, Edit box, itd.). Projektując okienko możesz również 
wyświetlić siatkę (Grid). Przy pomocy Resource Workshop możesz poddawać edycji i  modyfikować pliki 
zasobów zarówno należące do programów  przykładowych zawartoch na dyskietce, jak i zasoby "firmowych" 
przykładów Borlanda. W katalogach \SOURCE (kody źródłowe .CPP) i \EXAMPLES (przykłady - projekty) 
znajdziesz wiele rozmaitych przykładów. Możesz także poddawać edycji pliki .BMP, .ICO i inne niekoniecznie 
należące do pakietu Borland C++. 

LEKCJA 46: O PROGRAMACH OBIEKTOWO - ZDARZENIOWYCH.  

________________________________________________________________ 

Po aplikacjach sekwencyjnych, proceduralno-zdarzeniowych, jedno- i 
dwupoziomowych, pora rozważyć dokładniej stosowanie technik  obiektowych. 

Programy pracujące w środowisku Windows tworzone są w oparciu o  tzw. model trójwarstwowy. Pierwsza 
warstwa to warstwa  wizualizacji, druga - interfejs, a trzecia - to właściwa  maszyneria programu. W tej lekcji 
zajmiemy się "anatomią"  aplikacji wielowarstwowych a następnie sposobami wykorzystania  bogatego 
instrumentarium oferowanego przez Borlanda wraz z  kompilatorami BC++ 3+...4+. Biblioteka OWL w 
wersjach BORLAND C++ 3, 3.1, 4 i 4.5 zawiera  definicje klas potrzebnych do tworzenia aplikacji dla 
Windows.  Fundamentalne znaczenie dla większości typowych aplikacji mają  następujące klasy:  
TModule (moduł - program lub biblioteka DLL) 
TApplication (program - aplikacja)  
TWindow (Okno)  
  Rozpocznę od krótkiego opisu dwu podstawowych klas.  

KLASA TApplication.  

 Tworząc obiekt klasy TNaszProgram będziemy wykorzystywać  dziedziczenie od tej właśnie klasy bazowej:  
 

371

background image

class TNaszProgram : public TApplication  

 Podstawowym celem zastosowania tej właśnie klasy bazowej jest  odziedziczenie gotowej funkcji - metody 
virtual InitMainWindow() (zainicjuj główne okno programu). Utworzenie obiektu klasy TNaszProgram 
następuje zwykle w czterech etapach:  
 * Windows uruchamiają program wywołując główną funkcję WinMain()  lub OwlMain() wchodzącą w skład 
każdej aplikacji.  
* Funkcja WinMain() tworzy przy pomocy operatora new nowy obiekt - aplikację.  
* Obiekt - aplikacja zaczyna funkcjonować. Konstruktor obiektu  (własny, bądź odziedziczony po klasie 
TApplication) wywołuje funkcję - wirtualną metodę InitMainWindow(). 
* Funkcja przy pomocy operatora new tworzy obiekt - okno  aplikacji.  

Wskaźnik do utworzonego obiektu zwraca funkcja GetApplication(). 

 Dla zobrazowania mechanizmów poniżej przedstawiamy uproszczony  "wyciąg" z dwu opisywanych klas. Nie 
jest to dokładna kopia kodu źródłowego Borlanda, lecz skrót tego kodu pozwalający na  zrozumienie metod 
implementacji okienkowych mechanizmów wewnątrz  klas biblioteki OWL i tym samym wewnątrz obiektów 
obiektowo -  zdarzeniowych aplikacji.  A oto najważniejsze elementy implementacji klasy TApplication:  
 - Konstruktor obiektu "Aplikacja":  
TApplication::TApplication(const char far* name,  
                           HINSTANCE       Instance,  
                           HINSTANCE       prevInstance,  
                           const char far* CmdLine,  
                           int             CmdShow,  
                           TModule*&       gModule)  
{  
  hPrevInstance = prevInstance;  
  nCmdShow = CmdShow;  
  MainWindow = 0;  
  HAccTable = 0;              //Accelerator Keys Table Handle 
  BreakMessageLoop = FALSE; 
  AddApplicationObject(this);    //this to wskaźnik do własnego 
  gModule = this;           //obiektu, czyli do bież. aplikacji 
}   

Funkcja - metoda "Zainicjuj Instancję": 

 void TApplication::InitInstance()  
{  
  InitMainWindow();  
if (MainWindow)  
   {  
    MainWindow->SetFlag(wfMainWindow);  
    MainWindow->Create();  
    MainWindow->Show(nCmdShow);  
   }  

Metoda "Zainicjuj główne okno aplikacji":  

 void TApplication::InitMainWindow()  
{  
  SetMainWindow(new TFrameWindow(0, GetName()));  
}  
 Metoda Run() - "Uruchom program":  
 int TApplication::Run()  
{  
  int status;  
   {  
    if (!hPrevInstance) InitApplication();  
    InitInstance();  
    status = MessageLoop();  
  }  

372

background image

 A oto pętla pobierania komunikatów w uproszczeniu. "Pump" to po  prostu "pompowanie" komunikatów 
(message) oczekujących (waiting)  w kolejce. PeekMessage() to sprawdzenie, czy w kolejce oczekuje 
komunikat. PM_REMOWE to "brak komunikatu". 
BOOL TApplication::PumpWaitingMessages()  
{  
  MSG  msg;  
  BOOL foundOne = FALSE;  
  while (::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))  
     {  
    foundOne = TRUE;  
    if (msg.message == WM_QUIT)  
      {  
      BreakMessageLoop = TRUE;  
      MessageLoopResult = msg.wParam;  
      ::PostQuitMessage(msg.wParam);  
      break;  
    }  
      if (!ProcessAppMsg(msg))  
      {  
      ::TranslateMessage(&msg);  
      ::DispatchMessage(&msg);  
    }  
  }  
  return foundOne;  
}  
  int TApplication::MessageLoop()  
{  
  long idleCount = 0;  
MessageLoopResult = 0;  
  while (!BreakMessageLoop) {  
    TRY {  
      if (!IdleAction(idleCount++))  
        ::WaitMessage();           
      if (PumpWaitingMessages())      
        idleCount = 0;  
    }  
      if (MessageLoopResult != 0) {  
        ::PostQuitMessage(MessageLoopResult);  
        break;  
      }  
    })  
  }  
  BreakMessageLoop = FALSE;  
  return MessageLoopResult;  
}  
  else if (::IsWindowEnabled(wnd)) {  
      *(info->Wnds++) = wnd;  
      ::EnableWindow(wnd, FALSE);  
    }  
  }  
  return TRUE;  
}  

KLASA TWindow.  

 Klasa TWindow (Okno) zawiera implementację wielu przydatnych  przy tworzeniu aplikacji "cegiełek". Poniżej 
przedstawiono  fragment pliku źródłowego (patrz \SOURCE\OWL\WINDOW.CPP). Łatwo  można rozpoznać 
pewne znane już elementy.  
 ... 
extern LRESULT FAR PASCAL _export InitWndProc(HWND, UINT,  
WPARAM, LPARAM);  

373

background image

 ... 
struct TCurrentEvent       //Struktura BieżąceZdarzenie 
{  
  TWindow*  win;           //Wskażnik do okna 
  UINT      message;       //Komunikat 
  WPARAM    wParam;         
  LPARAM    lParam;  
}; 
 ... 
DEFINE_RESPONSE_TABLE(TWindow)  
//Makro: Zdefiniuj tablicę odpowiedzi na zdarzenia 
//EV_WM_SIZE - Zdarzenie (EVent)-nadszedł komunikat WM_SIZE  
... 
  EV_WM_SETCURSOR,  
  EV_WM_SIZE,  
  EV_WM_MOVE,  
  EV_WM_PAINT,  
  EV_WM_LBUTTONDOWN,  
  EV_WM_KILLFOCUS,  
  EV_WM_CREATE,  
  EV_WM_CLOSE,  
  EV_WM_DESTROY, 
  EV_COMMAND(CM_EXIT, CmExit),  
  ...  
END_RESPONSE_TABLE;  

Funkcje - metody obsługujące komunikaty zaimplementowane zostały 

 wewnątrz klasy TWindow tak: 
TWindow::EvCreate(CREATESTRUCT far&)  
{  
  SetupWindow();  
  return (int)DefaultProcessing();  
}  
void TWindow::EvSize(UINT sizeType, TSize&)  
{  
  if (Scroller && sizeType != SIZE_MINIMIZED)  
  {  
    Scroller->SetPageSize();  
    Scroller->SetSBarRange();  
  }  

Metoda GetWindowClass() bardzo przypomina klasyczne zainicjowanie zanej 
już struktury WNDCLASS:  

void TWindow::GetWindowClass(WNDCLASS& wndClass)  
{  
  wndClass.cbClsExtra = 0;  
  wndClass.cbWndExtra = 0;  
  wndClass.hInstance = *GetModule();  
  wndClass.hIcon = 0;  
  wndClass.hCursor = ::LoadCursor(0, IDC_ARROW);  
  wndClass.hbrBackground = HBRUSH(COLOR_WINDOW + 1);  
  wndClass.lpszMenuName = 0;  
  wndClass.lpszClassName = GetClassName();  
  wndClass.style = CS_DBLCLKS;  
  wndClass.lpfnWndProc = InitWndProc;  
}  
  Skoro te wszystkie "klocki" zostały już zaimplementowane  wewnątrz definicji klas, nasze programy powinny 
tylko umiejętnie z nich korzystać a teksty źródłowe programów powinny ulec  skróceniu i uproszczeniu.  

374

background image

STADIA TWORZENIA OBIEKTOWEJ APLIKACJI.  

 Ponieważ znakomita większość dzisiejszych użytkowników pracuje z Windows 3.1, 3.11, i NT - zaczniemy 
tworzenie aplikacji od umieszczenia na początku informacji dla OWL, że nasz docelowy  program ma być 
przeznaczony właśnie dla tego środowiska:  
 #define WIN31  
 Jak już wiemy dzięki krótkiemu przeglądowi struktury bazowych klas przeprowadzonemu powyżej - funkcje 
API Windows są w istocie klasycznymi funkcjami posługującymi się mechanizmami języka C.  C++ jest 
"pedantem typologicznym" i przeprowadza dodatkowe  testowanie typów parametrów przekazywanych do 
funkcji (patrz  "Technika programowania w C++"). Aby ułatwić współpracę,  zwiększyć poziom bezpieczeństwa 
i "uregulować" potencjalne  konflikty - dodamy do programu:  
 #define STRICT  

Chcąc korzystać z biblioteki OWL wypada dołączyć właściwy plik nagłówkowy: 

 #include <owl.h>  
 Plik OWL.H zawiera już wewnątrz dołączony WINDOWS.H, który  występował we wcześniejszych 
aplikacjach proceduralno -  zdarzeniowych i jeszcze parę innych plików.  Ponieważ chcemy skorzystać z 
gotowych zasobów - odziedziczymy  pewne cechy po klasie bazowej TApplication. Zgodnie z zasadami 
programowania obiektowego chcąc utworzyć obiekt musimy najpierw  zdefiniować klasę:  
class TOkno ... 
i wskazać po której klasie bazowej chcemy dziedziczyć:  
class TOkno  :  public TApplication  
{  
 ...  
 Konstruktor obiektu klasy TOkno powinien tylko przekazać  parametry konstruktorowi klasy bazowej - i już.  
class TOkno  :  public TApplication  

public:  
TOkno(LPSTR name, HANDLE hInstance, HANDLE hPrevInstance,  
      LPSTR lpCmdLine, int nShow)  :  TApplication(name,  
      hInstance, hPrevInstance, lpCmdLine, nShow)  
  {  
    return;  
  }  
  virtual void InitMainWindow();  
};  
Umieściliśmy w definicji klasy jeszcze jedną funkcję inicjującą  główne okno aplikacji. Możemy ją zdefiniować 
np. tak:  
 void TOkno::InitMainWindow(void)  
{  
  MainWindow = new (TWindow(0, "Napis - Tytul Okna"));  
}  
 Działanie funkcji polega na utworzeniu nowego obiektu (operator  new) klasy bazowej TWindow. Główne okno 
stanie się zatem  obiektem klasy TWindow (Niektóre specyficzne aplikacje posługują  się okienkiem 
dialogowym jako głównym oknem programu. W takiej  sytuacji dziedziczenie powinno następować po klasie 
TDialog).  Konstruktorowi tego obiektu przekazujemy jako parametr napis,  który zostanie umieszczony w 
nagłówku głównego okna aplikacji.  Pierwszy argument (tu ZERO) to wskażnik do macieżystego okna, 
ponieważ w bardziej złożonych aplikacjach występują okna  macieżyste (parent) i okna potomne (child). Okno 
macieżyste to  zwykle obiekt klasy "główne okno" a okno potomne to najczęściej  okienko dialogowe, bądź 
okienko komunikatów. W tym przypadku  wpisujemy zero, ponieważ program nie posiada w tym stadium 
wcześniejszego okna macieżystego.  Pozostało nam jeszcze dodać funkcję WinMain() i pierwszy program 
obiektowy w wersji "Maszyna do robienia nic" jest gotów.  

Listing    . Obiektowa "Maszyna do robienia nic" 

#define STRICT  
#define WIN31  
#include <owl.h>  
class TOkno : public TApplication  
{  
public:  

375

background image

  TOkno(LPSTR AName, HANDLE hInstance, HANDLE hPrevInstance,  
    LPSTR lpCmdLine, int nCmdShow)  
    : TApplication(AName, hInstance, hPrevInstance, lpCmdLine,  
nCmdShow) {};  
  void InitMainWindow(){MainWindow = new TWindow(NULL, Name);};  
};  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
  LPSTR lpCmdLine, int nCmdShow)  
{  
  TOkno OBIEKT("Windows - Program PW1", hInstance,  
                  hPrevInstance, lpCmdLine, nCmdShow);  
  OBIEKT.Run();  
  return 0;  
}  
Wykonanie takiej aplikacji przebiega następująco. Windows  wywołują główną funkcję WinMain(), która 
przekazuje swoje  parametry do konstruktora klasy TOkno::TOkno(). Konstruktor  przekazuje parametry do 
konstruktora klasy bazowej  TApplication(). Po skonstruowaniu obiektu w pamięci funkcja  wywołuje 
odziedziczoną metodę Run(). Funkcja Run() wywołuje  metody InitApplication() (zainicjuj aplikację) i 
InitInstance()  (zainicjuj dane wystąpienie programu). Metoda InitInstance()  wywołuje funkcję 
InitMainWindow(), która buduje główne okno  aplikacji na ekranie. Po pojawieniu się okna rozpoczyna 
działanie pętla pobierania komunikatów (message loop). Pętla  komunikatów działa aż do otrzymania 
komunikatu WM_QUIT.  Rozbudujmy aplikację o okienko komunikatów. Zastosujemy do tego  funkcję 
MessageBox(). Funkcja zostanie użyta nie jako metoda  (składnik obiektu), lecz jako "wolny strzelec" (stand 
alone  function). 

Listing B. Maszyna rozszerzona o okienka komunikatów.  

#define WIN31  
#define STRICT 
#include <owl.h>  
class TOkno : public TApplication  
{  
public:  
  TOkno(LPSTR Nazwa, HANDLE hInstance, HANDLE hPrevInstance,  
    LPSTR lpCmdLine, int nCmdShow)  
    : TApplication(Nazwa, hInstance, hPrevInstance, lpCmdLine,  
nCmdShow) {};  
  void InitMainWindow(){MainWindow = new TWindow(NULL, "Okno  
PW2" );};  
};  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
  LPSTR lpCmdLine, int nCmdShow)  
{  
  TOkno OBIEKT("Okno PW2", hInstance, hPrevInstance,  
       lpCmdLine, nCmdShow);  
  LPSTR p1 = "Jesli wybierzesz [Anuluj]\n- aplikacja nie  
ruszy!";  
  LPSTR p2 = "START";  
   if (MessageBox(NULL, p1, p2, MB_OKCANCEL) == IDCANCEL)  
    MessageBox(NULL, "I juz..." , "KONIEC" , MB_OK);  
 else  
     OBIEKT.Run();  
return 0;  
}  

Uwagi techniczne.  

Ścieżki do katalogów:  
..\INCLUDE;..\CLASSLIB\INCLUDE;..\OWL\INCLUDE;  ..\LIB;..\CLASSLIB\LIB;..\OWL\LIB;  
Konsolidacja:  
Options | Linker | Settings: Windows EXE  (typ aplikacji) 

376

background image

Options | Linker | Libraries:  
- Container class Libraries: Static  (bibl. klas CLASSLIB) 
- OWL: Static                        (bibl. OWL statycze .LIB) 
- Standard Run-time Lib: Static      (bibl. uruchomieniowe .LIB) 
(.) None - oznacza żadne (nie zostaną dołączone);  
(.) Static - oznacza statyczne .LIB  
(.) Dinamic - oznacza dynamiczne .DLL 

JAK ROZBUDOWYWAĆ OBIEKTOWE APLIKACJE? 

 Mimo całego uroku obiektowych aplikacji pojawia się tu wszakże  drobny problem. Skoro komunikacja 
klawiatura/myszka -> program  -> ekran nie odbywa się wprost, lecz przy pomocy wymiany danych  pomiędzy 
obiektami różnych warstw - w jaki sposób (w którym miejscu programu) umieścić "zwyczajne" funkcje i 
procedury i jak  zorganizować wymianę informacji. "Zwyczajne" funkcje będą  przecież wchodzić w skład 
roboczych części naszych programów  (Engine). Rozważmy to na przykładzie aplikacji reagującej na 
naciśnięcie klawisza myszki. Najbardziej istotny -  "newralgiczny" punkt programu został zaznaczony w tekście 
"<--  TU". Od Windows przejmiemy obsługę komunikatów WM_LBUTTONDOWN, WM_RBUTTONDOWN. 
Aby wiedzieć, w którym miejscu ekranu jest  kursor myszki, wykorzystamy informacje przenoszone przez 
parametr lParam.  
 Rozpoczniemy tworzenie programu od zdefiniowania klasy.  
#define WIN31  
#define STRICT 
#include <stdio.h>  
#include <string.h>  
#include <owl.h>  
class TNAplikacja : public TApplication  
{  
public:  
  TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE  
              hPrevInstance, LPSTR lpCmdLine, int nCmdShow) :  
  TApplication(AName, hInstance, hPrevInstance, lpCmdLine,  
                    nCmdShow) {};  
  virtual void InitMainWindow();  
};  
 Wykorzystamy okienko komunikatu do świadomego zakończenia pracy  aplikacji. Klasa TApplication jest 
wyposażona w metodę  CanClose() (czy można zamknąć?) służącą do zamykania głównego  okna aplikacji. 
Metoda została zaimplementowana tak: 
BOOL TApplication::CanClose()  
{  
  if (MainWindow)  
    return (MainWindow->CanClose());  
  else  
    return (TRUE);  
}  
 Będzie nam więc potrzebna własna wersja metody CanClose() i  wskaźnik do obiektu MainWindow. Wskaźnik 
(typu far utworzony  przez składowe makro _FAR) wygenerujemy przy pomocy makra  
_CLASSDEF(nazwa_klasy):  
_CLASSDEF(TGOkno)  
 Implementujemy teraz klasę główne okno aplikacji. Jako klasę bazową stosujemy TWindow. 
class TGOkno : public TWindow  
{  
public:  
  TGOkno(PTWindowsObject AParent, LPSTR ATitle)  
    : TWindow(AParent, ATitle) {};  
 Konstruktor tradycyjnie wykorzystujemy do przekazania parametrów do konstruktora klasy bazowej. 
PTWindowsObject AParent to wskażnik (PoinTer) do obiektu "okno" a ATitle to string - tytuł. Obsługa 
komunikatów kierowanych do tego okna może być  realizowana przy pomocy metod zaimplementowanych jako 
elemeny  składowe klasy Główne Okno - TGOkno. Program graficzny powinien reagować raczej na myszkę niż 
na  klawiaturę. Windows rozpoznają zdarzenia związane z myszką i  generują komunikaty o tych zdarzeniach.  

377

background image

Zdarzenia myszki (mouse events).  

Komunikat              Zdarzenie 
________________________________________________________________ 
WM_MOUSEMOWE        - przesunięto myszkę (wewnątrz obszaru roboczego - inside the client area -  ICA) 
WM_LBUTTONDOWN      -    naciśnięto LEWY klawisz myszki (ICA) 
WM_LBUTTONDBLCLK    -    naciśnięto dwukrotnie LEWY klaw. (ICA) 
WM_LBUTTONUP        -    puszczono LEWY klawisz (ICA)  
WM_RBUTTONDOWN      -    naciśnięto PRAWY klawisz myszki (ICA) 
WM_RBUTTONDBLCLK    -    naciśnięto dwukrotnie PRAWY klaw. (ICA) 
WM_RBUTTONUP        -    puszczono PRAWY klawisz (ICA)  
WM_MBUTTONDOWN      -    naciśnięto ŚRODK. klawisz myszki (ICA) 
WM_MBUTTONDBLCLK    -    naciśnięto dwukrotnie ŚROD. klaw. (ICA) 
WM_MBUTTONUP        -    puszczono ŚRODKOWY klawisz (ICA)  
WM_NCMOUSEMOVE      -    ruch myszki poza client area (NCA) 
WM_NLBUTTONDOWN      -  naciśnięto LEWY klawisz myszki poza obszarem roboczym - non-client area 
(NCA) 
WM_NCLBUTTONDBLCLK   -    naciśnięto dwukrotnie LEWY klaw. (NCA) 
WM_NCLBUTTONUP       -    puszczono LEWY klawisz (NCA)  
WM_NCRBUTTONDOWN      -    naciśnięto PRAWY klawisz myszki (NCA) 
WM_NCRBUTTONDBLCLK    - naciśnięto dwukrotnie PRAWY klaw. (NCA) 
WM_NCRBUTTONUP        -    puszczono PRAWY klawisz (NCA) 
WM_NCMBUTTONDOWN      -    naciśnięto ŚR. klawisz myszki (NCA) 
WM_NCMBUTTONDBLCLK    - naciśnięto dwukrotnie ŚRODK. klaw. (NCA) 
WM_LBUTTONUP        -    puszczono ŚRODKOWY klawisz (NCA) 
Następna tabelka zawiera (znacznie skromniejszy) zestaw  komunikatów generowanych pod wpływem zdarzeń 
związanych z  klawiaturą. Choćby z wizualnego porównaia wielkości tych tabel  wyrażnie widać, że Windows 
znacznie bardziej "lubią" współpracę  z myszką. 

Komunikaty Windows w odpowiedzi na zdarzenia związane z klawiaturą. 

_______________________________________________________________ 
Komunikat            Zdarzenie  
_______________________________________________________________  
WM_KEYDOWN           Naciśnięto (jakiś) klawisz.  
WM_KEYUP             Puszczono klawisz.  
WM_SYSKEYDOWN        Naciśnięto klawisz "systemowy".  
WM_SYSKEYUP          Puszczono klawisz "systemowy".  
WM_CHAR              Kod ASCII klawisza.  
________________________________________________________________ 
 
Klawisz systemowy to np. [Alt]+[Esc], [Alt]+[F4] itp.  
________________________________________________________________ 
Komunikaty Windows możemy wykorzystać w programie. 
 ... 
BOOL CanClose();  
void WMLButtonDown(RTMessage Msg)= [WM_FIRST + WM_LBUTTONDOWN];  
void WMRButtonDown(RTMessage Msg)= [WM_FIRST + WM_RBUTTONDOWN];  
};  
 Nasze Główne_Okno potrafi obsługiwać następujące zdarzenia:  
* Funkcja CanClose() zwróciła wynik TRUE/FALSE,  
* Naciśnięto lewy/prawy klawisz myszki.  
Komunikat Msg zadeklarowany jako zmienna typu RTMessage jest w  klasie macieżystej TWindow 
wykorzystywany tak:  
_CLASSDEF(TWindow)  
class _EXPORT TWindow : public TWindowsObject  

 ... 
protected:  
    virtual LPSTR GetClassName()  
        { return "OWLWindow"; }  

378

background image

    virtual void GetWindowClass(WNDCLASS _FAR & AWndClass);  
    virtual void SetupWindow();    
    virtual void WMCreate(RTMessage Msg) = [WM_FIRST +  
                                                     WM_CREATE]; 
    virtual void WMMDIActivate(RTMessage Msg) =  
                 [WM_FIRST + WM_MDIACTIVATE];  
 ... 
    virtual void WMSize(RTMessage Msg) = [WM_FIRST + WM_SIZE];  
    virtual void WMMove(RTMessage Msg) = [WM_FIRST + WM_MOVE];  
    virtual void WMLButtonDown(RTMessage Msg) = [WM_FIRST +  
                                               WM_LBUTTONDOWN];  
  Zwróć uwagę na notację. Zamiast WM_CREATE pojawiło się [WM_FIRST+ WM_CREATE]. Komunikat 
WM_FIRST jest predefiniowany w OWLDEF.H i musi wystąpić w obiektowych aplikacjach w dowolnej klasie 
okienkowej, bądź sterującej (window class/controll class), która winna odpowiadać na określony komunikat. Oto 
fragment pliku  OWLDEF.H zawierający definicje stałych tej grupy:  
#define WM_FIRST        0x0000   
/* 0x0000- 0x7FFF window messages */  
#define WM_INTERNAL     0x7F00   
/* 0x7F00- 0x7FFF reserved for internal use */  
#define ID_FIRST        0x8000   
/* 0x8000- 0x8FFF child id messages */  
#define NF_FIRST        0x9000   
/* 0x9000- 0x9FFF notification messages */  
#define CM_FIRST        0xA000   
/* 0xA000- 0xFFFF command messages */  
#define WM_RESERVED             WM_INTERNAL - WM_FIRST  
#define ID_RESERVED             ID_INTERNAL - ID_FIRST  
#define ID_FIRSTMDICHILD        ID_RESERVED + 1  
#define ID_MDICLIENT            ID_RESERVED + 2  
#define CM_RESERVED             CM_INTERNAL - CM_FIRST  
  W tym momencie zwróćmy jeszcze uwagę, że funkcje z grupy  MessageHandlers są typu void i zwykle są 
metodami wirtualnymi -  przeznaczonymi "z definicji" do nadpisywania przez programistów  w klasach 
potomnych. Wszystkie te metody mają zawsze jedyny  argument - referencję do struktury TMessage 
zdefiniowanej  następująco:  
struct TMessage  
{  
  HWND Receiver;    //Identyfikator okna - odbiorcy  
  WORD Message;     //sam komunikat  
   union  
      {  
        WORD WParam;   //Parametr WParam stowarzyszony z  
                           //komunikatem; ALBO (dlatego unia!) 
        struct tagWP  
             {  
               BYTE Lo;  
               BYTE Hi;  
              } WP;  
   union  
      {  
         DWORD lParam;  
         struct tagLP  
           {  
             WORD Lo;  
             WORD Hi;  
            } LP;  
   };  
  long Result;  
}; 
 Po tych wyjaśnieniach możemy zaimplementować poszczególne  funkcje.  
void TAplikacja::InitMainWindow()  

379

background image

{  
  MainWindow = new (0, Name);  
}  
 Jeśli wybrano klawisz [Yes] funkcja zwróci IDYES. Jeśli funkcja  zwróciła IDYES - operator porównania 
zwróci TRUE (prawda) i ta  też wartość zostanie zwrócona przez metodę CanClose: 
BOOL TMyWindow::CanClose()  
{  
  return (MessageBox(HWindow, "Wychodzimy?",  
    "Koniec", MB_YESNO | MB_ICONQUESTION) == IDYES);  
}  
  Stosunkowo najciekawsza kombinacja odbywa się wewnątrz handlera  komunikatu WM_LBUTTONDOWN. 
Ze struktury komunikatów pobierana  jest zawartość młodszego słowa parametru lParam - Msg.LP.Lo i 
starszego słowa Msg.LP.Hi. Są to względne współrzędne graficzne  kursora myszki (względem narożnika okna) 
w momencie naciśnięcia  lewego klawisza myszki. Funkcja sprintf() zapisuje je w postaci  dwu liczb 
dziesiętnych %d, %d do bufora znakowego char  string[20]. Funkcja GetDC() (Get Device Context) określa 
kontekst urządzenia (warstwa sterownika urządzenia) i dalej  obiekt może już stosując funkcję kontekstową 
"czuć się"  niezależny od sprzętu. Dane te w postaci znakowej są pobierane  przez funkcję kontekstową 
OutText() jako string a równocześnie pobierane są w formie liczbowej: Msg.LP.Hi. Msg.LP.Lo, aby  wyznaczyć 
współrzędne tekstu na ekranie. Funkcja strlen()  oblicza długość łańcucha znakowego - i to już ostatni potrzebny 
nam parametr. 
void TMyWindow::WMLButtonDown(RTMessage Msg)  
{  
  HDC DC;  
  char string[20];  
  sprintf(string, "(%d, %d)", Msg.LP.Lo, Msg.LP.Hi);   <-- TU 
  DC = GetDC(HWindow);  
  TextOut(DC, Msg.LP.Lo, Msg.LP.Hi, string, strlen(string));  
/* Można zwolnić kontekst */ 
  ReleaseDC(HWindow, DC); 

 Ewentualna metoda unieważniająca prostokąt (invalid rectangle) i kasująca w ten sposób zawartość okna w 
odpowiedzi na  WM_RBUTTONDOWN może zostać zaimplementowana np. tak:  
 void TMyWindow::WMRButtonDown(RTMessage)  
{  
  InvalidateRect(HWindow, 0, 1);  
}  
 Główny program to już tylko wywołanie metody Run() wobec obiektu.  
 int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
  LPSTR lpCmdLine, int nCmdShow)  
{  
  TNAplikacja OBIEKT("Wspolrzedne w oknie", hInstance,  
                         hPrevInstance, lpCmdLine, nCmdShow);  
  OBIEKT.Run();  
  return (OBIEKT.Status);  
}  
  Wyświetlanie współrzędnych jakkolwiek wartościowe z  dydaktycznego punktu widzenia jest mało 
interesujące. Pokusimy  się o obiektową aplikację umożliwiającą odręczne rysowanie w  oknie (freehand 
drawing).  

UWAGA  

Pakiety Borland C++ 3..4.5 zawierają wiele gotowych "klocków" do wykorzystania. Oto przykład 
wykorzystania w pliku zasobów .RC  standardowego okienka wejściowego (Input Dialog Box) i  standardowego 
okienka typu Plik (File Dialog Box):  
 #include <windows.h>  
#include <owlrc.h>  
 rcinclude INPUTDIA.DLG  
rcinclude FILEDIAL.DLG  
 ROZKAZY MENU LOADONCALL MOVEABLE PURE DISCARDABLE  
BEGIN  
  POPUP "&File"  

380

background image

    BEGIN  
       MENUITEM "&New" CM_FILENEW 
       MENUITEM "&Open" CM_FILEOPEN 
       MENUITEM "&Save" CM_FILESAVE 
    END  
END 
 Takie menu można zastosować w programie obiektowym umieszcając  je w konstruktorze i dokonując 
nadpisania metody AssignMenu()  (przypisz menu):  
 TGOkno::TGOkno(PTWindowsObject AParent, LPSTR ATitle) : 
TWindow(AParent, ATitle)  
{  
  AssignMenu("ROZKAZY");  
 ...  
}   
rcinclude - dołącz zasoby  
LOADONCALL - załaduj po wywołaniu  
owlrc - zasoby biblioteki klas OWL  
 Gotowe "klocki" można wykorzystać nawet wtedy, gdy nie pasują w  100%. Inne niż typowe odpowiedzi na 
wybór rozkazu implementujemy w programie głównym poprzez nadpisanie wirtualnej metody 
 virtual void CMFileOpen(RTMessage msg) = 
[CM_FIRST + CM_FILEOPEN]  
 TGOkno GOkno;  
 void TGOkno::CMFileOpen(RTMessage)  
{  
 ... obsługa zdarzenia ...  
}  

Zadania

1. Przeanalizuj gotowe zasoby dołączone do Twojej wersji Borland C++.  
2. Uruchom kilka projektów "firmowych" dołączonych w katalogu  \EXAMPLES. Zwróć szczególną uwagę na 
projekty STEPS (kolejne  kroki w tworzeniu aplikacji obiektowej).  

LEKCJA 47: APLIKACJA OBIEKTOWA - RYSOWANIE W OKNIE.  

________________________________________________________________ 
W trakcie tej lekcji opracujemy obiektową aplikację psoługując  
się biblioteką klas Object Windows Library.  
________________________________________________________________ 
 Zaczniemy oczywiście od standardowych "klocków". Definicja klasy  Nasza_Aplikacja i moduł prezentacyjno - 
uruchomieniowy będą  wyglądać standardowo, nie musimy im zatem poświęcać zbytniej  uwagi. Przytoczymy je 
jedynie. Pointer do napisu inicjujemy po  to, by okienko komunikatu zawierało jakąś bardziej konkretną 
informację dla użytkownika. Rysunki z wnętrza tej aplikacji  można przy pomocy Schowka przenieść jako pliki 
.CLP, bądź za  pomocą PAINTBRUSH - jako .BMP, .PCX i drukować. 

#include <owl.h>  
LPSTR Ptr = "Jesli chcesz zapamietac rysunek, \  
     powinienes przeniesc go do Clipboard'u \  
     klawiszami [Print Screen] \  
     lub [Alt]+[PrtScr].";  
class TNAplikacja : public TApplication  
{  
public:  
  TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE  
hPrevInstance,  
    LPSTR lpCmdLine, int nCmdShow)  
    : TApplication(AName, hInstance, hPrevInstance, lpCmdLine,  
nCmdShow) {};  
  virtual void InitMainWindow();  
};  
 ... 

381

background image

int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpCmdLine, int nCmdShow)  
{  
TNAplikacja OBIEKT("Rysownik. Prawy klawisz umozliwia wyjscie.", 
       hInstance, hPrevInstance, lpCmdLine, nCmdShow);  
  OBIEKT.Run();  
  return (OBIEKT.Status);  
}  
  Nic specjalnie ciekawego nie dzieje się w funkcji inicjującej  główne okno, ani w funkcji zamykającej 
aplikację. Zmieniły się  tylko napisy w okienku komunikatów. 
void TNAplikacja::InitMainWindow()  
{  
  MainWindow = new TGOkno(0, Name);  
}   
BOOL TGOkno::CanClose()  
{  
  return (MessageBox(HWindow, Ptr, "KONIEC",  
  MB_YESNO | MB_ICONQUESTION) == IDYES);  
}  
 Zajmiemy się teraz główną "maszynerią" programu. Rozbudujemy  obsługę komunikatów przez handlery 
zaimplenmentowane w klasie  Główne_Okno. 
_CLASSDEF(TGOkno)  
class TGOkno : public TWindow  
{  
public:  
  HDC dc;  
  BOOL ButtonDown;  
  BOOL Flaga_Start;  
  TGOkno(PTWindowsObject AParent, LPSTR ATitle);  
//Konstruktor  
  virtual void WMLButtonDown(RTMessage Msg)  
                             = [WM_FIRST + WM_LBUTTONDOWN];  
  virtual void WMLButtonUp(RTMessage Msg)  
                             = [WM_FIRST + WM_LBUTTONUP];  
  virtual void WMMouseMove(RTMessage Msg)  
                             = [WM_FIRST + WM_MOUSEMOVE];  
  virtual void WMRButtonDown(RTMessage Msg)  
                             = [WM_FIRST + WM_RBUTTONDOWN];  
  virtual BOOL CanClose();  
};  
Konstruktor przekazuje parametry do konstruktora klasy bazowej i zeruje flagę ButtonDown - lewy klawisz 
myszki przyciśnięty.  
TGOkno::TGOkno(PTWindowsObject AParent, LPSTR ATitle)  
  : TWindow(AParent, ATitle)  
{  
  ButtonDown = FALSE;  
}  
Funkcja obsługująca zdarzenie WM_LBUTTONDOWN jeden raz inicjuje  obsługę myszki i ustawia flagę. 
Funkcje SetCapture() i GetDC()  załatwiją problem relacji kontekstowych i określają obszar  roboczy (client 
area). Jeśli umieścimy te funkcje w  konstruktorze za obszar client area uznany zostanie cały ekran.  Po 
zadziałaniu tych funkcji komunikaty od myszki będą dotyczyć  wyłącznie obszaru roboczego. Do naciśnięcia 
prawego klawisza nie będzie dostępu do "ramki" okna.  
void TGOkno::WMLButtonDown(RTMessage Msg)  
{  
 if (!Flaga_Start)  
  {  
    Flaga_Start = TRUE;     //UWAGA:  
    SetCapture(HWindow);    //Jesli zainicjujemy SetCapture()  
    dc = GetDC(HWindow);    //w konstruktorze - mamy caly ekran  
  }  

382

background image

    MoveTo(dc, Msg.LP.Lo, Msg.LP.Hi);  
    ButtonDown = TRUE;  
}  
  Funkcja MoweTo() powoduje przesunięcie kursora graficznego do  aktualnej pozycji myszki (już względnej - z 
uwzględnieniem dc)  bez rysowania linii. Flaga ButtnDown została ustawiona.  Rysowanie scedujemy na metodę 
obsługującą WM_MOUSEMOVE -  przesunięcie myszki. 
void TGOkno::WMMouseMove(RTMessage Msg)  
{  
  if (ButtonDown)  
    LineTo(dc, Msg.LP.Lo, Msg.LP.Hi);  
}  
  Jeśli lewy klawisz jest naciśnięty - funkcja LineTo() będzie  kreślić linię do kolejnych punktów "śledząc" ruch 
myszki. Jeśli  użytkownik puści lewy klawisz - zerujemy flagę stanu klawisza  
ButtonDown <== FALSE. 
void TGOkno::WMLButtonUp(RTMessage)  
{  
  if (ButtonDown) ButtonDown = FALSE;  
}  
  Jak już nabazgrzemy po ekranie, prawy klawisz umożliwi nam  skasowanie zawartości przy pomocy 
InvalidateRect(). 
 void TGOkno::WMRButtonDown(RTMessage)  
{  
  InvalidateRect(HWindow, 0, 1);  
   ReleaseCapture();  
   ReleaseDC(HWindow, dc);  
   Flaga_Start = FALSE;  
}  
 Para funkcji ReleaseDC() i ReleaseCapture() pozwala przekazać  komunikaty od myszki do "ramki okna". 
Dzięki temu można po  skasowaniu ekranu np. rozwinąć menu systemowe i zakończyć  aplikację. A oto program 
w całości.  

Listing. Odręczne rysowanie.  

#define STRICT  
#define WIN31  
#include <owl.h>  
  LPSTR Ptr = "Jesli chcesz zapamietac rysunek, \  
     powinienes przeniesc go do Clipboard'u \  
     klawiszami [Print Screen] \  
     lub [Alt]+[PrtScr].";  
class TNAplikacja : public TApplication  
{  
public:  
  TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE  
hPrevInstance,  
    LPSTR lpCmdLine, int nCmdShow)  
    : TApplication(AName, hInstance, hPrevInstance, lpCmdLine,  
nCmdShow) {};  
  virtual void InitMainWindow();  
};  
_CLASSDEF(TMyWindow)  
class TMyWindow : public TWindow  
{  
public:  
  HDC dc;  
  BOOL ButtonDown;  
  BOOL Flaga_Start;  
  TMyWindow(PTWindowsObject AParent, LPSTR ATitle);  
//Konstruktor  
  virtual void WMLButtonDown(RTMessage Msg)  
= [WM_FIRST + WM_LBUTTONDOWN];  

383

background image

  virtual void WMLButtonUp(RTMessage Msg)  
= [WM_FIRST + WM_LBUTTONUP];  
  virtual void WMMouseMove(RTMessage Msg)  
= [WM_FIRST + WM_MOUSEMOVE];  
  virtual void WMRButtonDown(RTMessage Msg)  
= [WM_FIRST + WM_RBUTTONDOWN];  
  virtual BOOL CanClose();  
};  
TMyWindow::TMyWindow(PTWindowsObject AParent, LPSTR ATitle)  
  : TWindow(AParent, ATitle)  
{  
  ButtonDown = FALSE;  
}  
void TMyWindow::WMLButtonDown(RTMessage Msg)  
{  
 if ( !Flaga_Start )  
  {  
    Flaga_Start = TRUE;     //UWAGA:  
    SetCapture(HWindow);    //Jesli zainicjujemy SetCapture()  
    dc = GetDC(HWindow);    //w konstruktorze - mamy caly ekran  
  }  
    MoveTo(dc, Msg.LP.Lo, Msg.LP.Hi);  
    ButtonDown = TRUE;  
}  
void TMyWindow::WMMouseMove(RTMessage Msg)  
{  
  if ( ButtonDown )  
    LineTo(dc, Msg.LP.Lo, Msg.LP.Hi);  
}  
void TMyWindow::WMLButtonUp(RTMessage)  
{  
  if (ButtonDown) ButtonDown = FALSE;  
}  
void TMyWindow::WMRButtonDown(RTMessage)  
{  
  InvalidateRect(HWindow, NULL, TRUE);  
   ReleaseCapture();  
   ReleaseDC(HWindow, dc);  
   Flaga_Start = FALSE;  
}  
void TNAplikacja::InitMainWindow()  
{  
  MainWindow = new TMyWindow(0, Name);  
}  
BOOL TMyWindow::CanClose()  
{  
  return (MessageBox(HWindow, Ptr, "KONIEC",  
  MB_YESNO | MB_ICONQUESTION) == IDYES);  
}  
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,  
   LPSTR lpCmdLine, int nCmdShow)  
{  
  TNAplikacja OBIEKT("Rysownik. Prawy klawisz umozliwia  
                         wyjscie.", hInstance, hPrevInstance,  
     lpCmdLine, nCmdShow);  
  OBIEKT.Run();  
  return (OBIEKT.Status);  
}  

384

background image

LEKCJA 48: O PAKIETACH BORLAND C++ 4/4.5.  

________________________________________________________________ 
Z tej lekcji dowiesz się, czy warto kupić nowszą wersję Borland  C++ 4/4.5 i jakie niespodzianki czekają Cię po 
zamianie  kompilatora na nowszy.  
________________________________________________________________ 

Czy warto sprawić sobie BORLAND C++ 4/4.5 ?   

 Kilka słów o tym, co oferuje Borland w pakietach "Borland C++  4/4.5" i jakie niespodzianki czekają nowych 
użytkowników przy  instalacji i uruchamianiu. 

Wymagania sprzętowe i instalacja  

  Aby instalacja i użytkowanie pakietu przebiegało poprawnie,  zaleca się następującą konfigurację sprzętu:  

Wymagania sprzętowe Borland C++ 4. 

________________________________________________________________ 
 Parametr             minimum           zalecane (pełna konfig.) 
________________________________________________________________ 
* procesor            80386/33 MHZ      486 DX (lub 386 + 387) 
* miejsce na dysku    8 MB              80 MB (bez kompresji) 
* pamięć RAM          4 MB              8 MB i więcej 
* system              DOS 4.01          DOS 6.0...6.22  
* MS Windows          3.1               Windows NT  
________________________________________________________________ 

Częściowa instalacja Borland C++ 4.  

________________________________________________________________ 
 
  Konfiguracja                               Dysk  
________________________________________________________________ 
1. Kompilator BCC 16 bitowy (D+W)             9 MB  
2. Kompilator BCC 32 bitowy (D+W)            13 MB  
3. Środowisko IDE 16 bitowe                  26 MB  
4. Środowisko IDE 32 bitowe                  30 MB  
5. Tylko dla DOS (minimum)                    8 MB  
_______________________________________________________________ 

D+W - dla DOS i Windows  

 Można próbować zainstalować Borland C++ 4 na małym dysku, można  także ograniczyć się do 4 MB RAM, 
ale generowanie 32-bitowych  aplikacji będzie wtedy znacznie utrudnione a praca kompilatora  wolniejsza. W 
przypadku stosowania kompresorów (np. SUPERSTOR,  DOUBLE SPACE) należy pamiętać, że wtórna 
kompresja plików jest  mało skuteczna i dysk zgłaszany jako 80 MB może okazać się  "ciasny".  Borland C++ 4 
można instalować z dyskietek, bądź z CD-ROM.  Ponieważ pakiet BC++ 4 jest "okienkowo - zorientowany", 
nawet  program instalacyjny wymaga obecności Windows. Uruchomienie  programu instalacyjnego następuje 
zatem z poziomu Menedżera  programów rozkazem File | Run... (w spolszczonej wersji Windows  - Plik | 
Uruchom...) lub z DOS-owskiego wiersza rozkazu:  
 
C:\>WIN X:INSTALL  
 Opcji jest trochę więcej - o najciekawszych z nich - kilka słów  poniżej. Warto zwrócić uwagę na tzw. 
"rozszerzenie dla Windows"  (extention to MS Windows) - Win32s. W programie INSTALL.EXE do 
zainstalowania tego pakietu (pakiet jest oryginalnym produktem  Microsofta i wymaga 8 MB przestrzeni 
dyskowej) służy opcja  [Install Win32s]. Najważniejszy plik-driver instaluje się w  pliku SYSTEM.INI:  
 
device=X:\WINDOWS\SYSTEM\WIN32S\W32S.386 
 Pozwala to na uruchamianie 32 - bitowych aplikacji pod Windows  3.1. Jeśli masz Windows NT - jest to zbędne 
- o ten "drobiazg"  zadbał już Microsoft.  W przypadku instalacji w sieci, gdzie Windows zainstalowane są  na 
serwerze należy pamiętać, że BC++ 4 w trakcie instalacji  modyfikuje nie tylko klasyczne parametry systemu:  
 

385

background image

FILES=40  
BUFFERS=40  
PATH=...;X:\BC4\BIN;  
 
ale także pliki konfiguracyjne i inicjacyjne w katalogu WINDOWS: 
 WIN.INI, PROGMAN.INI, SYSTEM.INI,  
 oraz tworzy nowe własne pliki, które będzie próbował zapisać w  katalogach \WINDOWS i 
\WINDOWS\SYSTEM, np. BCW.INI, TDW.INI, HELP.ICO, OWL.INI, BWCC.DLL, itp. (łącznie 18 nowych 
plików). Brak prawa zapisu na dysk serwera może uniemożliwić poprawną instalację i skonfigurowanie BC++ 
4/4.5 w sieci. Borland wraz z wersjami bibliotek dostarcza komplet kodów  źródłowych. Jeśli chcesz - możesz 
sam wygenerować sobie całą  bibliotekę, jeśli chcesz - możesz na własne oczy przekonać się  jak to wszystko 
działa i jak jest zbudowane. Oprócz  teoretycznych możliwości poznawczych daje to praktyczną  możliwość 
dostosowania bibliotek do nowej wersji kompilatora, co w przypadku "czwórki" może okazać się dla wielu 
użytkowników bardzo przydatne (o czy dokładniej za chwilę). Oprócz klasycznego paska głównego menu 
zintegrowane środowisko  (IDE) zostało wyposażone w rozbudowaną listwę narzędziową.  

W skład pakietu wchodzą między innymi:   

  * BCW - zintegrowane środowisko (IDE) dla środowiska Windows  
* TDW - Turbo Debugger for Windows   
* BCC - kompilator uruchamiany z DOS'owskiego wiersza rozkazu   
* BCC32 - kompilator 32 - bitowy (odpowiednik BCC)   
* BRCC - kompilator zasobów do kompilacji plików 
*.RC z zasobami do postaci binarnej *.RES   
* RLINK - konsolidator służący do dołączania plików z zasobami  przy tworzeniu plików wykonywalnych 
*.EXE   
* TLINK - "zwykły" konsolidator   
* MAKE - program narzędziowy do automatyzacji kompilacji i konsolidacji, korzystający z tzw. Plików 
instruktażowych  (emulujący NMAKE Microsofta)   
* WINSIGHT - przeglądanie informacji o okienkach (dla Windows) i komunikatach   
* TDUMP - bezpośrednie przeglądanie informacji zawartych w  plikach *.EXE i *.OBJ   
* TDSTRIP - narzędzie do usuwania tablicy symboli z plików wykonywalnych   
* IMPLIB - importer bibliotek z DLL   
* TDMEM - wyświetlanie informacji o zajętości pamięci  
* MAKESWAP - zarządzanie swapowaniem (tworzenie plików tymczasowych EDPMI.SWP o zadanej 
wielkości) i jeszcze parę narzędzi (np. tradycyjny bibliotekarz TLIB, TOUCH, GREP, itp.), o których tu nie 
wspominam.    

Czego robić nie należy? 

Przede wszystkim nie należy traktować Borland C++ 4/4.5 jako "upgrade" do wcześniejszych wersji (3, czy 3.1). 
W kompilatorze  dokonano sporych zmian (np. inaczej działa operator new). Nie wolno zatem "nadpisać" 
zawartości poprzednich katalogów i plików o tych samych nazwach. Szczególnie dotyczy to plików 
konfiguracyjnych BCCONFIG.BCW i TDCONFIG.TDW. Jeśli stare wersje tych plików nie zostaną 
przemianowane, bądź usunięte z pola  widzenia (PATH) - pojawią się konflikty przy uruchamianiu BC++.  Ze 
względu na wprowadzone zmiany pliki .OBJ tworzone przez  wcześniejsze kompilatory C będą w zasadzie 
przenośne, natomiast  pliki .OBJ i biblioteki utworzone przez wcześniejsze wersje  kompilatorów C++ 
(szczególnie Borland C++ 3.1) będą sprawiać  kłopoty (nie będą np. poprawnie wywoływane destruktory). Przy 
konsolidacji "starych" plików można stosować opcję -K2  konsolidatora, co pozwoli zmniejszyć do minimum 
ryzyko konfliktów. 

Jeśli jest już Borland Pascal 7... 

 Jeśli masz już zainstalowany Borland Pascal 7 należy pamiętać,  że poprawna praca obu kompilatorów w 
jednym systemie wymaga  "uregulowania stosunków":  
 1. Każdy kompilator musi mieć własną kopię debuggera TDW. Aby  uniknąć konfliktu pascalowski debugger 
można przemianować np.:  
TDW.EXE  -->  PASTDW.EXE  
 2. Należy usunąć stare pliki inicjujące TDW.INI. Można tu  
posłużyć się narzędziem TDWINI.EXE.  
3. Należy sprawdzić poprawność instalacji driverów w pliku  
SYSTEM.INI:  

386

background image

 
DEVICE=X:\BC4\BIN\WINDPMI.386  
DEVICE=X:\BC4\BIN\TDDEBUG.386  <-- tu możliwy konflikt z BP 7  
 
Należy usunąć dublujące się instalacje pozostawiając tylko te z BC++ 4 oraz usunąć pascalowskie 
TDDEBUG.386 (pas) i TDWIN.DLL by uniemożliwić omyłkowe zainstalowanie. Przy poprawnym 
skonfigurowaniu systemu pozostałe zasoby ( w tym  np. Resource Workshop 4) będą poprawnie współpracować 
z BP 7.  Stare zasoby C++  

Zapewne większość użytkowników Borland C++ 4 "przesiądzie się" z 

 BC++ 3/3.1 lub Turbo C++. I tu także czychają pewne  niebezpieczeństwa. Stare projekty - tradycyjnie .PRJ w 
BC++ 4  zyskują nowe domyślne rozszerzenie .IDE. W okienku dialogowym  zarządzania projektem: Project | 
Open... przy pomocy opcji  
 [3.1 Project Files (*.prj)]  
 można dokonać automatycznej konwersji do formatu .IDE, przy czym 
 stara wersja pliku *.PRJ pozostanie bez zmian. Niektóre stare  kody źródłowe będą wymagać drobnych 
modyfikacji. Szczególnie  należy zwróćić uwagę na:  
 - nakładki (overlay support)  
- zarządzanie pamięcią (new - delete)  
- informacje diagnostyczne w plikach (debug info)  
- zmianne pseudorejestrowe (dostępne teraz tylko w niektórych  trybach)  

O bibliotece Turbo Vision.  

 Biblioteka Turbo Vision Library - TV.LIB współpracująca  poprawnie z BC++ 3.0/3.1 powinna zostać 
powtórnie skompilowana,  ponieważ BC++ 4 stosuje inny format: 
-      informacji diagnostycznych (debug info format)  
-

inną długość identyfikatorów (symbol length)  

-

inną bibliotekę Runtime Library  

Kod żródłowy biblioteki znajduje się w katalogu:  

 \BIN\TVISION\SOURCE  
 Po (Uwaga!) wprowadzeniu kilku niewielkich zmian  
- do plików żródłowych .CPP  
- do pliku instruktażowego MAKEFILE  
 oraz po skompilowaniu przy pomocy BCC 4 w DWU WERSJACH: TVO.LIB  (z nakładką - Overlay) i 
TVNO.LIB (bez nakładki - No Overlay)  biblioteka TVL może być nadal z powodzeniem stosowana z Borland 
C++ 4. Podobnie rekompilacji wymaga bibiloteka klas dołączona w  wersji żródłowej w katalogu 
X:\BC4\SOURCE\CLASSLIB. 

O AUTOMATYZACJI - CASE.  

 Prócz znanego już od dość dawna (w komputerologii kilka lat to  cała epoka) tradycyjnego narzędzia Resource 
Worshop, w wersji  BC4 występują jeszcze inne narzędzia CASE kategorii "wizard" (kreator aplikacji): 
- ClassExpert  
- ApplicationExpert  
- DialogExpert  
- TargetExpert  
 Nazwa TargetExpert pochodzi od ang. "Target platform" - docelowa  platforma pracy aplikacji (DOS, Win16, 
Win32).  Biblioteka OWL 2.0 została wzbogacona o dodatkowe klasy VBX  umożliwiające współpracę z Visual 
Basic i wykorzystanie  elementów utworzonych przy pomocy VB.  Wspomaganie tworzenie programu przy 
pomocy tych narzędzi  (AppExpert podobnie jak inne narzędzie typu Wizard jest  automatycznym generatorem 
aplikacji) wymaga od użytkownika  wyboru z listy "zagadnienia" a z okienek docelowych cech  programu. 
Przytoczę tu dla przykładu listę opcji z pojedynczego  okienka AppExperta z krótkim wyjaśnieniem: 
________________________________________________________________ 
Topics:                         (okienko z listą: Zagadnienia)  
Application                   (program docelowy)  
-- Basic Opttions               (wybór opcji podstawowych) 
-- Advanced Options             (opcje zaawansowane)  
-- Code Gen Control             (sposób generacji kodu)  

387

background image

-- Admin Options                (opcje "administracyjne")  
Main Window                   (główne okno programu)  
-- Basic Options                (podstawowe opcje)  
-- SDI Client                   (interf. jednego dokumentu)  
-- MDI Client                   (interf. wielu dokumentów)  
MDI Child/View                 (okna potomne, widok/edycja)  
-- Basic Options                 (opcje podstawowe)  
 Model:                               (Szkielet programu) 
[X] Multiple document interface    - interfejs MDI  
[ ] Single document interface      - interfejs SDI  
 
Features:                        (cechy) 
[.] SpeedBar                     (ma pasek narzędzi)  
[.] Status line                  (ma wiersz statusowy)  
[.] Drag/drop                    (obsługuje ciągnij/upuść)  
[.] Printing                     (obsługuje drukarkę)  
________________________________________________________________ 
 Po wybraniu w okienku klawisza [Generate] (wygeneruj) AppExpert  generuje szkielet programu aplikacji o 
podanych własnościach.  Wygenerowane zostaje od sześciu do dziewięciu (zależnie od  ustawienia opcji i 
Twoich życzeń) plików projektu:  
*.IDE                  - plik projektu (lub .PRJ) 
*.APX                  - plik roboczy AppExpert'a (odpowiednik .PRJ) 
*.RC                   - plik zasobów  
*.RH                   - plik nagłówkowy zasobów  
*.H                    - plik nagłówkowy, źródłowy  
*.CPP                  - moduł główny źródłowy  
*.HPJ                  - plik pomocy 
*.RTF                  - źródłowy pomocy kontekstowej  
*.ICO                  - ikonka projektu  
 Przy pomocy rozkazu Generate makefile można również  automatycznie utworzyć plik instruktażowy 
MAKEFILE dla  generatora MAKE.EXE.  
 Uzyskany plik szkieletowy *.CPP należy tylko uzupełnić o obsługę  interesujących nas zdarzeń/komunikatów. 
Przyspiesza to znacznie tworzenie typowych aplikacji. 

Programiści wszystkich krajów... 

 BC++ 4 zawiera bibliotekę LOCALE.DLL umożliwiającą obsługę angielsko- francusko- i niemiecko- języczną. 
Borland zapowiada, że następne wersje będą coraz bogatsze. Doczekaliśmy się spolszczenia Windows - może i 
Borland C++ po polsku już tuż tuż? Póki co, najwygodniej podmienić czcionki.  
________________________________________________________________ 

ZAKOŃCZENIE. 

 I to już niestety koniec. Po przeanalizowaniu historii: programowania sekwencyjnego i strukturalnego oraz 
nowoczesnych styli programowania: obiektowego i zdarzeniowego pozostał Ci już tylko wykonanie trzech 
rzeczy. Powinieneś teraz: 
1. Pisać własne aplikacje 
2. Pisać własne aplikacje 
3. Pisać własne aplikacje 
 Tak naprawdę - jest to jedyny sposób, by zostać dobrym programistą. Przez pewien czas okaże Ci się zapewne 
przydatna dyskietka dołączona do książki. Znajdziesz tam jeszcze sporo programów przykładowych, które nie 
zmieściły się w książce. Przyjemnej pracy z programem MEDYT. 

388


Document Outline