Programowanie w C++ Borland Builder - Wyższa SzkoÅa Informatyki ...
Programowanie w C++ Borland Builder - Wyższa SzkoÅa Informatyki ...
Programowanie w C++ Borland Builder - Wyższa SzkoÅa Informatyki ...
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
<strong>Programowanie</strong><br />
w C i <strong>C++</strong><br />
BORLAND <strong>C++</strong><br />
- 1-
Spis Treści<br />
Lekcja 1. Co o C i <strong>C++</strong> każdy wiedzieć powinien. 3<br />
Lekcja 2. Jak korzystać z kompilatora <strong>Borland</strong> <strong>C++</strong>? 8<br />
Lekcja 3. Główne menu i inne elementy IDE. 11<br />
Lekcja 4. Jeszcze o IDE <strong>C++</strong> . 18<br />
Lekcja 5. Działania przy pomocy myszki i błędy w programie. 24<br />
Lekcja 6. Następny program - komputerowa arytmetyka. 29<br />
Lekcja 7. Z czego składa się program. 33<br />
Lekcja 8. Jakich słów kluczowych używa <strong>C++</strong>. 42<br />
Lekcja 9. O sposobach odwoływania się do danych. 51<br />
Lekcja 10. Jakie operatory stosuje <strong>C++</strong>. 59<br />
Lekcja 11. Jak deklarować zmienne. Co to jest wskaźnik. 65<br />
Lekcja 12. Wskaźniki i tablice w C i <strong>C++</strong>. 74<br />
Lekcja 13. Jak tworzyć w programie pętle i rozgałęzienia. 82<br />
Lekcja 14. Jak tworzyć i stosować struktury. 90<br />
Lekcja 15. Jak posługiwać się funkcjami. 97<br />
Lekcja 16. Asembler tasm i basm. 108<br />
Lekcja 17. Trochę szczególów technicznych. 115<br />
Lekcja 18. O łańcuchach tekstowych 123<br />
Lekcja 19. Kilka innych przydatnych funkcji. 131<br />
Lekcja 20. Jeśli program powinien uruchomić inny program... 135<br />
Lekcja 21. Kilka procesów jednocześnie. 141<br />
Lekcja 22. Na zdrowy chłopski rozum programisty. 148<br />
Lekcja 23. Co nowego w <strong>C++</strong>? 153<br />
Lekcja 24. Skąd wzięły się klasy i obiekty w c++. 164<br />
Lekcja 25. Przykład obiektu. 171<br />
Lekcja 26. Co to jest konstruktor. 176<br />
Lekcja 27. O dziedziczeniu. 185<br />
Lekcja 28. Dziedziczenie złożone. 188<br />
Lekcja 29. Funkcje i overloading. 193<br />
Lekcja 30. Wymiana danych między obiektami. 197<br />
Lekcja 31. Przekazanie obiektów jako argumentów do funkcji. 205<br />
Lekcja 32. Wskaźniki do obiektów. 210<br />
Lekcja 33. Overloading operatorów. 212<br />
Lekcja 34. O zastosowaniu dziedziczenia. 222<br />
Lekcja 35. Funkcje wirtualne i klasy abstrakcyjne. 227<br />
Lekcja 36. Kaźdy dysk jest za mały, a kaźdy procesor zbyt wolny. 232<br />
Lekcja 37. O <strong>C++</strong>, Windows i małym Chińczyku. 236<br />
Lekcja 38. Korzystamy ze standardowych zasobów Windows. 242<br />
Lekcja 39. Struktura programu proceduralno - zdarzeniowego 248<br />
Lekcja 40. Jak tworzy się aplikację dla Windows? 254<br />
Lekcja 41. Kompilatory "specjalnie dla Windows". 259<br />
Lekcja 42. Elementy sterujące i zarządzanie programem. 265<br />
Lekcja 43. O okienkach dialogowych. 272<br />
Lekcja 44. Dołączanie zasobów - menu i okienka dialogowe. 275<br />
Lekcja 45. O programach obiektowo - zdarzeniowych. 280<br />
Lekcja 46. Aplikacja obiektowa - rysowanie w oknie. 290<br />
Lekcja 47. O pakietach <strong>Borland</strong> <strong>C++</strong> 4/4.5. 294<br />
Plik zostal sciagniety ze strony BINBOY'a<br />
http://www.binboy.w.pl<br />
mail: karol@binboy.w<br />
http://binboy.koti.com.pl<br />
binboy@binboy.koti.com<br />
- 2-
Lekcja 1. Co o C i <strong>C++</strong> każdy wiedzieć powinien.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, dlaczego pora na <strong>C++</strong>.<br />
________________________________________________________________<br />
Język <strong>C++</strong> jest uniwersalnym, nowoczesnym językiem programowania.<br />
Stosowane przez USA i inne kraje wobec Polski wieloletnie<br />
embargo COCOM'u (przeszkody w dostępie do nowoczesnej<br />
technologii) sprawiły m. in., że popularność OS2, UNIXa i C/<strong>C++</strong><br />
jest w Polsce do dziś nieproporcjonalnie mała, a Basica, Pascala<br />
i DOSa nieproporcjonalnie duża. W USA <strong>C++</strong> już od kilku lat<br />
stanowi podstawowe narzędzie programistów.<br />
Już słyszę oburzenie (A co mnie obchodzi historia<br />
"komputerologii" i koligacyjki!). Otóż obchodzi, bo wynikają z<br />
niej pewne "grzechy pierworodne" języka C/<strong>C++</strong>, a dla Ciebie,<br />
szanowny Czytelniku - pewne wnioski praktyczne.<br />
Grzech Pierwszy:<br />
* Kompilator języka C/<strong>C++</strong> jest standardowym wyposażeniem systemu<br />
operacyjnego UNIX.<br />
Skutki praktyczne:<br />
Każdy PC jest w momencie zakupu (co często wchodzi w cenę zakupu<br />
komputera) wyposażany w system operacyjny DOS - np. DR DOS, PC<br />
DOS, PTS DOS lub MS DOS. Standardowo w zestaw systemu MS DOS<br />
wchodzi interpreter języka BASIC (w MS-DOS - QBasic.EXE). Możesz<br />
więc być pewien, że jeśli jest DOS, to musi być i BASIC.<br />
Podobnie rzecz ma się z C/<strong>C++</strong>. Jeśli jest na komputerze system<br />
UNIX (za wyjątkiem najuboższych wersji systemu XENIX), masz tam<br />
do dyspozycji kompilator C/<strong>C++</strong>, za to BASICA ani Pascala prawie<br />
na pewno tam nie ma. Podobnie coraz popularniejszy OS/2<br />
wyposażony jest w kompilator (całkiem niezły) <strong>C++</strong> i dodatkowo<br />
jeszcze w pewne gotowe-firmowe biblioteki.<br />
Grzech drugi:<br />
* Język C/<strong>C++</strong> powstał jeszcze zanim wymyślono PC, DOS, GUI<br />
(Graficzny Interfejs Użytkownika), Windows i inne tym podobne.<br />
Dwa najważniejsze skutki praktyczne:<br />
I. W założeniach twórców język <strong>C++</strong> miał być szybki (i jest) i<br />
zajmować mało miejsca w pamięci (bo ówczesne komputery miały jej<br />
bardzo mało!). Zawiera więc różne, niezrozumiałe dla nas z<br />
dzisiejszego punktu widzenia skróty. Np. to co w Pascalu czy<br />
Basicu wygląda zrozumiale:<br />
i:=i+1;<br />
(Pascal)<br />
10 I=I+1 lub inaczej NEXT I (Basic)<br />
- 3-
to w języku <strong>C++</strong> wygląda dziwacznie:<br />
i++; albo jeszcze dziwniej ++i;<br />
Tym niemniej zwróć uwagę, że w Pascalu zajmuje to 7 znaków, w<br />
Basicu - 8 znaków (spacja to też znak!), a w <strong>C++</strong> tylko 4.<br />
Inny przykład:<br />
X=X+5<br />
X:=X+5<br />
X+=5<br />
(Basic, 5 znaków),<br />
(Pascal, 6 znaków),<br />
(<strong>C++</strong>, tylko 4 znaki).<br />
Z takiej właśnie filozofii wynika i sama nazwa - najkrótsza z<br />
możliwych. Jeśli bowiem i++ miało znaczyć mniej więcej tyle samo<br />
co NEXT I (następne I) to <strong>C++</strong> znaczy mniej więcej tyle samo co<br />
"NASTĘPNA WERSJA C".<br />
II. Nie ma nic za darmo. W języku C/<strong>C++</strong>, podobnie jak w<br />
samochodzie wyścigowym formuły I, za szybkość i skuteczność<br />
płaci się komfortem. Konstrukcje stosowane w języku C/<strong>C++</strong> są<br />
maksymalnie dostosowane do "wygody" komputera. Pozwala to na<br />
uzyskiwanie ˙niezwykle szybkich "maszynowo-zorientowanych" kodów<br />
wykonywalnych programu, ale od programisty wymaga<br />
przyzwyczajenia się do "komputerowo-zorientowanego sposobu<br />
myślenia".<br />
Grzech Trzeci (i chyba najcięższy):<br />
* Jest najlepszy. Ostrożniej - jest najchętniej stosowanym<br />
narzędziem profesjonalnych programistów.<br />
Ma oczywiście konkurentów. Visual Basic (do małych aplikacji<br />
okienkowych), Turbo Pascal (do nauki podstaw - elementów<br />
programowania sekwencyjnego, proceduralno-strukturalnego),<br />
QuickBasic (programowanie strukturalne w środowisku DOS),<br />
Fortran 90, ADA, SmallTalk, itp, itd.<br />
Sam wielki Peter Norton przyznaje, że początkowe wersje swojego<br />
słynnego pakietu Norton Utilities pisał w Pascalu, ale dopiero<br />
przesiadka na C/<strong>C++</strong> pozwoliła mu doprowadzić NU do dzisiejszej<br />
doskonałości. Jakie są programy Petera Nortona - każdy widzi...<br />
Zapewne masz na swoim komputerze kilka różnych aplikacji (np.<br />
TAG, QR-Tekst, Word, itp.) - jeśli zajrzysz do nich do środka<br />
(View), możesz sam przekonać się, że większość z nich została<br />
napisana właśnie w <strong>C++</strong> (Kompilatory <strong>C++</strong> pozostawiają w kodzie<br />
wynikowym .EXE swoją wizytówkę zwykle czytelną przy pomocy<br />
przeglądarki; przekonasz się o tym także zaglądając przez [View]<br />
do własnych programów); stosowane narzędzia możesz rozpoznać<br />
także po obecności dynamicznych bibliotek - np. BWCC.DLL -<br />
biblioteka elementów sterujących - klawiszy, itp - <strong>Borland</strong><br />
Custom Controls for Windows).<br />
Skutki praktyczne:<br />
- 4-
Nauczywszy się języka C/<strong>C++</strong> możesz nie bać się ani systemu<br />
UNIX/XENIX a ich środowiska okienkowego - X Windows, ani OS2,<br />
ani Windows 95 (dotychczasowe testy starych 16-bitowych<br />
aplikacji wykazały b. wysoki stopień kompatibilności), ani<br />
stacji roboczych, ani dużych komputerów klasy mainframe. Język<br />
C/<strong>C++</strong> dosłużył się bowiem ogromnej ilości tzw. implementacji<br />
czyli swoich odmian, przeznaczonych dla różnych komputerów i dla<br />
różnych systemów operacyjnych. Windows NT i Windows 95 również<br />
zostały napisane w <strong>C++</strong>.<br />
Czytając prasę (np. Computer World, PC-Kurier i in.) zwróć<br />
uwagę, że najwięcej ofert pracy jest właśnie dla programistów<br />
posługujących się <strong>C++</strong> (i tak zapewne będzie jeszcze przez kilka<br />
lat, póki nie wymyślą czegoś lepszego - np. jakiegoś <strong>C++</strong>+).<br />
Z Grzechu Trzeciego (choć nie tylko) wynika także pośrednio<br />
Grzech Czwarty.<br />
Języka <strong>C++</strong> Grzech Czwarty - ANSI C, <strong>C++</strong>, czy Turbo <strong>C++</strong>, Visual<br />
<strong>C++</strong>, czyli mała wieża BABEL.<br />
Nie jestem pewien, czy "wieża BABEL" jest określeniem<br />
trafniejszym niż "kamień filozoficzny", bądź "perpetuum mobile".<br />
To co w ciągu ostatnich lat stało się z językiem <strong>C++</strong> ma coś<br />
wspólnego z każdym z tych utopijnych symboli. A w dużym<br />
uproszczeniu było to tak.<br />
Podobnie, jak mechanikom od zarania dziejów marzyło się<br />
perpetuum mobile, tak informatykom zawsze marzyło się stworzenie<br />
jednego SUPER-UNIWERSALNEGO języka programowania. Takiego, który<br />
byłby zupełnie niezależny od sprzętu tzn., aby program napisany<br />
w takim języku mógł być przeniesiony BEZ ŻADNYCH ZMIAN na<br />
dowolny komputer I DZIAŁAŁ. Do takiej roli pretendowały kolejno<br />
FORTRAN, Algol a potem przyszła pora na C/<strong>C++</strong>. Gdyby informatycy<br />
nie okazali się zbyt zachłanni, może coś by z tego wyszło. Ale,<br />
jak to w życiu, programiści (podobnie jak żona rybaka z bajki "O<br />
rybaku i złotej rybce") chcieli wszystkiego naraz:<br />
* żeby program dał się przenieść na komputer innego typu i<br />
działał,<br />
* żeby działał szybko i optymalnie wykorzystywał sprzęt,<br />
* żeby umiał wszystko, co w informatyce tylko wymyślono (tj. i<br />
grafika, i obiekty, i obsługa peryferii i...).<br />
I stało się. W pomyślanym jako uniwersalny języku zaczęły<br />
powstawać odmiany, dialekty, mutacje, wersje itp. itd.<br />
Jeśli <strong>C++</strong> nie jest Twoim pierwszym językiem, z pewnością<br />
zauważyłeś Czytelniku, że pomiędzy GW Basic a Quick Basic są<br />
pewne drobne różnice. Podobnie Turbo Pascal 7.0 trochę różni się<br />
od Turbo Pascala 5.0. Mimo to przykład poniżej pewnie Cię trochę<br />
zaskoczy. Dla zilustrowania skali problemu przedstawiam poniżej<br />
dwie wersje TEGO SAMEGO PROGRAMU napisanego w dwu różnych<br />
wersjach TEGO SAMEGO JĘZYKA <strong>C++</strong>. . Obydwa programy robią<br />
dokładnie to samo. Mają za zadanie wypisać na ekranie napis<br />
"Hello World" (czyli "Cześć świecie!").<br />
- 5-
Program (1)<br />
main()<br />
{<br />
printf("Hello World\n");<br />
}<br />
Program (2)<br />
#include <br />
#include <br />
LPSTR p = "Hello World\n";<br />
main(void)<br />
{<br />
cout
upodobania)<br />
* nazwy stałych<br />
(gdyby chodziło tylko o PI i e - wszystko byłoby proste)<br />
* nazy zasobów (FILE, PRN, CONSOLE, SCREEN itp. itd)<br />
(tu jest lepiej, ale też rozbieżności są zauważalne)<br />
Autor programu może jeszcze nadawać zmiennym (liczbom, zmiennym<br />
napisom, obiektom, itp.) własne nazwy, więc czasem nawet<br />
wytrawny programista ma kłopoty ze zrozumieniem tekstu<br />
żródłowego programu...<br />
W języku C a następnie <strong>C++</strong> przyjęto pewne maniery nadawania nazw<br />
- identyfikatorów ułatwiające rozpoznawanie tych grup słów:<br />
* nazwa() - funkcja<br />
* słowa kluczowe i nazwy zmiennych - małymi literami<br />
* STAŁE - nazwy stałych najczęściej dużymi literami<br />
* long/LONG - typy danych podstawowe/predefiniowane dla Windows<br />
_NAZWA - nazwy stałych predefiniowanych przez producenta<br />
__nazwa lub __nazwa__ - identyfikatory charakterystyczne dla<br />
danej wersji kompilatora<br />
itp., których to zwyczajów i ja postaram się przestrzegać w<br />
tekście książki.<br />
Amerykański Instytut Standardów ANSI od lat prowadzi walkę z<br />
wiatrakami. Stoi na straży jednolitego standardu języka, który<br />
nazywa się standardem ANSI C i ANSI <strong>C++</strong>. Wielcy producenci od<br />
czasu do czasu organizują konferencje i spotkania gdzieś w<br />
ciepłych krajach i uzgadniają niektóre standardy - czyli wspólne<br />
dla nich i zalecane dla innych normy, ale niektórzy bywają<br />
zazdrośni o własne tajemnice i nie publikują wszystkich<br />
informacji o swoich produktach. Dlatego wszelkie "słuszne i<br />
uniwersalne" standardy typu ODBC, Latin 2, Mazovia, LIM, OLE,<br />
DDE, BGI, itp., itd. mają niestety do dziś ograniczony zakres<br />
stosowalności a wszelkie zapewnienia producentów o całkowitej<br />
zgodności ich produktu z... (tu wpisać odpowiednie) należy<br />
niestety nadal traktować z pewną rezerwą.<br />
W niniejszej książce zajmiemy się kompilatorem <strong>Borland</strong> <strong>C++</strong> w<br />
jego wersjach 3.0 do 4.5, jest to bowiem najpopularniejszy w<br />
Polsce kompilator języka C/<strong>C++</strong> przeznaczony dla komputerów IBM<br />
PC. Nie bez znaczenia dla tej decyzji był także fakt, że <strong>Borland</strong><br />
<strong>C++</strong> i Turbo <strong>C++</strong> bez konfliktów współpracuje z:<br />
* Turbo Pascal i <strong>Borland</strong> Pascal;<br />
* Assemblerami: TASM, BASM i MASM;<br />
* Turbo Debuggerem i Turbo Profilerem;<br />
* bibliotekami Turbo Vision, ObjectVision, Object Windows<br />
Library, Database Tools, itp.<br />
* pakietami innych producentów - np. Win/Sys Library, Object<br />
- 7-
Professional, CA-Visual Objects, Clipper, itp.<br />
i in. produktami "ze stajni" <strong>Borland</strong>a popularnymi wśród<br />
programistów. Programy TASM/BASM, Debugger, Profiler a także<br />
niektóre biblioteki (np. Object Windows Library, Turbo Vision<br />
Library, itp. wchodzą w skład pakietów instalacyjnych BORLANDA,<br />
ale UWAGA - niestety nie wszystkich). <strong>Borland</strong> <strong>C++</strong> 4+ pozwala,<br />
dzięki obecności specjalnych klas VBX w bibliotece klas i<br />
obiektów Object Windows Library na wykorzystanie programów i<br />
zasobów tworzonych w środowisku Visual Basic'a. Podobnie<br />
kompilatory <strong>C++</strong> firmy Microsoft (szczególnie Visual <strong>C++</strong>)<br />
bezkonfliktowo współpracują z zasobami innych aplikacji - np.<br />
Access, Excel, itp..<br />
Warto tu zwrócić uwagę na jeszcze jeden czynnik, który może stać<br />
się Twoim, Czytelniku atutem. Jeśli znasz już kompilatory Turbo<br />
Pascal, bądź <strong>Borland</strong> Pascal, zwróć uwagę, że wiele funkcji<br />
zaimplementowanych w Turbo Pascal 6.0. czy 7.0 ma swoje<br />
odpowiedniki w BORLAND <strong>C++</strong> i Turbo <strong>C++</strong>. Odpowiedniki te zwykle<br />
działają dokładnie tak samo, a różnią się najczęściej<br />
nieznacznie pisownią nazwy funkcji. Wynika to z błogosławieństwa<br />
"lenistwa" (ponoć homo sapiens najwięcej wynalazków popełniał<br />
właśnie ze strachu, bądź z lenistwa...). Firmie <strong>Borland</strong> "nie<br />
chciało się" wymyślać od nowa tego, co już sprawdziło się<br />
wcześniej i do czego przyzwyczaili się klienci! I odwrotnie.<br />
Poznawszy <strong>Borland</strong>/Turbo <strong>C++</strong> z łatwością zauważysz te same<br />
funkcje w <strong>Borland</strong>/Turbo Pascalu.<br />
[!!!]UWAGA!<br />
________________________________________________________________<br />
O Kompilatorach BORLAND <strong>C++</strong> 4 i 4.5 napiszę nieco póżniej,<br />
ponieważ są bardziej skomplikowane i wymagają trochę większej<br />
znajomości zasad tworzenia i uruchamiania programów (projekty).<br />
To prawda, że zawierają narzędzia klasy CASE do automatycznego<br />
generowania aplikacji i jeszcze kilka innych ułatwień, ale miej<br />
trochę cierpliwości...<br />
________________________________________________________________<br />
[???] C.A.S.E.<br />
________________________________________________________________<br />
CASE - Computer Aided Software Engineering - inżynieria<br />
programowa wspomagana komputerowo. Najnowsze kompilatory <strong>C++</strong><br />
wyposażone są w narzędzia nowej generacji. W różnych wersjach<br />
nazywają się one AppExpert, ClassExpert, AppWizard, VBX<br />
Generator, itp. itd, które pozwalają w dużym stopniu<br />
zautomatyzować proces tworzenia aplikacji. Nie można jednak<br />
zaczynać kursu pilotażu od programowania autopilota - a kursu<br />
programowania od automatycznych generatorów aplikacji dla<br />
Windows...<br />
________________________________________________________________<br />
- 8-
Zaczynamy zatem od rzeczy najprostszych, mając jedynie tę<br />
krzepiącą świadomość, że gdy już przystąpimy do pisania<br />
aplikacji konkurencyjnej wobec Worda, QR-Tekst'a, czy Power<br />
Point'a - może nas wspomagać system wspomaganina CASE dołączony<br />
do najnowszych wersji BORLAND <strong>C++</strong> 4 i 4.5. Jeśli mamy już gotowe<br />
aplikacje w Visual Basic'u - <strong>Borland</strong> <strong>C++</strong> 4+ pozwoli nam<br />
skorzystać z elementów tych programów (ale pracować te aplikacje<br />
po przetransponowaniu do <strong>C++</strong> będą od kilku do kilkuset razy<br />
szybciej).<br />
_______________________________________________________________<br />
- 9-
LEKCJA 2. Jak korzystać z kompilatora BORLAND <strong>C++</strong>?<br />
________________________________________________________________<br />
W trakcie tej lekcji poznasz sposoby rozwiązania typowych<br />
problemów występujących przy uruchomieniu kompilatora <strong>Borland</strong><br />
<strong>C++</strong>.<br />
________________________________________________________________<br />
UWAGA:<br />
Z A N I M rozpoczniesz pracę z dyskietką dołączoną do niniejszej<br />
książki radzimy Ci SPORZĄDZIĆ ZAPASOWĄ KOPIĘ DYSKIETKI przy<br />
pomocy rozkazu DISKCOPY, np.<br />
DISKCOPY A: A: lub DISKCOPY B: B:<br />
Unikniesz dzięki temu być może wielu kłopotów, których może Ci<br />
narobić np. przypadkowy wirus lub kropelka kawy.<br />
INSTALACJA DYSKIETKI.<br />
Na dyskietce dołączonej do niniejszej książki, którą najlepiej<br />
zainstalować na dysku stałym (z dyskiem pracuje się znacznie<br />
szybciej, a prócz tego jest tam znacznie więcej miejsca), w jej<br />
katalogu głównym znajduje się programik instalacyjny o nazwie:<br />
INSTALUJ.BAT<br />
napisany jako krótki plik wsadowy w języku BPL (Batch<br />
Programming Language - język programowania wsadowego). Aby<br />
zainstalować programy z dyskietki na własnym dysku powinieneś:<br />
* sprawdzić, czy na dysku (C:, D:, H: lub innym) jest co<br />
najmniej 2 MB wolnego miejsca,<br />
* włożyć dyskietkę do napędu i wydać rozkaz:<br />
Program instalacyjny utworzy na wskazanym dysku katalog<br />
\C-BELFER<br />
i tam skopiuje całą zawartość dyskietki oraz dokona dekompresji<br />
(rozpakowania) plików. Jeśli chcesz skopiwać zawartość dyskietki<br />
do własnego katalogu roboczego, wystarczy "wskazać" programowi<br />
instalacyjnemu właściwy adres:<br />
komputerze; najlepiej zasięgnij rady lokalnego eksperta).<br />
[???] NIE CHCE USTAWIĆ ŚCIEŻKI ?<br />
________________________________________________________________<br />
Tak czasem się zdarza - zwykle wtedy, gdy pracujesz w DOS-ie z<br />
programem Norton Commander. Musisz pozbyć się "na chwilę"<br />
programu NC. Naciśnij [F10] - Quit i potwierdź przez [Y] lub<br />
[Enter]. Po ustawieniu ścieżek możesz powtórnie uruchomić NC.<br />
________________________________________________________________<br />
Albo<br />
3. Dodać do pliku AUTOEXEC.BAT dodatkową ścieżkę. Jest to<br />
wyjście najlepsze. Na końcu linii ustawiającej ścieżki - np.:<br />
PATH C:\; C:\DOS; C:\NC; C:\WINDOWS<br />
dodaj ścieżkę do kompilatora <strong>C++</strong>, np.:<br />
PATH C:\; C:\DOS; C:\NC; D:\BORLANDC\BIN;<br />
Załatwi to problem "raz na zawsze". Po uruchomieniu komputera<br />
ścieżka będzie odtąd zawsze ustawiana automatycznie.<br />
Ponieważ kompilator <strong>C++</strong> wymaga w trakcie pracy otwierania i<br />
łączenia wielu plików, różne wersje (program instalacyjny<br />
INSTALL.EXE podaje tę informację w okienku pod koniec<br />
instalacji) wymagają dodania do pliku konfiguracyjnego<br />
CONFIG.SYS wiersza:<br />
FILES = 20<br />
(dla różnych wersji wartość ta wacha się w granicach od 20 do<br />
50). Najbezpieczniej, jeśli nie masz pewności dodać 50. Jeśli<br />
wybrałeś wariant trzeci i ewentualnie zmodyfikowałeś swój<br />
CONFIG.SYS, wykonaj przeładowanie systemu [Ctrl]-[Alt]-[Del].<br />
Teraz możesz wydać rozkaz<br />
BC[Enter]<br />
Mam nadzieję, że tym razem się udało i oto jesteśmy w IDE<br />
<strong>Borland</strong> <strong>C++</strong>. Jeśli nie jesteś jedynym użytkownikiem, na ekranie<br />
rozwinie się cała kaskada okienek roboczych. Skonsultuj z<br />
właścicielem, które z nich można pozamykać a które pliki można<br />
skasować lub przenieść. Pamiętaj "primo non nocere" - przede<br />
wszystkim nie szkodzić!<br />
€[S!] IDE = Integrated Development Environment,<br />
IDE, czyli Zintegrowane Środowisko Uruchomieniowe. Bardziej<br />
prozaicznie - połączony EDYTOR i KOMPILATOR. Zapewne znasz już<br />
coś podobnego z Pascala lub Quick Basica. Od dziś będzie to<br />
Twoje środowisko pracy, w którym będziesz pisać, uruchamiać i<br />
modyfikować swoje programy.<br />
€[???] DISK FULL!<br />
- 12-
________________________________________________________________<br />
Co robić, jeśli przy próbie uruchomienia kompilator <strong>C++</strong><br />
odpowiedział Ci:<br />
Disk full! Not enough swap space.<br />
Program BC.EXE (TC.EXE) jest bardzo długi. Jeśli wydasz rozkaz<br />
(wariant 1: Turbo <strong>C++</strong> 1.0, niżej BORLAND <strong>C++</strong> 3.1):<br />
DIR TC.EXE<br />
uzyskasz odpowiedź, jak poniżej:<br />
C:>DIR TC.EXE<br />
Directory of D:\TC\BIN<br />
TC EXE 876480 05-04-90 1:00a<br />
1 file(s) 876480 bytes<br />
17658880 bytes free<br />
C:>DIR BC.EXE<br />
Directory of C:\BORLANDC\BIN<br />
BC EXE 1410992 06-10-92 3:10a<br />
1 file(s) 1410992 bytes<br />
18926976 bytes free<br />
Ponieważ plik kompilatora nie mieści się w 640 K pamięci musi<br />
dokonywać tzw. SWAPOWANIA i tworzy na dysku dodatkowy plik<br />
tymczasowy (ang. swap file). Na dysku roboczym powinno<br />
pozostawać najmniej 500 KB wolnego miejsca. Jeśli możesz,<br />
pozostaw na tym dysku wolne nie mniej niż 1 MB. Ułatwi to i<br />
przyspieszy pracę.<br />
________________________________________________________________<br />
Tworzony tymczasowo plik roboczy wygląda tak:<br />
Volume in drive D has no label<br />
Directory of D:\SIERRA<br />
TC000A SWP 262144 12-13-94 5:42p<br />
1 file(s) 262144 bytes<br />
11696320 bytes free<br />
(13-XII to dziś!)<br />
€[!!!] UWAGA:<br />
Kompilator <strong>C++</strong> będzie próbował tworzyć plik tymczasowy zawsze w<br />
bieżącym katalogu, tzn. tym, z którego wydałeś rozkaz<br />
TC lub BC.<br />
II. WNIOSKI PRAKTYCZNE.<br />
* Lepiej nie uruchamiać <strong>C++</strong> "siedząc" na dyskietce, ponieważ<br />
może mu tam zabraknąć miejsca na plik tymczasowy.<br />
* Dla użytkowników Novella: Uruchamiajcie kompilator <strong>C++</strong> tylko<br />
- 13-
we własnych katalogach - do innych możecie nie mieć praw zapisu.<br />
Plik .SWP jest tworzony tylko podczas sesji z kompilatorem <strong>C++</strong> i<br />
usuwany natychmiast po jej zakończeniu. Możesz go zobaczyć tylko<br />
wychodząc "na chwilę" do systemu DOS przy pomocy rozkazu DOS<br />
Shell (menu File).<br />
€[S!] SWAP - Zamiana.<br />
________________________________________________________________<br />
Jeśli wszystkie dane, potrzebne do pracy programu nie mieszczą<br />
się jednocześnie w pamięci operacyjnej komputera, to program -<br />
"właściciel", (lub system operacyjny - DOS, OS2, Windows) może<br />
dokonać tzw. SWAPOWANIA. Polega to na usunięciu z pamięci<br />
operacyjnej i zapisaniu na dysk zbędnej w tym momencie części<br />
danych, a na ich miejsce wpisaniu odczytanej z dysku innej<br />
części danych, zwykle takich, które są programowi pilnie<br />
potrzebne do pracy właśnie teraz.<br />
________________________________________________________________<br />
€[Z] - Propozycje zadań do samodzielnego wykonania.<br />
----------------------------------------------------------------<br />
1.1 Sprawdź ile bajtów ma plik .EXE w tej wersji kompilatora<br />
<strong>C++</strong>, której używasz.<br />
1.2. Posługując się rozkazem DOS Shell z menu File sprawdź gdzie<br />
znajduje się i jakiej jest wielkości plik tymczasowy .SWP. Ile<br />
masz wolnego miejsca na dysku ?<br />
________________________________________________________________<br />
EOF<br />
- 14-
LEKCJA 3. Główne menu i inne elementy IDE.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się jak poruszać się w<br />
zintegrowanym środowisku (IDE) Turbo <strong>C++</strong>.<br />
________________________________________________________________<br />
Najważniejszą rzeczą w środowisku IDE jest GŁÓWNE MENU (ang.<br />
MENU BAR), czyli pasek, który widzisz w górnej części ekranu.<br />
Działa to podobnie, jak główne menu w programie Norton Commander<br />
(dostępne tam przez klawisz [F9]).<br />
KRÓTKI PRZEGLĄD GŁÓWNEGO MENU.<br />
Przyciśnij klawisz [F10].<br />
Główne menu stało się aktywne. Teraz przy pomocy klawiszy<br />
kursora (ze strzałkami []) możesz poruszać się po menu i<br />
wybrać tę grupę poleceń, która jest Ci potrzebna. A oto nazwy<br />
poszczególnych grup:<br />
[S!]€GRUPY POLECEŃ - NAZWY POSZCZEGÓLNYCH "ROZWIJANYCH" MENU.<br />
= Bez nazwy (menu systemowe).<br />
File Operacje na plikach.<br />
Edit Edycja plików z tekstami źródłowymi programów.<br />
Search Przeszukiwanie.<br />
Run Uruchomienie programu.<br />
Compile Kompilacja programu.<br />
Debug "Odpluskwianie", czyli wyszukiwanie błędów w<br />
programie.<br />
Project Tworzenie dużych, wielomodułowych programów.<br />
Options Opcje, warianty IDE i kompilatora.<br />
Window Okna (te na ekranie).<br />
Help Pomoc, niestety po angielsku.<br />
UWAGA:<br />
__________________________________________________________<br />
W niektórych wersjach kompilatora na pasku głównego menu pojawi<br />
się jeszcze Browse - przeglądanie (funkcji, struktury klas i<br />
obiektów). Zwróć uwagę, że w okienkowych wersjach niektóre<br />
rozkazy "zmieniają" menu i trafiają do<br />
Browse, Debug, Project.<br />
W B<strong>C++</strong> 4 menu Run brak (!). Tworzenie aplikacji sprowadza się<br />
tam do następujących kroków:<br />
Project | Open project lub | AppExpert<br />
Debug | Run<br />
ROZWIJAMY MENU.<br />
Z takiego kręcenia się w kółko po pasku (a propos, czy<br />
zauważyłeś, że pasek podświetlenia może być "przewijany w<br />
kółko"?) jeszcze niewiele wynika. Robimy więc następny krok.<br />
- 15-
Wskaż w menu głównym nazwę "File" i naciśnij [Enter].<br />
Rozwinęło się menu File zawierające listę rozkazów dotyczących<br />
operacji na plikach. Po tym menu też możesz się poruszać przy<br />
pomocy klawiszy kursora ze strzałkami górę lub w dół. Masz do<br />
wyboru dwie grupy rozkazów rozdzielone poziomą linią:<br />
€[S!]<br />
______________________________________________________________<br />
Open - Otwórz istniejący już plik z programem (np. w celu<br />
dopisania czegoś nowego).<br />
New<br />
Save<br />
- Utwórz nowy plik (zaczynamy tworzyć nowy program).<br />
- Zapisz bieżący program na dysk. Pamiętaj: Pliki z<br />
dysku nie znikają po wyłączeniu komputera. Zawsze<br />
lepiej mieć o jedną kopię za dużo niż o jedną za mało.<br />
oraz<br />
Print - Wydrukuj program.<br />
Get Info€€ - Wyświetl informacje o stanie IDE.<br />
Dos Shell - Wyjście "na chwilę" do systemu DOS z możliwością<br />
powrotu do IDE przez rozkaz EXIT.<br />
Quit - Wyjście z IDE Turbo <strong>C++</strong> i powrót do DOSa. Inaczej -<br />
KONIEC PRACY.<br />
_______________________________________________________________<br />
Skoro już wiemy jak rozpocząć pracę nad nowym programem,<br />
zacznijmy przygotowanie do uruchomienia naszego pierwszego<br />
programu.<br />
Wybierz z menu File rozkaz Open... (otwórz plik). Ponieważ<br />
rozkaz taki jest niejednoznaczny, wymaga przed wykonaniem<br />
podania dodatkowych informacji. Gdyby Twój komputer mówił,<br />
zapytałby w tym momencie "który plik mam otworzyć?". Pytanie<br />
zadać musi, będzie więc prowadził dialog z Tobą przy pomocy<br />
OKIENEK DIALOGOWYCH. Jeśli wybrałeś z menu rozkaz OPEN i<br />
nacisnąłeś [Enter], to masz właśnie na ekranie takie okienko<br />
dialogowe. Okienko składa się z kilku charakterystycznych<br />
elementów:<br />
OKIENKO TEKSTOWE - (ang. Text Box lub Input Box) w którym możesz<br />
pisać (klawisz BackSpace [
[Shift]-[Tab] (spróbuj!).<br />
Możesz także posługiwać się myszką.<br />
Więcej o okienkach i menu dowiesz się z następnych lekcji, a na<br />
razie wróćmy do naszego podstawowego zadania - tworzenia<br />
pierwszego programu.<br />
Zanim zaczniemy tworzyć program włóż do kieszeni napędu A: (lub<br />
B:) dyskietkę dołączoną do niniejszej książki. Może ona stać się<br />
Twoją dyskietką roboczą i pomocniczą zarazem na okres tego<br />
kursu.<br />
Jeżeli zainstalowałeś zawartość dyskietki na dysku - przejdź do<br />
stosownego katalogu - C:\C-BELFER (D:\C-BELFER) i odszukaj tam<br />
programy przykładowe. Jeśli nie - możesz nadal korzystać z<br />
dyskietki (jest na niej trochę miejsca).<br />
Wpisz do okienka tekstowego nazwę A:\PIERWSZY (lub odpowiednio<br />
np. C:\C-BELFER\PIERWSZY). Rozszerzeniem możesz się nie<br />
przejmować - zostanie nadane automatycznie. Plik roboczy z Twoim<br />
programem zostanie utworzony na dyskietce w napędzie A:.<br />
Wskaż klawisz [Open] w okienku dialogowym i naciśnij [Enter] na<br />
klawiaturze.<br />
UWAGA!<br />
_________________________________________________________________<br />
Dopóki manipulujesz okienkiem tekstowym i okienkiem z listą<br />
klawisz polecenia [Open] jest wyróżniony (podświetlony) i<br />
traktowany jako tzw. OPCJA DOMYŚLNA (ang. default). W tym<br />
stadium aby wybrać [Open] WYSTARCZY NACISNĄĆ [Enter].<br />
__________________________________________________________________<br />
Wróciliśmy do IDE. zmieniło się tyle, że w nagłówku okna edytora<br />
zamiast napisu<br />
"NONAME00.CPP"<br />
(ang. no mame - bez nazwy)<br />
jest teraz nazwa Twojego programu - PIERWSZY.CPP. Kursor miga w lewym<br />
górnym rogu okna edytora. Możemy zaczynać.<br />
Pierwsze podejście do programu zrobimy trochę "intuicyjnie".<br />
Zamiast wyjaśniać wszystkie szczegóły posłużymy się analogią do<br />
konstrukcji w Pascalu i Basicu (zakładam, że napisałeś już<br />
choćby jeden program w którymś z tych języków). Szczegóły te<br />
wyjaśnię dokładniej począwszy od następnej lekcji.<br />
WPISUJEMY PROGRAM "PIERWSZY.CPP".<br />
Wpisz następujący tekst programu:<br />
/* Program przykładowy - [P-1] */<br />
- 17-
#include <br />
main()<br />
{<br />
printf("Autor: ..........."); /*tu wpisz imie Twoje!*/<br />
printf(" TO JA, TWOJ PROGRAM - PIERWSZY.CPP");<br />
printf("...achoj !!!");<br />
}<br />
I już. Jak widzisz nie jest to aż takie straszne. Gdyby nie to,<br />
że zamiast znajomego PRINT"TO JA...", albo writeln(".."); jest<br />
printf("...");, byłoby prawie całkiem zrozumiałe. Podobny<br />
program w Pascalu mógłby wyglądać np. tak:<br />
# include uses Crt;<br />
main() /* początek */ program AHOJ; {początek}<br />
{ Begin<br />
printf("Autor");<br />
write('Autor');<br />
printf("TO JA");<br />
write('TO JA');<br />
printf("ahoj");<br />
write('ahoj');<br />
} end.<br />
a w BASICU:<br />
10 PRINT "Autor" : REM Początek<br />
20 PRINT "TO JA"<br />
30 PRINT "ahoj"<br />
40 END<br />
€[!!!]UWAGA<br />
______________________________________________________________<br />
Zwróć uwagę, że działanie funkcji:<br />
PRINT (Basic),<br />
printf() (<strong>C++</strong>),<br />
Write i Writeln (Pascal)<br />
nie jest identyczne, a TYLKO PODOBNE.<br />
________________________________________________________________<br />
Sprawdzimy, czy program działa. Tam, gdzie są kropki wpisz Twoje<br />
imię - np. Ewa, Marian, Marcin. Pamiętaj o postawieniu na końcu<br />
znaków cudzysłowu ("), zamknięciu nawiasu i średniku (;) na<br />
końcu linii (wiersza).<br />
Naciśnij kombinację klawiszy [Alt]-[R]. Jest to inny, niż<br />
opisano poprzednio sposób dostępu do menu. Kombinacja klawiszy<br />
[Alt]-[Litera] powoduje uaktywnienie tego menu, którego nazwa<br />
zaczyna się na podaną literę. Przy takiej konwencji litera nie<br />
musi być zawsze pierwszą literą nazwy opcji. Może to być także<br />
litera wyróżniona w nazwie przez podkreślenie lub wyświetlenie<br />
np. w innym kolorze. I tak:<br />
[Alt]+[F] menu File (Plik)<br />
[Alt]+[C] menu Compile (Kompilacja<br />
- 18-
[Alt]+[R] menu Run (Uruchamianie)<br />
[Alt]+[W] menu Window (Okna)<br />
itd., itd..<br />
Kombinacja [Alt]+[R] wybiera więc menu Run (uruchomienie<br />
programu). Menu Run daje Ci do wyboru następujące polecenia:<br />
[S!]<br />
________________________________________________________________<br />
Run - Uruchomienie programu (Utwórz plik .EXE i Wykonaj).<br />
Program Reset - "Wyzerowanie" zmiennych programu.<br />
Go to Cursor - Wykonanie programu do miejsca wskazanego kursorem<br />
w tekście.<br />
Trace Into - Uruchom śledzenie programu.<br />
Step Over - Śledzenie programu z możliwością pominięcia funkcji.<br />
(dosł. tzw. "Przekraczanie" funkcji).<br />
Arguments - Uruchom program z zadanymi argumentami.<br />
________________________________________________________________<br />
Wybierz "Run". Jeśli nie zrobiłeś żadnego błędu, program<br />
powinien się skompilować z komentarzem "Success" i wykonać<br />
(kompilacja zakończona sukcesem; napis mignie tak szybko, że<br />
możesz tego nie zauważyć). Jeśli chcesz spokojnie obejrzeć<br />
wyniki działania swojego programu powinieneś wykonać<br />
następujące czynności:<br />
1. Rozwiń menu Window naciskając klawisze [Alt]-[W].<br />
2. Wybierz z menu rozkaz User screen (ekran użytkownika).<br />
Możesz wykonać to samo bez rozwijania menu naciskając kombinację<br />
klawiszy [Alt]-[F5].<br />
3. Po przejrzeniu wydruku naciśnij [Enter]. Wrócisz do okna<br />
edytora.<br />
Jeśli zrobiłeś błędy - kompilacja się nie uda i program nie<br />
zostanie wykonany, w okienku natomiast pojawi się napis "Errors"<br />
(czyli "Błędy"). Jeśli tak się stało naciśnij [Enter]<br />
dwukrotnie. Popraw ewentualne niezgodności i spróbuj jeszcze<br />
raz.<br />
Błędów zwykle bywa nie więcej niż dwa. Najczęściej jest to brak<br />
lub przekłamanie którejś litery (w słowie main lub printf) i<br />
brak średnika na końcu linii. W okienku komunikatów (Message)<br />
mogą pojawić się napisy - np.:<br />
Error: Statement missing ;<br />
(Błąd: Zgubiony znak ;)<br />
€[S] Error Messages - Komunikaty o błędach.<br />
________________________________________________________________<br />
Najczęściej w komunikatach o błędach będą na początku pojawiać<br />
się następujące słowa:<br />
- 19-
Error - błąd<br />
Warning - ostrzeżenie<br />
Syntax - składnia (składniowy)<br />
Expression - wyrażenie<br />
never used - nie użyte (nie zastosowane)<br />
assign - przypisywać, nadawać wartość/znaczenie<br />
value - wartość<br />
statement - operator, operacja, wyrażenie<br />
________________________________________________________________<br />
€[???] Co z tym średnikiem?<br />
________________________________________________________________<br />
Zwróć uwagę, że po pdświetleniu komunikatu o błędzie (pasek<br />
wyróżnienia podświetlenia możesz przesuwać po liście przy pomocy<br />
klawiszy ze strzałkami w górę i w dół) i po naciśnięciu [Entera]<br />
kompilator pokaże ten wiersz programu, w którym jego zdaniem<br />
jest coś nie w porządku. Brak średnika zauważa zwykle dopiero po<br />
przejściu do następnego wiersza (i tenże wiersz pokaże), co bywa<br />
na początku trochę mylące.<br />
________________________________________________________________<br />
[???] CZEGO ON JESZCZE CHCE ?<br />
________________________________________________________________<br />
Nawet po usunięciu wszystkich błędów <strong>C++</strong> nie "uspokoi się"<br />
całkiem i będzie wyświetlał ciągle komunikat ostrzegawczy:<br />
* w OKIENKU KOMPILACJI: (bardzo typowa sytuacja)<br />
Errors: 0 (Błędy: 0)<br />
Warnings: 1 (Ostrzeżenia: 1)<br />
* W OKIENKU KOMUNIKATÓW - (Messages - tym w dolnej części<br />
ekranu):<br />
*WARNING A:\PIERWSZY.C 4: Function should return a value in<br />
function main<br />
(Uwaga: Funkcja main powinna zwrócić wartość.)<br />
Na razie zadowolimy się spostrzeżeniem, że:<br />
* Błędy UNIEMOŻLIWIAJĄ KOMPILACJĘ i powodują komunikat ERRORS.<br />
* Ostrzeżenia NIE WSTRZYMUJĄ KOMPILACJI i powodują komunikat<br />
WARNINGS.<br />
Jaki jest sens powyższego ostrzeżenia i jak go uniknąć dowiesz<br />
się z następnych lekcji.<br />
________________________________________________________________<br />
Pozostaje nam w ramach tej lekcji:<br />
* Zapisać Twój pierwszy program na dysku i<br />
* Wyjść z IDE <strong>C++</strong>.<br />
- 20-
JAK STĄD WYJŚĆ ?<br />
Aby zapisać plik PIERWSZY.CPP z Twoim programem (końcową<br />
ostateczną wersją) na dysk należy wykonać następujące czynności:<br />
1. Naciśnij klawisz [F10].<br />
W głównym menu pojawi się pasek wyróżnienia sygnalizując, że<br />
menu stało się aktywne.<br />
2. Naciśnij klawisz [F].<br />
Pasek wyróżnienia przesunie się podświetlając menu File<br />
(operacje na plikach). Rozwinie się menu File.<br />
3. Naciśnij klawisz [S] - wybierz polecenie Save (jeśli chcesz<br />
zapisać program w bieżącym katalogu i pod bieżącą nazwą) lub<br />
rozkaz Save As... (zapisz jako...), podaj nowy dysk/katalog i<br />
nową nazwę pliku.<br />
Tekst Twojego programu został zapisany na dysku/dyskietce. Teraz<br />
możemy wyjść z <strong>C++</strong>.<br />
Aby to zrobić, wykonaj następujące czynności:<br />
1. Naciśnij klawisz [F10]. Uaktywni się główne menu.<br />
2. Rozwiń menu File naciskając klawisz [F].<br />
3. Wybierz z menu polecenie "Exit/Quit" i naciśnij [Enter].<br />
€[!!!] SAVE szybciej.<br />
________________________________________________________________<br />
Zwróc uwagę, że zamiast rozwijać kolejne menu, możesz korzystać<br />
z kombinacji klawiszy, które pozwalają Ci wydać rozkaz bez<br />
rozwijania menu. Takie kombinacje klawiszy (ang. hot keys lub<br />
shortcut keys) znajdziesz w menu obok rozkazu, np.:<br />
[Alt]-[X] - Quit/Exit<br />
[F2] - Save<br />
[F3] - Open<br />
[Alt]-[F5] - User screen (Podglądanie działania programu) itp.<br />
________________________________________________________________<br />
€[Z]<br />
________________________________________________________________<br />
1. Spróbuj napisać i uruchomić kilka własnych programów<br />
wypisujących różne napisy. W swoich programach zastosuj funkcję<br />
printf() według następującego wzoru:<br />
printf("....tu wpisz napis do wydrukowania...");<br />
zastosuj znaki przejścia do nowego wiersza według wzoru:<br />
printf("...napis...\n");<br />
porównaj działanie.<br />
- 21-
Swoim programom staraj się nadawać łatwe do rozpoznania nazwy<br />
typu PIERWSZY, DRUGI, ADAM1, PRZYKLAD itp.<br />
€[???] NIE CHCE DZIAŁAĆ ?<br />
________________________________________________________________<br />
Pamiętaj, że dla języka C i <strong>C++</strong> (w przeciwieństwie np. do<br />
Basica) PRINTF i printf to nie to samo! Słowa kluczowe i nazwy<br />
standardowych funkcji<br />
MUSZĄ BYĆ PISANE MAŁYMI LITERAMI !!!<br />
________________________________________________________________<br />
€[???] GDZIE MOJE PROGRAMY ?<br />
________________________________________________________________<br />
Bądź spokojny. Zapisz wersje źródłowe programów na dyskietkę<br />
(dysk). Swoje programy skompilowane do wykonywalnej wersji *.EXE<br />
znajdziesz najprawdopodobniej w katalogu głównym tego dysku, na<br />
którym zainstalowany został <strong>C++</strong> lub w katalogu<br />
\BORLANDC\BIN\.... Jeśli ich tam nie ma, zachowaj zimną krew i<br />
przeczytaj uważnie kilka następnych stron.<br />
________________________________________________________________<br />
€ PAMIĘTAJ:<br />
________________________________________________________________<br />
Jeśli masz oryginalny tekst programu, nazywany WERSJĄ ŹRÓDŁOWĄ<br />
PROGRAMU, zawsze możesz uzyskać ten program w wersji "roboczej",<br />
tzn. skompilować go na plik wykonywalny typu *.EXE (ang.<br />
EXEcutable - wykonywalny).<br />
________________________________________________________________<br />
€[S!] printf() - PRINTing Function - Funkcja DRUKująca<br />
________________________________________________________________<br />
na ekranie (dokładniej - na standardowym urządzeniu wyjścia).<br />
Odpowiednik PRINT w Basicu lub write w Pascalu. Dla ułatwienia<br />
rozpoznawania nazw funkcji w tekście większość autorów pisząca o<br />
języku <strong>C++</strong> umieszcza zawsze po nazwie funkcji parę nawiasów (tak<br />
też musi ją stosować programista w programach w <strong>C++</strong>). Ja także<br />
będę stosować dalej tę zasadę.<br />
________________________________________________________________<br />
€[???] A JEŚLI NIE MA <strong>C++</strong> ???<br />
________________________________________________________________<br />
W przeciwieństwie do INTERPRETERÓW (np. QBasic), które muszą być<br />
obecne, by program zadziałał, KOMPILATORY tworzą wersje<br />
wykonywalne programów, które mogą pracować niezależnie. W<br />
katalogu głównym tego dysku, na którym jest zainstalowany Twój<br />
BORLAND/Turbo <strong>C++</strong> znajdziesz swoje programy PIERWSZY.EXE,<br />
DRUGI.EXE itp. Aby te programy uruchomić nie musisz uruchamiać<br />
kompilatora <strong>C++</strong>. Wystarczy:<br />
1. Przejść na odpowiedni dysk przy pomocy polecenia:<br />
D: (E: lub F:)<br />
- 22-
2. Przejść do odpowiedniego katalogu - np. głównego:<br />
CD \<br />
3. Wydać polecenie:<br />
PIERWSZY[Enter]<br />
________________________________________________________________<br />
€[!!!]UWAGA:<br />
________________________________________________________________<br />
Jeśli nie jesteś jedynym użytkownikiem kompilatora <strong>C++</strong> i na tym<br />
samym komputerze pracuje jeszcze ktoś inny, sprawdź, czy inny<br />
użytkownik nie ustawił inaczej katalogu wyjściowego (Options |<br />
Directories | Output Directory). Katalog wyjściowy (ang. output<br />
directory) to ten katalog, w którym <strong>C++</strong> zapisuje pliki *.EXE po<br />
wykonaniu kompilacji. Jeśli jesteś skazany na własne siły -<br />
patrz - następne lekcje.<br />
________________________________________________________________<br />
SPECJALNE KLAWISZE, które warto poznać.<br />
Oto skrócona tabela z najważniejszymi kombinacjami klawiszy<br />
służącymi do "nawigacji" (czyli prościej - poruszania się) w<br />
środowisku IDE kompilatorów BORLAND <strong>C++</strong> i Turbo <strong>C++</strong>.<br />
Przydatne w <strong>Borland</strong> <strong>C++</strong> i Turbo <strong>C++</strong> kombinacje klawiszy.<br />
________________________________________________________________<br />
Wybór rozkazów z menu:<br />
Alt+F Rozwinięcie menu File (operacje na plikach)<br />
Alt+E Rozwinięcie menu Edit (edycja tekstu programu)<br />
Alt+S Rozwinięcie menu Search (przeszukiwanie)<br />
Alt+R Rozwinięcie menu Run (uruchamianie programu)<br />
Alt+C Rozwinięcie menu Compile (kompilacja)<br />
Alt+D Rozwinięcie menu Debug (diagnostyka i błędy)<br />
Alt+P Rozwinięcie menu Project (program wielomodułowy)<br />
Alt+O Rozwinięcie menu Option (opcje, konfiguracja)<br />
Alt+W Rozwinięcie menu Window (zarządzanie oknami)<br />
Alt+H Rozwinięcie menu Help (pomoc)<br />
Alt+B Rozwinięcie menu przeglądarki - Browse (Win)<br />
Alt+X Wyjście z kompilatora DOS'owskiego - Exit<br />
Alt+F4 Wyjście z kompilatora dla Windows<br />
________________________________________________________________<br />
Rozkazy w trybie edycji tekstu programu:<br />
________________________________________________________________<br />
Shift+Delete Wytnij wybrany blok tekstu (Cut) i umieść w<br />
przechowalni (Clipboard)<br />
Shift+Insert Wstaw blok tekstu z przechowalni (Paste)<br />
Ctrl+Insert Skopiuj zaznaczony blok tekstu do przechowalni<br />
(Copy)<br />
Ctrl+Y Skasuj wiersz tekstu (Delete a line)<br />
Ctrl+Delete Skasuj zaznaczony blok tekstu<br />
- 23-
Shift+[-->] Zaznaczanie bloku tekstu w prawo<br />
Shift+[
LEKCJA 4. Jeszcze o IDE <strong>C++</strong> .<br />
_______________________________________________________________<br />
W trakcie tej lekcji:<br />
1. Dowiesz się więcej o menu i okienkach w środowisku IDE.<br />
2. Poznasz trochę technik "myszologicznych".<br />
3. Napiszesz i uruchomisz swój drugi program.<br />
________________________________________________________________<br />
W dolnej części ekranu jest podobny pasek do paska menu,<br />
niemniej ważny, choć o innym przeznaczeniu. Pasek ten jest to<br />
tzw. WIERSZ STATUSOWY (ang. Status Line). Jak wynika z nazwy w<br />
tym wierszu wyświetlane są informacje dotyczące bieżącego stanu<br />
(i bieżących możliwości) środowiska IDE. Zaryzykuję tezę, że<br />
często jeden prosty, własny eksperyment może być więcej wart niż<br />
wiele stron opisów. Poeksperymentujmy zatem chwilę z wierszem<br />
statusowym.<br />
[???] NIE CHCE SIĘ URUCHOMIĆ ???<br />
________________________________________________________________<br />
Jeśli przy starcie kompilatora <strong>C++</strong> nastąpi komunikat:<br />
System Message<br />
Disk is not ready in drive A<br />
[Retry] [Cancel]<br />
(Komunikat systemu <strong>C++</strong>: Dyskietka w napędzie A nie gotowa do<br />
odczytu; Jeszcze raz? Zrezygnować?)<br />
to znaczy, że <strong>C++</strong> nie może odtworzyć ostatniego ekranu<br />
roboczego, ponieważ nie udostępniłeś mu dyskietki z programami,<br />
nad którymi ostatnio pracowałeś.<br />
________________________________________________________________<br />
W wierszu statusowym wyjaśnione jest działanie klawiszy<br />
funkcyjnych F1, F2, itd. Mogą tam również pojawiać się<br />
krótkie napisy-wyjaśnienia dotyczące np. rozkazu wyróżnionego<br />
właśnie w menu. Powinien tam być napis:<br />
F1 Help F2 Save F3 Load AltF9 Compile F9 Make F10 Menu<br />
znaczy to:<br />
[F1] - Pomoc<br />
[F2] - Zapamiętanie bieżącego pliku na dysku pod bieżącą nazwą<br />
(nawet jeśli tą nazwą jest NONAME01.CPP, tzn. została nadana<br />
automatycznie i znaczy - o ironio - "BEZNAZWY01.CPP") i w<br />
bieżącym katalogu.<br />
[F3] - Załadowanie do okienka edycyjnego nowego pliku tekstowego<br />
(np. nowego programu).<br />
[Alt]-[F9] - Kompilacja w trybie "Compile".<br />
[F9] - Kompilacja w trybie "Make" (jednoczesnej kompilacji i<br />
- 25-
konsolidacji).<br />
[F10] - Uaktywnienie głównego menu.<br />
JAK ZROBIĆ PORZĄDEK?<br />
W trakcie uruchamiania kompilator korzysta z plików<br />
zewnętrznych. <strong>C++</strong> stara się być USER FRIENDLY (przyjazny wobec<br />
użytkownika) i odtworzyć taki stan ekranu, w jakim ostatnio<br />
przerwałeś pracę, co nie zawsze jednak jest korzystne. W wierszu<br />
statusowym pojawiają się napisy informujące o tym (np:<br />
Loading Desktop File . . .<br />
- ładuję plik zawierający konfigurację ostatniego ekranu<br />
roboczego...). Jeśli chcesz by na początku<br />
sesji z <strong>C++</strong> ekran był "dziewiczo" czysty, powinieneś:<br />
* zmienić nazwę pliku [D:]\BORLANDC\BIN\TCDEF.DSK<br />
na dowolną inną, np. STARY.DSK lub STARY1.DSK, stosując<br />
polecenie systemu DOS RENAME. [D:] oznacza odpowiedni dla<br />
Twojego komputera dysk. <strong>C++</strong> wystartuje wtedy z czystym ekranem i<br />
utworzy nowy plik TCDEF.DSK.<br />
* Plików TCDEF nie należy usuwać. Kiedy nabierzesz trochę wprawy<br />
pliki te znacznie przyspieszą i ułatwią Ci pracę z <strong>C++</strong>.<br />
Aby zamknąć zbędne okna możesz zastosować również rozkaz Close<br />
(ang. Close - zamknij) z menu Window (okna). Zwróć uwagę, że<br />
polecenie Close odnosi się do bieżącego okna wyróżnionego przy<br />
pomocy podwójnej ramki. Aby zamknąć bieżące okno, powinieneś:<br />
1. Nacisnąć klawisze [Alt]-[W]<br />
Rozwinie się menu Windows.<br />
2. Wybrać z menu rozkaz Close - [C].<br />
Może pojawić się okienko z ostrzeżeniem:<br />
WARNING: A:\PIERWSZY.CPP not saved. Save?<br />
(UWAGA: plik A:\PIERWSZY.CPP nie zapisany na dysku. Zapisać ?).<br />
[???] ZNIKNĄŁ PROGRAM ???<br />
________________________________________________________________<br />
<strong>C++</strong> chce Cię uchronić przed utratą programu, ale uważaj! Jeśli<br />
odpowiesz Yes - Tak ([Y] lub [Enter]), to nowa wersja programu<br />
zostanie nadpisana na starą!<br />
________________________________________________________________<br />
[!!!] ZAMYKANIE OKIEN.<br />
________________________________________________________________<br />
Możesz szybciej zamknąć okno naciskając kombinację klawiszy<br />
- 26-
[Alt]-[F3].<br />
________________________________________________________________<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
Bądź ostrożny podejmując decyzję o zapisie wersji programu na<br />
dysk. Okienko z ostrzeżeniem pojawi się za każdym razem przed<br />
zamknięciem okna edycyjnego z tekstem programu. Jeśli przy<br />
zamykaniu okna nie pojawi się ostrzeżenie, to znaczy, że program<br />
w tej wersji, którą widzisz na ekranie został już zapisany na<br />
dysk.<br />
________________________________________________________________<br />
A JEŚLI NIE CHCĘ ZAMYKAĆ OKIEN?<br />
W porządku, nie musisz. W menu Window ([Alt]-[W]) masz do<br />
dyspozycji rozkaz Next (następne okno). Możesz go wybrać albo<br />
naciskając klawisz [N], albo przy pomocy klawiszy kursora. Każde<br />
z okien na Twoim roboczym ekranie ma nazwę - nagłówek - np.<br />
NONAME00.CPP, PIERWSZY.CPP, ale nie tylko. Pierwsze dziesięć<br />
okien ma również swoje numery - podane blisko prawego - górnego<br />
rogu okna w nawiasach kwadratowych - np. [1], [2] itd.<br />
Posługując się tym rozkazem możesz przechodzić od okna do okna<br />
nie zamykając żadnego z okien. Spróbuj!<br />
Jest jeszcze inny sposób przejścia od okna do okna. Jeśli chcesz<br />
przejść do okna o numerze np. [1], [2], [5] itp. powinieneś<br />
nacisnąć kombinację klawiszy [Alt]-[1], [Alt]-[5] itp..<br />
Niestety, tylko pierwsze 9 okien ma swoje numerki.<br />
Możesz korzystać z listy okien (Window | List) lub klawisza<br />
funkcyjnego [F6].<br />
[S] ACTIVE WINDOW - AKTYWNE OKNO.<br />
________________________________________________________________<br />
Na ekranie może się znajdować jednocześnie wiele okien, ale w<br />
danym momencie tylko jedno z nich może być AKTYWNE. Aktywne<br />
okno, to to, w którym miga kursor i w którym aktualnie<br />
pracujesz. Aktywne okno jest dodatkowo wyróżnione podwójną<br />
ramką.<br />
________________________________________________________________<br />
[???] Robi "na szaro"?<br />
________________________________________________________________<br />
Zwróć uwagę, że dopóki bieżącym aktywnym oknem jest okienko<br />
komunikatów (Message - to w dolnej części ekranu), nie możesz<br />
np. powtórzyć kompilacji programu. Rozkazy Compile | Compile i<br />
Run | Run będą "zrobione na szaro" (ang. grayed out) - czyli<br />
nieaktywne. Najpierw trzeba przejść do okna edycji tekstu<br />
programu (np. poprzez kliknięcie myszką).<br />
________________________________________________________________<br />
- 27-
Rozwiń menu Options (opcje).<br />
Możesz to zrobić na wiele sposobów. Najszybciej chyba naciskając:<br />
[Alt]+[O]<br />
Rozwinęło się menu, udostępniając następującą listę poleceń:<br />
FULL MENUs - Pełne Menu ("s" oznacza, że chodzi o "te" menu w<br />
liczbie mnogiej, a nie o pojedyncze menu).<br />
COMPILER - Kompilator.<br />
MAKE... - dosł. "ZRÓB", dotyczy tworzenia "projektów" (zwróć<br />
uwagę na wielokropek [...]).<br />
DIRECTORIES... - KATALOGI (znów wielokropek !).<br />
ENVIRONMENT... - OTOCZENIE lub inaczej ŚRODOWISKO.<br />
SAVE - ZAPAMIĘTAJ (UWAGA: To jest zupełnie inne SAVE niż<br />
w menu File. Nie wolno mylić tych poleceń.<br />
Pomyłka grozi utratą tekstu programu!).<br />
Popatrz na linię statusową. Jeśli będziesz poruszać się po menu<br />
Option, podświetlając kolejne rozkazy, w wierszu statusowym<br />
będzie wyświetlany krótki opis działania wskazanego rozkazu. I<br />
tak, powinieneś zobaczyć kolejno następujące napisy:<br />
Full Menus [Off/On]- Use or don't use full set of menu commands.<br />
(Stosuj lub nie stosuj pełnego zestawu rozkazów w menu -<br />
domyślnie przyjmowane jest Off/Nie).<br />
Compiler - Set compiler defaults for code generation, error<br />
messages and names.<br />
(Ustaw domyślne parametry pracy kompilatora dotyczące<br />
generowania kodu programu, komunikatów o błędach i nazw).<br />
Make... - Set condition for project-makes.<br />
(Ustawianie warunków do tworzenia projektu).<br />
Directories... - Set path for compile, link and executable<br />
files.<br />
(Wybierz katalogi i ustaw ścieżki dostępu dla kompilacji,<br />
konsolidacji i WSKAŻ MIEJSCE - GDZIE ZAPISAĆ PLIK TYPU *.EXE po<br />
kompilacji).<br />
Environment... - Make environment wide settings (eg, mouse<br />
settings).<br />
(Ustawienie parametrów rozszerzonego otoczenia, np. parametrów<br />
pracy myszki).<br />
Save - Save all the settings you've made in the Options menu.<br />
(Powoduje zapamiętanie na dysku wszystkich zmian parametrów<br />
roboczych IDE, które ustawiłeś, korzystając z rozkazów<br />
dostępnych za pośrednictwem menu Options.).<br />
- 28-
Ten rozkaz pozwala Ci ustawić konfigurację IDE "raz na zawsze".<br />
Przygotujmy się do powtórzenia kompilacji programu PIERWSZY.CPP.<br />
Jeśli masz na ekranie rozwinięte menu Options, wybierz z menu<br />
polecenie Directories... .<br />
KOMPILACJA ZE WSKAZANIEM ADERSU.<br />
1. Wskaż w menu polecenie Directories i naciśnij [Enter].<br />
Po poleceniu umieszczony jest wielokropek. Znaczy to, że rozkaz<br />
nie zostanie wykonany, zanim komputer nie uzyska od Ciebie<br />
pewnych dodatkowych informacji. Wiesz już, że praktycznie<br />
oznacza to dla Ciebie konieczność "wypełnienia" okienka<br />
dialogowego. Po wybraniu polecenia Directories ukazało się<br />
okienko dialogowe już "wstępnie wypełnione". Takie "wstępne<br />
wypełnienie" okienka daje Ci pewne dodatkowe informacje. Wynika<br />
z niego mianowicie JAKIE PARAMETRY SĄ PRZYJMOWANE DOMYŚLNIE<br />
(default).<br />
W okienku dialogowym masz trzy okienka tekstowe:<br />
* Include Directories (Katalog zawierający pliki nagłówkowe, np.<br />
STDIO.H, CONIO.H, GRAPHICS.H itp. dołączane do programów).<br />
* Library Directories (Katalog zawierający gotowe biblioteki,<br />
zawarte w plikach typu *.LIB,).<br />
* Output Directory (Katalog wyjściowy, w którym po kompilacji<br />
będą umieszczane Twoje programy w wersji *.EXE).<br />
Pierwsze dwa zostawimy w spokoju.<br />
2. Naciśnij dwukrotnie klawisz [Tab]. Kursor wskazuje teraz<br />
okienko tekstowe Output Directory.<br />
3. Wpisz do okienka tekstowego Output Directory:<br />
A:\ lub C:\C-BELFER<br />
znaczy to, że od teraz po wykonaniu kompilacji i utworzeniu<br />
pliku wykonywalnego typu *.EXE, plik taki zostanie zapisany we<br />
wskazanym katalogu i na wskazanym dysku/dyskietce.<br />
UWAGA:<br />
________________________________________________________________<br />
* Jeśli zainstalowałeś zawartość dyskietki na dysku i wolisz<br />
posługiwać się własnym katalogiem roboczym - wpisz tam<br />
odpowiednią ścieżkę dostępu - np. C:\C-BELFER. Jeśli Twój<br />
katalog zagnieżdżony jest głębiej (np. w przypadku użytkowników<br />
sieci Novell) - podaj pełną ścieżkę dostępu - np.:<br />
F:\USERS\ADAM\C-BELFER<br />
* Wszędzie, gdzie w treści książki odwołuję się do dyskietki A:<br />
możesz konsekwentnie po zainstalowaniu stosować odpowiedni<br />
- 29-
katalog na dysku stałym, bądź na dysku sieciowym.<br />
________________________________________________________________<br />
4. Naciśnij [Enter].<br />
Spróbuj teraz, znaną z poprzedniej lekcji metodą, wczytać do<br />
okienka edytora Twój pierwszy program. Musisz wykonać<br />
następujące czynności:<br />
1. Włóż do napędu A: dyskietkę z programem PIERWSZY.CPP (jeśli<br />
jeszcze jej tam nie ma).<br />
2. Rozwiń menu File, naciskając kombinację klawiszy [Alt]-[F].<br />
3. Wybierz z menu rozkaz Open, naciskając klawisz [O].<br />
Pojawi się znane Ci okienko dialogowe. Zwróć uwagę na wiersz<br />
statusowy. Napis:<br />
Enter directory path and file mask<br />
znaczy:<br />
Wpisz ścieżkę dostępu do katalogu i "wzorzec" nazwy pliku.<br />
Użyte słowo "wzorzec" oznacza, że wolno Ci wpisać do okienka<br />
tekstowego także nazwy wieloznaczne, zawierające znaki "*" i<br />
"?", np.:<br />
*.C<br />
A:\???.C<br />
D:\BORLANDC\SOURCE\P*.*<br />
itp. (Spróbuj!, zawsze możesz się wycofać lub zmienić zdanie,<br />
posługując się klawiszami [BackSpace], [Shift], [Tab] i [Esc].).<br />
Klawisz [Tab] umożliwia Ci skok od okienka do okienka "do<br />
przodu", a [Shift]-[Tab] - "do tyłu". Zgodnie z nazwą (ang.<br />
ESCape - uciekać), klawisz [Esc] pozwala Ci wycofać się z<br />
niewygodnych sytuacji - np. zamknąć okienko dialogowe lub zwinąć<br />
rozwinięte menu bez żadnej akcji.<br />
Jeśli wpiszesz wzorzec nazwy, to w okienku z listą zobaczysz<br />
wszystkie pliki wybrane z podanego dysku i z podanego katalogu<br />
według zadanego wzorca. Aby wybrać plik z listy należy klawiszem<br />
[Tab] przejść do okienka z listą, klawiszami kursora wskazać<br />
potrzebny plik i nacisnąć [Enter].<br />
4. Wpisz do okienka tekstowego<br />
A:\PIERWSZY.CPP<br />
5. Naciśnij [Enter].<br />
[!!!]FAST START - SZYBKI START.<br />
________________________________________________________________<br />
Jeśli chcesz by <strong>C++</strong> automatycznie wczytał Twój program do<br />
okienka edytora, to możesz zadać nazwę pliku z tekstem programu<br />
jako parametr w wierszu polecenia, uruchamiając <strong>C++</strong> np. tak:<br />
BC A:\PIERWSZY.CPP<br />
- 30-
Jeśli korzystasz z programu Norton Commander, to możesz dodać do<br />
pliku NC.EXT następujący wiersz:<br />
C: TC !.!<br />
cpp: bc !.!<br />
wówczas wystarczy tylko wskazać odpowiedni plik typu *.C lub<br />
.CPP z tekstem programu i nacisnąć [Enter].<br />
________________________________________________________________<br />
Kompilatory <strong>Borland</strong>a mogą w różnych wersjach nazywać się różnie:<br />
TC.EXE, BC.EXE, BCW.EXE (dla Windows), itp.. Sprawdź swoją<br />
wersję kompilatora i wpisz właściwe nazwy dodając ewentualnie<br />
ścieżki dostępu - np.:<br />
C: D:\BORLANDC\BIN\BC !.!<br />
CPP: WIN C:\BORLANDC\BIN\BCW !.!<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
Rozkazy uruchamiające kompilator mogą być złożone nawet z 4<br />
parametrów - np.:<br />
WIN /3 C:\BORLANDC\BIN\BCW C:\C-BELFER\PROGRAMY\P027.CPP<br />
spowoduje:<br />
* uruchomienie Windows w trybie rozszerzonym 386<br />
* uruchomienie kompilatora w wersji dla Windows - BCW.EXE<br />
* załadowanie pliku z programem - P27.CPP z wskazanego katalogu<br />
________________________________________________________________<br />
[P002.CPP]<br />
Dokonaj w swoim programie następujących zmian:<br />
________________________________________________________________<br />
#include (stdio.h><br />
#include <br />
main()<br />
{<br />
printf("\n");<br />
printf("Autor: np. Antoni Kowalski\n");<br />
printf("program: PIERWSZY.CPP \n - wersja II \n");<br />
getch();<br />
}<br />
________________________________________________________________<br />
******Uwaga: Jeśli pracujesz w Windows - Z TEGO MIEJSCA********<br />
przy pomocy rozkazów Edit | Copy<br />
możesz przenieść program do okna kompilatora<br />
- 31-
poprzez schowek Windows (Clipboard).<br />
W oknie kompilatora należy:<br />
1. Otworzyć nowe okno edytora tekstowego:<br />
File | New<br />
2. Wstawić plik ze schowka:<br />
Edit | Paste<br />
--- To okno (AM-Edit) i całego BELFRA możesz w tym czasie zredukować<br />
--- Do ikonki.------------------------------------------------------<br />
********************************************************************<br />
Dzięki dodaniu do tekstu programu funkcji getch(), program nie<br />
powinien już tak szybko mignąć na ekranie i zniknąć. Zatrzyma<br />
się teraz i zaczeka na przyciśnięcie klawisza. Funkcja getch(),<br />
działa podobnie do:<br />
10 IF INKEY$="" GOTO 10<br />
w Basicu lub Readln w Pascalu.<br />
Nazwa pochodzi od GET CHaracter (POBIERZ ZNak, z klawiatury).<br />
Skompiluj program PIERWSZY.CPP. Aby to zrobić, powinieneś:<br />
1. Rozwinąć menu Compile - [Alt]-[C].<br />
2. Wybrać z menu rozkaz Compile - [C].<br />
Ostrzeżenie WARNING na razie ignorujemy.<br />
Wykonaj kompilację programu powtórnie przy pomocy rozkazu Run z<br />
menu Run. Naciśnij kolejno klawisze:<br />
[Alt]-[R], [R]<br />
lub<br />
[Alt]-[R], [Enter]<br />
Ten sam efekt uzyskasz naciskając kombinację klawiszy<br />
[Ctrl]-[F9].<br />
Uruchom program powtórnie naciskając kombinację klawiszy<br />
[Alt]-[R], [R]. Zwróć uwagę, że teraz kompilacja nastąpi<br />
znacznie szybciej. Tak naprawdę <strong>C++</strong> stwierdzi tylko, że od<br />
ostatniej kompilacji nie dokonano żadnych zmian w programie i<br />
odstąpi od zbędnej kompilacji. Takie właśnie znaczenie ma<br />
komunikat "Checking dependences" (sprawdzam zależności, który<br />
mignie w okienku kompilacji. Po korekcie programu napisy<br />
wyglądają znacznie przyzwoiciej, prawda? Po obejrzeniu napisów<br />
naciśnij [Enter].<br />
Możemy teraz wyjść z programu <strong>C++</strong>. Rozwiń menu File naciskając<br />
klawisze [Alt]-[F] i wybierz z menu rozkaz Quit. Pojawi się<br />
okienko z ostrzeżeniem:<br />
WARNING: A:\PIERWSZY.CPP not saved. Save?<br />
(UWAGA: plik A:\PIERWSZY.CPP nie zapisany na dysku. Zapisać ?).<br />
- 32-
W ten sposób <strong>C++</strong> ZNOWU chce Cię uchronić przed utratą programu,<br />
ale uważaj! Jeśli odpowiesz Tak ([Y] lub [Enter]), to nowa<br />
wersja programu zostanie nadpisana na starą! Jeśli odpowiesz Nie<br />
[N]<br />
na dysku pozostanie stara wersja programu a nowa<br />
zniknie.<br />
Po wyjściu z <strong>C++</strong> znajdziesz się w jego katalogu roboczym, lub w<br />
tym katalogu bieżącym, z którego wydałeś rozkaz uruchomienia<br />
kompilatora <strong>C++</strong>. Aby uruchomić swój program musisz zatem wydać<br />
następujący rozkaz:<br />
A:\PIERWSZY.EXE<br />
lub krócej<br />
A:\PIERWSZY<br />
a jeśli chcesz się przekonać, czy Twój program jest tam, gdzie<br />
powinien być, możesz go zobaczyć. Napisz rozkaz<br />
DIR A:\<br />
lub<br />
DIR A:\*.EXE<br />
Aby upewnić się całkowicie, że to właśnie ten program, zwróć<br />
uwagę na datę i czas utworzenia pliku. Jeśli masz prawidłowo<br />
ustawiony zegar w swoim komputerze, data powinna być dzisiejsza<br />
a czas - kilka minut temu. Jeśli coś jest nie tak, powinieneś<br />
przy pomocy rozkazów systemu DOS: DATE i TIME zrobić porządek w<br />
swoim systemie. O takich drobiazgach warto pamiętać. Pozwoli Ci<br />
to w przyszłości odróżnić nowsze i starsze wersje programów,<br />
uniknąć pomyłek i zaoszczędzić wiele pracy.<br />
[Z] 1. - Propozycja zadania - ćwiczenia do samodzielnego wykonania.<br />
-------------------------------------------------------------------<br />
Spróbuj odszukać plik żródłowy .CPP i plik wynikowy .EXE<br />
wychodząc "na chwilę" z IDE przy pomocy rozkazu File | DOS<br />
Shell.<br />
-------------------------------------------------------------------<br />
A teraz zajrzyjmy do środka do pliku PIERWSZY.EXE. Jeśli<br />
korzystasz z programu Norton Commander, to masz do dyspozycji<br />
opcje [F3] - View (przeglądanie) i [F4] - Edit (edycja). Jeśli<br />
nie korzystasz z NC, musisz wydać następujący rozkaz:<br />
TYPE A:\PIERWSZY.EXE | C:\DOS\MORE<br />
lub<br />
C:\DOS\EDIT A:\PIERWSZY.EXE<br />
Jak widzisz na ekranie, napisy zawarte w programie pozostały<br />
czytelne, ale to co widać dookoła nie wygląda najlepiej. Na<br />
podstawie tego co widzisz, można (na razie ostrożnie) wysnuć<br />
wniosek, że ani Viewer (przeglądarka), ani Edytor, które<br />
- 33-
doskonale spisują się przy obróbce plików tekstowych, nie nadają<br />
się do analizy i obróbki programów w wersji *.EXE. Narzędziami,<br />
które będziemy musieli stosować, mogą być programy typu<br />
DEBUGGER, PROFILER, LINKER (konsolidator), kompilator i in..<br />
Mam nadzieję, że czujesz się w środowisku IDE już trochę<br />
swobodniej, a więc bierzemy się za drugi program.<br />
__________________________________________________________<br />
EOF<br />
- 34-
LEKCJA 5. DZIAŁANIA PRZY POMOCY MYSZKI I BŁĘDY W PROGRAMIE.<br />
________________________________________________________________<br />
Z tej lekcji dowiesz się,<br />
* Jak posługiwać się myszką w środowisku IDE (DOS)<br />
* O czy należy pamiętać, przy tworzeniu i uruchamianiu<br />
programów.<br />
* Jak poprawiać błędy w programie.<br />
________________________________________________________________<br />
Zanim będzie można kontynuować eksperymenty, trzeba coś zrobić,<br />
by robocze okno edytora było puste. Aby otworzyć takie nowe<br />
puste okno edytora należy:<br />
* Rozwinąć menu File;<br />
* Wybrać z menu rozkaz New (nowy).<br />
Na ekranie monitora otworzy się nowe puste okno zatytułowane<br />
"NONAME00.CPP", "NONAME01.CPP", itp (lub "bez nazwy" i o<br />
kolejnym numerze). Różne edytoro-podobne aplikacje mają zwyczaj<br />
otwierania okna dla nowego pliku tekstowego i nadawanie mu na<br />
początku jednej z dwóch nazw:<br />
[S] SŁOWNICZEK: UFO w trybie Edycji<br />
________________________________________________________________<br />
Untitled - niezatytułowany<br />
Noname - bez nazwy<br />
(Tak na marginesie UFO to skrót od Unidentified Flying Object -<br />
Niezidentyfikowany Obiekt Latający, gdy przejdziemy do<br />
programowania obiektowego, znajomość tego terminu też Ci się<br />
przyda).<br />
________________________________________________________________<br />
Nadanie plikowi dyskowemu z tekstem źródłowym programu jego<br />
właściwej nazwy i zapisanie go na dysku stałym komputera w<br />
określonym miejscu następuje w tym momencie, kiedy po napisaniu<br />
programu zapisujesz go na dysk rozkazem:<br />
File | Save lub File | Save As...<br />
Zapis File | Save oznacza "Rozkaz Save z menu File". Gdy po<br />
opracowaniu programu rozwiniesz menu File i wybierzesz rozkaz<br />
Save as... (zapisz jako...), pojawi się okienko dialogowe "Save<br />
File as" (zapis pliku jako...).<br />
Do okienka edycyjnego "Name" (nazwa) możesz wpisać nazwę, którą<br />
chcesz nadać swojemu nowemu programowi. Zwróć uwagę, że możesz<br />
podać nazwę pliku i jednocześnie wskazać miejsce - np.:<br />
Name:<br />
F:\USERS\ADAM\PROBY\PROGRAM.CPP<br />
Po wpisaniu nazwy naciśnij klawisz [Enter] lub wybierz klawisz<br />
[OK] w okienku dialogowym myszką. Tytuł okna edytora zmieni się<br />
na wybraną nazwę.<br />
- 35-
Możesz również (jeśli odpowiedni katalog już istnieje), wskazać<br />
właściwy katalog w okienku z listą "Files" i dwukrotnie<br />
"kliknąć" lewym klawiszem myszki.<br />
Możesz wskazać myszką okienko edycyjne i nacisnąć lewy klawisz<br />
myszki, bądź naciskać klawisz [Tab] aż do momentu, gdy kursor<br />
zostanie przeniesiony do okienka edycyjnego. Okienko edycyjne to<br />
to okienko, do którego wpisujesz nazwę pliku. W okienku<br />
edycyjnym (Save File As) naciskaj klawisz [BackSpace] aż do<br />
chwili skasowania zbędnej nazwy pliku i pozostawienia tam tylko<br />
ścieżki dostępu - np. A:\PROBY\. Wpisz nazwę programu - np.<br />
PROG1.CPP. Po wpisaniu nazwy możesz nacisnąć [Enter] lub wskazać<br />
myszką klawisz [OK] w okienku i nacisnąć lewy klawisz myszki.<br />
Jeśli tak zrobisz w przypadku pustego okienka NONAME00.CPP -<br />
kompilator utworzy na dysku we wskazanym katalogu plik o zadanej<br />
nazwie - np. A:\PROBY\PROGR1.CPP (na razie pusty). Zmieni się<br />
także nagłówek (nazwa) okienka edycyjnego na ekranie roboczym.<br />
[!!!]UWAGA.<br />
________________________________________________________________<br />
Wszystkie pliki zawierające teksty programów w języku <strong>C++</strong><br />
powinny ˙mieć charakterystyczne rozszerzenie *.CPP (CPP to skrót<br />
od C Plus Plus), lub .C. Po tym rozszerzeniu rozpoznaje te<br />
programy kompilator. Nadanie rozszerzenia .C lub .CPP może<br />
dodatkowo wpływać na sposób kompilacji programu. Zanim wyjaśnimy<br />
te szczegóły, będziemy zawsze stosować rozszerzenie .CPP.<br />
Wszelkie inne rozszerzenia (.BAK, .TXT, .DEF, itp.) nie<br />
przeszkadzają w edycji i kompilacji programu, ale mogą w<br />
niejawny sposób wpłynąć na sposób kompilacji.<br />
________________________________________________________________<br />
Jeśli masz puste robocze okno edytora - możesz wpisać tam<br />
swój własny nowy program. Wpisz:<br />
void main(void)<br />
Każdy program w <strong>C++</strong> składa się z instrukcji. Wiele takich<br />
instrukcji to wywołania funkcji. W <strong>C++</strong> rozkaz wywołania i<br />
wykonania funkcji polega na wpisaniu nazwy funkcji (bez żadnego<br />
dodatkowego słowa typu run, execute, load, itp.). Tych funkcji<br />
może być w programie jedna, bądź więcej. Tworzenie programu w<br />
<strong>C++</strong> z zastosowaniem funkcji (takich jakgdyby mini-programików)<br />
przypomina składanie większej całości z klocków.<br />
Należy podkreślić, że:<br />
każdy program w <strong>C++</strong> musi zawierać funkcję main() (ang. main -<br />
główna).<br />
Wykonanie każdego programu rozpoczyna się właśnie od początku<br />
funkcji main(). Innymi słowy - miejsce zaznaczone w programie<br />
przy pomocy funkcji main() to takie miejsce, w które komputer<br />
zagląda zawsze na początku wykonania programu i od tego właśnie<br />
miejsca rozpoczyna poszukiwanie i wykonywanie rozkazów.<br />
- 36-
[S] Entry Point<br />
___________________________________________________________________<br />
Punkt wejścia do programu nazywa się:<br />
Program Entry Point<br />
Taki właśnie punkt wejścia wskazuje słowo main().<br />
Punk wejścia mogą mieć nie tylko programy .EXE ale także biblioteki<br />
(.DLL - dynamicznie dołączanie biblioteki).<br />
____________________________________________________________________<br />
Każda funkcja powinna mieć początek i koniec. Początek funkcji w<br />
C/<strong>C++</strong> zaznacza się przez otwarcie nawiasów klamrowych { a koniec<br />
funkcji poprzez zamknięcie } . Początek głównej funkcji main()<br />
to zarazem początek całego programu. Zaczynamy zwykle od<br />
umieszczenia w oknie edytora <strong>C++</strong> znaków początku i końca<br />
programu.<br />
main()<br />
{<br />
Podobnie jak wcześniej, kompilator wyświetli na ekranie okienko<br />
zawierające komunikaty o przebiegu kompilacji. Po zakończeniu<br />
kompilacji nastąpi wykonanie programu. Na moment mignie roboczy<br />
ekran użytkownika. Na nieszczęście program nic nie robi, więc<br />
nic się tam nie wydarzy.<br />
Aby przeanalizować, jak kompilator <strong>C++</strong> reaguje na błędy w<br />
programach, zmień tekst w pierwszej linii programu na błędny:<br />
vod main(void)<br />
{<br />
}<br />
Spróbuj powtórnie skompilować i uruchomić program.<br />
Kompilator wyświetli okienko, w którym pojawi się komunikat o<br />
błędach. W taki właśnie sposób kompilator taktownie informuje<br />
programistę, że nie jest aż taki dobry, jak mu się czasami<br />
wydaje. Komputer jest niestety pedantem. Oczekuje (my, ludzie<br />
tego nie wymagamy) absolutnej dokładności i żelaznego<br />
przestrzegania pewnych zasad. "Zjadając" jedną literę naruszyłeś<br />
takie zasady, co zauważył kompilator.<br />
W górnej części ekranu kompilator wyróżnił paskiem podświetlenia<br />
ten wiersz programu, który zawiera błąd. W dolnej części ekranu,<br />
w tzw. okienku komunikatów (ang. Message window) pojawił się<br />
komunikat, jaki rodzaj błędu został wykryty w Twoim programie. W<br />
danym przypadku komunikat brzmi:<br />
Declaration syntax error - Błąd w składni deklaracji<br />
Co to jest deklaracja?<br />
Pierwsza linia (wiersz) funkcji nazywa się deklaracją funkcji.<br />
Taka pierwsza linia zawiera informacje ważne dla kompilatora:<br />
nazwę funkcji oraz tzw. typy wartości używanych przez funkcję.<br />
Komunikat o błędzie oznacza, że nieprawidłowo została napisana<br />
nazwa funkcji lub nazwy typów wartości, którymi posługuje się<br />
funkcja. W naszym przypadku słowo void zostało przekręcone na<br />
"vod", a słowo to ma w <strong>C++</strong> specjalne znaczenie. Słowo "void"<br />
jest częścią języka <strong>C++</strong>, a dokładniej - słowem kluczowym (ang.<br />
keyword).<br />
[S] Function declaration - Deklaracja funkcji.<br />
Keyword - Słowo kluczowe.<br />
________________________________________________________________<br />
Function declaration - Deklaracja funkcji.<br />
Pierwszy wiersz funkcji jest nazywany deklaracją funkcji. Ten<br />
wiersz zawiera informacje dla kompilatora <strong>C++</strong> pozwalające<br />
poprawnie przetłumaczyć funkcję na kod maszynowy.<br />
Keyword - Słowo kluczowe.<br />
- 38-
to specjalne słowo wchodzące w skład języka programowania. Słowa<br />
kluczowe to słowa o zastrzeżonym znaczeniu, które można stosować<br />
w programach wyłącznie w przewidzianym dla nich sensie.<br />
________________________________________________________________<br />
Popraw błąd w tekście. Aby robocze okienko edytora stało się<br />
oknem aktywnym, wskaż kursorem myszki dowolny punkt w oknie<br />
edytora i naciśnij lewy klawisz myszki, albo naciśnij klawisz<br />
[F6]. Zmień słowo "vod" na "void". Przy pomocy klawiszy ze<br />
strzałkami umieść migający kursor po prawej stronie nawiasu {<br />
sygnalizującego ˙początek programu i naciśnij [Enter]. Spowoduje<br />
to wstawienie pomiędzy początek a koniec programu nowej pustej<br />
linii i umieszczenie kursora na początku nowego wiersza. Wpisz<br />
do nowego wiersza instrukcję oczyszczenia ekranu (odpowiednik<br />
instrukcji CLS w Basicu):<br />
clrscr();<br />
W <strong>C++</strong> clrscr() oznacza wywołanie funkcji czyszczącej roboczy<br />
ekran programu (User screen). Nazwa funkcji pochodzi od skrótu:<br />
CLeaR SCReen - czyść ekran.<br />
Że to funkcja - można rozpoznać po dodanej za nazwą parze<br />
nawiasów okrągłych - (). W tym jednak przypadku wiersz:<br />
clrscr();<br />
stanowi nie deklarację funkcji, lecz wywołanie funkcji (ang.<br />
function call). <strong>C++</strong> znalazłszy w programie wywołanie funkcji<br />
wykona wszystkie rozkazy, które zawiera wewnątrz funkcja<br />
clrscr(). Nie musisz przejmować się tym, z jakich rozkazów<br />
składa się funkcja clrscr(). Te rozkazy nie stanowią części<br />
Twojego programu, lecz są zawarte w jednym z "fabrycznych"<br />
plików bibliotecznych zainstalowanych wraz z kompilatorem <strong>C++</strong>.<br />
[S]<br />
Function - Funkcja<br />
Fuction call - Wywołanie funkcji<br />
________________________________________________________________<br />
Funkcja to coś przypominające mini-program. Funkcja zawiera<br />
listę rozkazów służących do wykonania typowych operacji (np.<br />
czyszczenie ekranu, wyświetlanie menu, wydruk, czy sortowanie<br />
listy imion). W programach posługujemy się zwykle wieloma<br />
funkcjami. Poznałeś już najważniejszą funkcję główną - main(). W<br />
C/<strong>C++</strong> możesz posługiwać się gotowymi funkcjami (tzw.<br />
bibliotecznymi) a także tworzyć nowe własne funkcje. Na razie<br />
będziemy posługiwać się gotowymi funkcjami dostarczanymi przez<br />
producenta wraz z kompilatorem <strong>C++</strong>.<br />
________________________________________________________________<br />
- 39-
Włącz kompilację i próbę uruchomienia programu.<br />
Kompilator stwierdzi, że program zawiera błędy.<br />
Naciśnij dowolny klawisz, by zniknęło okienko kompilacji.<br />
Kompilator napisał:<br />
Error: Function 'clrscr' should have a prototype<br />
(Funkcja 'clrscr' powinna mieć prototyp)<br />
[???] O co mu chodzi?<br />
________________________________________________________________<br />
Tzw. PROTOTYP funkcji to coś bardzo podobnego do deklaracji<br />
funkcji. Prototyp służy do przekazania kompilatorowi pewnych<br />
informacji o funkcji jeszcze przed użyciem tej funkcji w<br />
programie. Dla przykładu, gdy pisałeś pierwszą linię programu:<br />
void main(void)<br />
podałeś nie tylko nazwę funkcji - main, lecz także umieściliśmy<br />
tam dwukrotnie słowo void. Dokładnie o znaczeniu tych słów<br />
napiszemy w dalszej części książki. Na razie zwróćmy jedynie<br />
uwagę, że podobnych "dodatkowych" informacji dotyczących funkcji<br />
clrscr() w naszym programie nie ma.<br />
________________________________________________________________<br />
Zwróć uwagę, że zapisy:<br />
main() int main(void) main(void) {<br />
{ { } }<br />
}<br />
są całkowiecie równoważne. Fakt, że słowa kluczowe void (w nawiasie)<br />
i int (przed funkcją i tylko tam!) mogą zostać pominięte wskazuje, że są<br />
to wartości domyślne (default settings) przyjmowane automatycznie.<br />
Funkcja clrscr() została napisana przez programistów z firmy<br />
BORLAND i znajduje się gdzieś w osobnym pliku dołączonym do<br />
kompilatora <strong>C++</strong>. Aby móc spokojnie posługiwać się tą funkcją w<br />
swoich programach, powinieneś dołączyć do swojego programu<br />
informację w jakim pliku dyskowym znajduje się opis funkcji<br />
clrscr(). Taki (dość szczegółowy) opis funkcji nazywa się<br />
właśnie prototypem funkcji. Aby dodać do programu tę (niezbędną)<br />
informację<br />
* naciśnij [F6] by przejść do okna edytora<br />
* ustaw migający kursor na początku tekstu programu<br />
* naciśnij [Enter] dwukrotnie, by dodać dwie nowe puste linie do<br />
tekstu programu<br />
* na samym początku programu wpisz:<br />
- 40-
#include <br />
Takie specjalne linie (zwróć uwagę na podświetlenie)<br />
rozpoczynające się od znaku # (ASCII 35) nie są właściwie<br />
normalną częścią składową programu. Nie stanowią one jednej z<br />
instrukcji programu, mówiącej komputerowi CO NALEŻY ROBIĆ, lecz<br />
stanowią tzw. dyrektywę (rozkaz) dla kompillatora <strong>C++</strong> - W JAKI<br />
SPOSÓB KOMPILOWAĆ PROGRAM. Dyrektywa kompilatora (ang. compiler<br />
directive) powoduje dokonanie określonych działań przez<br />
kompilator na etapie tłumaczenia programu na kod maszynowy. W<br />
danym przypadku dyrektywa<br />
#include ....<br />
(ang. include - włącz, dołącz) powoduje włączenie we wskazane<br />
miejsce zawartości zewnętrznego tekstowego pliku dyskowego - np.:<br />
CONIO.H,<br />
(plik CONIO.H<br />
nazywany ˙także "plikiem nagłówkowym" znajduje się w podkatalogu<br />
\INCLUDE). Kompilator dołącza zawartość pliku CONIO.H jeszcze<br />
przed rozpoczęciem procesu kompilacji programu.<br />
Naciśnij kombinację klawiszy [Ctrl]+[F9]. Spowoduje to<br />
kompilację i uruchomienie programu (Run). Przykładowy program<br />
powinien tym razem przekompilować się bez błędów. Po dokonaniu<br />
kompilacji powinien szybko błysnąć ekran użytkownika. Po tym<br />
błysku powinien nastąpić powrót do roboczego środowiska IDE<br />
kompilatora <strong>C++</strong>. Jeśli nie zdążyłeś się przyjrzeć i chcesz<br />
spokojnie sprawdzić, co zrobił Twój program - naciśnij<br />
kombinację klawiszy [Alt]+[F5].<br />
Dzięki działaniu funkcji clrscr() ekran będzie całkowicie<br />
czysty.<br />
[S] Compiler directive - DYREKTYWA KOMPILATORA<br />
________________________________________________________________<br />
Dyrektywa kompilatora to rozkaz wyjaśniający kompilatorowi <strong>C++</strong> w<br />
jaki sposób dokonywać kompilacji programu. Dyrektywy kompilatora<br />
zawsze rozpoczynają się od znaku # (ang. hash).<br />
Kompilatory <strong>C++</strong> posiadają pewien dodatkowy program nazywany<br />
PREPROCESOREM. Preprocesor dokonuje przetwarzania tekstu<br />
programu jescze przed rozpoczęciem właściwej kompilacji.<br />
Dokładniej rzecz biorąc #include jest właściwie dyrektywą<br />
preprocesora (szczegóły w dalszej części książki).<br />
________________________________________________________________<br />
[Z] - Propozycje zadań do samodzielnego wykonania.<br />
________________________________________________________________<br />
1. Spróbuj poeksperymentować "zjadając" kolejno różne elementy w<br />
poprawnie działającym na początku programie:<br />
- 41-
- litera w nazwie funkcji<br />
- średnik na końcu wiersza<br />
- cudzysłów obejmujący tekst do wydrukowania<br />
- nawias ( lub ) w funkcji printf()<br />
- nawias klamrowy { lub }<br />
- znak dyrektywy #<br />
- całą dyrektywę #include <br />
Porównaj komunikaty o błędach i zgłaszaną przez kompilator<br />
liczbę błędów. Czy po przekłamaniu jednego znaku kompilator<br />
zawsze zgłasza dokładnie jeden błąd?<br />
________________________________________________________________<br />
______________________________________________________________________<br />
EOF<br />
- 42-
LEKCJA 6. NASTĘPNY PROGRAM - KOMPUTEROWA ARYTMETYKA.<br />
________________________________________________________________<br />
W trakcie tej lekcji napiszesz i uruchomisz następny program<br />
wykonujący proste operacje matematyczne.<br />
________________________________________________________________<br />
Aby przystąpić po wyjaśnieniach do pracy nad drugim programem,<br />
powinieneś wykonać następujące czynności:<br />
1. Zrób porządek na ekranie. Zamknij rozkazem Close z menu<br />
Window zbędne okna (możesz posłużyć się kombinacją [Alt]-[F3]).<br />
2. Rozwiń menu File.<br />
3. Wybierz z menu rozkaz Open...<br />
4. Wpisz do okienka tekstowego:<br />
A:\DRUGI.CPP<br />
5. Naciśnij [Enter].<br />
6. Wpisz do okienka edytora tekst programu:<br />
[P003.CPP ]<br />
/* Program przykladowy: _DRUGI.CPP */<br />
# include /* zwróć uwagę, że tu NIE MA [;] ! */<br />
# include /* drugi plik nagłówkowy */<br />
int main() /* tu tez nie ma średnika [;] ! */<br />
{<br />
float x, y;<br />
float wynik;<br />
clrscr();<br />
printf("Zamieniam ulamki zwykle na dziesietne\n");<br />
printf("\nPodaj licznik ulamka: ");<br />
scanf("%f", &x); /* pobiera liczbę z klawiatury */<br />
printf("\nPodaj mianownik ulamka: ");<br />
scanf( "%f", &y);<br />
wynik = x / y; /* tu wykonuje sie dzielenie */<br />
printf("\n %f : %f = %f", x, y, wynik);<br />
printf("\n nacisnij dowolny klawisz...\n");<br />
getch(); /* program czeka na nacisniecie klawisza. */<br />
return 0;<br />
}<br />
//
* Tekst komentarza */<br />
// Tekst komentarza<br />
w drugim przypadku ogranicznikiem pola komentarza jest koniec<br />
wiersza.<br />
* Spacjami i TABami możesz operować dowolnie. Kompilator<br />
ignoruje także puste miejsca w tekście. Nie należy natomiast<br />
stosować spacji w obrębie słów kluczowych i identyfikatorów.<br />
________________________________________________________________<br />
7. Skompiluj program [Alt]-[C], [M] lub [Enter].<br />
8. Popraw ewentualne błędy.<br />
9. Uruchom program rozkazem Run, naciskając [Alt]-[R], [R].<br />
10. Zapisz wersję źródłową programu DRUGI.CPP na dyskietkę A:\<br />
stosując tym razem SHORTCUT KEY - klawisz [F2].<br />
[S!] scanf() - SCANing Function - Funkcja SKANująca.<br />
________________________________________________________________<br />
Funkcja pobiera ze standardowego urządzenia wejścia- zwykle z<br />
klawiatury podaną przez użytkownika liczbę lub inny ciąg znaków.<br />
Działa podobnie do funkcji INPUT w Basicu, czy readln w Pascalu.<br />
* float - do Floating Point - "Pływający" - zmienny przecinek.<br />
Słowo kluczowe służące do tzw. DEKLARACJI TYPU ZMIENNEJ lub<br />
funkcji. Oznacza liczbę rzeczywistą np.: float x = 3.14;<br />
* int - od Integer - całkowity.<br />
Słowo kluczowe służące do deklaracji typu zmiennej lub funkcji.<br />
Oznacza liczbę całkowitą np.: 768.<br />
* #include - Włącz.<br />
Dyrektywa włączająca cały zewnętrzny plik tekstowy. W tym<br />
przypadku włączone zostały dwa tzw. pliki nagłówkowe:<br />
CONIO.H i STDIO.H.<br />
* CONIO.H - CONsole Input/Output.<br />
Plik nagłówkowy zawierający prototypy funkcji potrzebnych do<br />
obsługi standardowego Wejścia/Wyjścia na/z konsoli (CONsole).<br />
Plik zawiera między innymi prototyp funkcji clrscr(), potrzebnej<br />
nam do czyszczenia ekranu.<br />
*STDIO.H - STanDard Input/Output<br />
Plik nagłówkowy zawierający prototypy funkcji potrzebnych do<br />
obsługi standardowego Wejścia/Wyjścia na/z konsoli (Input -<br />
Wejście, Output - Wyjście). Plik zawiera między innymi prototyp<br />
funkcji printf(), potrzebnej nam do drukowania wyników na<br />
ekranie.<br />
- 44-
eturn - słowo kluczowe: Powrót, zwrot.<br />
Po wykonaniu programu liczba 0 (tak kazaliśmy programowi<br />
rozkazem return 0;) jest zwracana do systemu operacyjnego, w<br />
naszym przypadku do DOSa. Zwróć uwagę, że nie pojawiło się tym<br />
razem ostrzeżenie WARNING podczas kompilacji.<br />
________________________________________________________________<br />
OPERATORY ARYTMETYCZNE <strong>C++</strong>.<br />
<strong>C++</strong> potrafi oczywiście nie tylko dzielić i mnożyć. Oto tabela<br />
operatorów arytmetycznych c i <strong>C++</strong>.<br />
OPERATORY ARYTMETYCZNE języka <strong>C++</strong>.<br />
________________________________________________________________<br />
Operator Nazwa Tłumaczenie Działanie<br />
________________________________________________________________<br />
+ ADDition Dodawanie Suma liczb<br />
- SUBstraction Odejmowanie Różnica liczb<br />
* MULtiplication Mnożenie Iloczyn liczb<br />
/ DIVision Dzielenie Iloraz liczb<br />
% MODulus Dziel Modulo Reszta z dzielenia<br />
________________________________________________________________<br />
Przykładowe wyniki niektórych operacji arytmetycznych.<br />
________________________________________________________________<br />
Działanie (zapis w <strong>C++</strong>) Wynik działania<br />
________________________________________________________________<br />
5 + 7 12<br />
12 - 7 5<br />
3 * 8 24<br />
10 / 3 3.333333<br />
10 % 3 1<br />
________________________________________________________________<br />
[???] Czym różni się dzielenie / od % ?<br />
________________________________________________________________<br />
Operator dzielenia modulo % zamiast wyniku dzielenia - daje<br />
rzesztę z dzielenia. Dla przykładu, dzielenie liczby 14 przez<br />
liczbę 4 daje wynik 3, reszta z dzielenia 2. Wynik operacji<br />
14%4<br />
będzie więc wynosić 2. Operator ten jest niezwykle przydatny np.<br />
przy sprawdzaniu podzielności, skalowaniu, określaniu zakresów<br />
liczb przypadkowych, itp..<br />
- 45-
Przykłady generacji liczb pseudolosowych wybiegają nieco w przyszłość,<br />
ale postanowiłem w Samouczku umieścić je razem. Po przestudiowaniu<br />
tworzenia pętli programowych możesz wrócić do tej lekcji i rozważyć<br />
przykłady po raz wtóry.<br />
Przykład 1:<br />
randomize();<br />
int X=ramdom();<br />
X = X % 10;<br />
Przykład 2:<br />
---------------------<br />
#include /* Zwróc uwagę na dołączony plik */<br />
#include <br />
main()<br />
{<br />
int i;<br />
}<br />
printf("Dziesięć liczb pseudo-losowych od 0 do 99\n\n");<br />
for(i=0; i
Zwróć uwagę, że to randomize() uruchamia generator liczb pseudolosowych,<br />
czyli jakgdyby "włącza bęben maszyny losującej".<br />
________________________________________________________________<br />
Wykonaj z programem DRUGI.CPP kilka eksperymentów.<br />
[Z]<br />
________________________________________________________________<br />
1. Zamień operator dzielenia na operator mnożenia [*]:<br />
wynik = x * y; /* tu wykonuje sie mnożenie */<br />
i napis w pierwszej funkcji printf() na np. taki:<br />
printf( "Wykonuje mnozenie liczb" );<br />
Uruchom program. Sprawdź poprawność działania programu w<br />
szerokim zakresie liczb. Przy jakiej wielkości liczb pojawiają<br />
się błędy?<br />
2. Zmień nazwy zmiennych x, y, wynik na inne, np.:<br />
to_jest_liczba_pierwsza,<br />
to_jest_liczba_druga,<br />
itp.<br />
Czy <strong>C++</strong> poprawnie rozpoznaje i rozróżnia takie długie nazwy?<br />
Kiedy zaczynają się kłopoty? Sprawdź, czy można w nazwie<br />
zmiennej użyć spacji? Jaki komunikat wyświetli kompilator?<br />
________________________________________________________________<br />
[???] PRZEPADŁ PROGRAM ???<br />
________________________________________________________________<br />
Nie przejmuj się. Wersja początkowa programu DRUGI.CPP jest na<br />
dyskietce dołączonej do niniejszej książki (tam nazywa się<br />
DRUGI.CPP).<br />
Zwróć uwagę, że kompilator <strong>C++</strong> tworzy automatycznie kopie<br />
zapasowe plików źródłowych z programami i nadaje im standardowe<br />
rozszerzenie *.BAK. Zanim zatem zaczniesz się denerwować,<br />
sprawdź, czy kopia np. DRUGI.BAK nie jest właśnie tą wersją<br />
programu, która Ci "przepadła".<br />
________________________________________________________________<br />
__________________________________________________________________<br />
EOF<br />
- 47-
LEKCJA 7. Z czego składa się program.<br />
_______________________________________________________________<br />
W trakcie tej lekcji:<br />
* Dowiesz się co robić, jeśli tęsknisz za Pascalem.<br />
* Zapoznasz się wstępnie z preprocesorem <strong>C++</strong>.<br />
* Poznasz dokładniej niektóre elementy języka <strong>C++</strong>.<br />
_______________________________________________________________<br />
Zanim zagłębimy się w szczegóły działania preprocesora i<br />
kompilatora, dla zilustrowania mechanizmu działania dyrektyw<br />
popełnimy żart programistyczny. Nie ma nic gorszego niż spalić<br />
dobry żart, upewnijmy się więc najpierw, czy nasza<br />
"czarodziejska kula" jest gotowa do magicznych sztuczek.<br />
Sprawdź, czy na dyskietce znajdują się pliki<br />
A:\PASCAL.H<br />
A:\POLTEKST.H<br />
Jeśli nie, to przed zabawą w magiczne sztuczki programistyczne<br />
musisz odtworzyć te pliki z zapasowej kopii dyskietki, którą<br />
sporządziłeś przed rozpoczęciem LEKCJI 1.<br />
Jeśli masz już oba pliki, to wykonaj następujące czynności:<br />
1. Włóż do napędu A: dyskietkę z plikami PASCAL.H i POLTEKST.H.<br />
2. Uruchom kompilator <strong>C++</strong>.<br />
PROGRAMY HOKUS.EXE i POKUS.EXE - czyli sztuczki z Preprpcesorem<br />
<strong>C++</strong><br />
1. Zrób porządek na ekranie - pozamykaj zbędne okna.<br />
2. Naciśnij klawisz [F3]. Pojawi się znajome okienko dialogowe<br />
"Open".<br />
3. Wpisz do okienka tekstowego nazwę nowego programu:<br />
A:\HOKUS.C<br />
i naciśnij [Enter].<br />
4. Wpisz następujący tekst programu:<br />
[P004.CPP]<br />
#include <br />
Program<br />
Begin<br />
Write("Ten program jest podobny");<br />
Write(" do Turbo Pascala ");<br />
Write(" tak tez mozna pisac w BORLAND <strong>C++</strong> !");<br />
Readln;<br />
End<br />
5. Uruchom program [Ctrl]-[F9]. Jeśli wystąpią błędy, skoryguj<br />
ewentualne niezgodności z oryginałem. Ostrzeżenie "WARNING"<br />
możesz zignorować.<br />
- 48-
UWAGA: MUSI ZOSTAĆ ZACHOWANA IDEALNA ZGODNOŚĆ z tekstem<br />
oryginału!<br />
6. Uruchom program rozkazem Run [Alt]-[R], [Enter]. Zwróć uwagę,<br />
że powtórna kompilacja przebiega szybciej, jeśli w międzyczasie<br />
nie dokonałeś zmian w programie.<br />
7. Zamknij okno edytora rozkazem Close (z menu Window). Zapisz<br />
program HOKUS.CPP w wersji źródłowej na dyskietkę A:.<br />
A teraz następna sztuczka, na którą pozwala <strong>C++</strong>.<br />
Utworzymy następny program POKUS.CPP.<br />
1. Wykonaj czynności z pp. 1 i 2 z poprzedniego przykładu.<br />
2. Otwórz okienko nowego programu - File | Open (np. klawiszem<br />
[F3]) i wpisz nazwę programu. Możesz zastosować również File |<br />
New.<br />
A:\POKUS.CPP<br />
3. Naciśnij [Enter].<br />
4. Wpisz tekst programu:<br />
[P005.CPP]<br />
# include <br />
program<br />
poczatek<br />
czysty_ekran<br />
drukuj ("Ten program - POKUS.CPP ");<br />
drukuj ("Jest napisany po polsku ");<br />
drukuj ("a mimo to Turbo <strong>C++</strong> go rozumie!");<br />
czekaj;<br />
koniec<br />
5. Uruchom program [Alt]-[R], [R]. Jeśli wystąpią błędy,<br />
skoryguj ewentualne niezgodności z oryginałem. Ostrzeżenie<br />
"WARNING" możesz zignorować.<br />
UWAGA: MUSI ZOSTAĆ ZACHOWANA IDEALNA ZGODNOŚĆ!<br />
6. Zamknij okno edytora rozkazem Close (z menu Window). Zapisz<br />
program HOKUS.C w wersji źródłowej na dyskietkę A:.<br />
WYJAŚNIENIE SZTUCZEK - PREPROCESOR <strong>C++</strong> CPP.EXE.<br />
A teraz wyjaśnienie naszych magicznych sztuczek. Jeśli jesteś<br />
niecierpliwy, na pewno już sam zajrzałeś do plików PASCAL.H i<br />
POLTEKST.H, bo jest chyba oczywiste od początku, że to tam<br />
właśnie musi ukrywać się to wszystko, co pozwala nam robić nasze<br />
hokus-pokus. Skorzystaliśmy z pewnej nie występującej ani w<br />
Pascalu, ani w Basicu umiejętności języków C i <strong>C++</strong> - a<br />
mianowicie z PREPROCESORA.<br />
- 49-
Najczęściej stosowanymi dyrektywami preprocesora są:<br />
# include - włącz<br />
i<br />
# define - zdefiniuj<br />
Do rozpoznania dyrektyw preprocesora służy znak (#) - HASH.<br />
Zwróć uwagę, że zapisy<br />
#include<br />
# include<br />
są całkowicie równoważne. Poza tym dyrektywy preprocesora nie<br />
kończą się średnikiem.<br />
Działanie preprocesora (czyli wstępne przetwarzanie tekstu<br />
programu jeszcze przed przystąpieniem do kompilacji) polega na<br />
zastąpieniu w tekście programu jednych łańcuchów znaków przez<br />
inne. Takie pary możemy "zadać" preprocesorowi właśnie dyrektywą<br />
#define. Nasze nagłówki wyglądają następująco:<br />
PASCAL.H:<br />
_______________________________________________________________<br />
# include <br />
# define Program main()<br />
# define Begin {<br />
# define Writeln printf<br />
# define Readln getch()<br />
# define End }<br />
________________________________________________________________<br />
POLTEKST.H:<br />
________________________________________________________________<br />
# include <br />
# define program main()<br />
# define poczatek {<br />
# define koniec }<br />
# define czysty_ekran clrscr();<br />
# define drukuj printf<br />
# define czekaj getch()<br />
________________________________________________________________<br />
Zwróć uwagę, że warunkiem poprawnego zadziałania preprocesora<br />
jest zrezygnowanie ze spacji wewnątrz łańcuchów znakowych,<br />
spacje bowiem w preprocesorze rozdzielają dwa łańcuchy znaków - np.<br />
"drukuj"<br />
- ten ZA KTÓRY CHCEMY COŚ PODSTAWIĆ oraz np.<br />
"printf"<br />
- ten, KTÓRY NALEŻY PODSTAWIAĆ. Często w programach<br />
- 50-
zauważysz łańcuchy znaków pisane w dość specjalny sposób:<br />
napisy_w_których_unika_się_spacji.<br />
ELEMENTY PROGRAMU W JĘZYKU <strong>C++</strong>.<br />
Uogólniając, program w języku <strong>C++</strong> składa się z następujących<br />
elementów:<br />
1. Dyrektyw preprocesora. Przykład:<br />
#define drukuj printf<br />
Działanie: W tekście programu PONIŻEJ niniejszej dyrektywy<br />
zastąp wszystkie łańcuchy znaków "drukuj" łańcuchami znaków<br />
"printf".<br />
#include <br />
Działanie: W to miejsce pliku wstaw zawartość pliku tekstowego<br />
NAZWA.ROZ z katalogu KATALOG na dysku D:.<br />
2. Komentarzy. Przykład:<br />
// Tu obliczamy sumę lub /*To jest komentarz*/<br />
3. Deklaracji. Przykład:<br />
KAŻDY PROGRAM musi zawierać deklarację funkcji main (ang. main -<br />
główna). Funkcja ta często jest bezparametrowa, co można<br />
zaakcentować wpisując w nawiasy słowo kluczowe void:<br />
main(void)<br />
lub pisząc puste nawiasy:<br />
main()<br />
4. Instrukcji.<br />
i++;<br />
Działanie: Dokonaj inkrementacji zmiennej i, tzn. wykonaj<br />
operację i:=i+1<br />
[???] Dla dociekliwych - kilka słów o funkcji main()<br />
________________________________________________________________<br />
Funkcja main() występuje najczęściej w następujących<br />
(równoważnych) postaciach:<br />
main() int main() int main(void)<br />
- program w momencie uruchomienia nie pobiera żadnych argumentów<br />
z wiersza rozkazu --> () lub (void)<br />
- program zwraca po zakończeniu jedną licznę (int = integer -<br />
- 51-
liczba całkowita) do systemu operacyjnego informując go w taki<br />
sposób, czy wykonał się do końca i bezbłędnie i czy można go<br />
usunąć z pamięci (bywają także programy rezydujące w pamięci -<br />
tzw. TSR, o czym system operacyjny powinien "wiedzieć").<br />
void main()<br />
void main(void)<br />
- program nie pobiera i nie zwraca żadnych paramatrów.<br />
Główna funkcja main() może w środowisku okienkowym przeobrazić<br />
się w główną funkcję okienkową:<br />
WinMain(.....)<br />
a w środowisku obiektowym w<br />
OwlMain(....)<br />
OWL - biblioteka obiektów dla Windows - Object Windows Library.<br />
W nawiasach funkcji main(), WinMain() i OwlMain() mogą pojawić<br />
się parametry (argumenty) pobierane przez program w momencie<br />
uruchomienia z wiersza rozkazu lub od środowiska operacyjnego<br />
(szczegóły w dalszej części książki).<br />
Programy w <strong>C++</strong> mogą składać się z wielu plików dyskowych. Typowy<br />
program zawiera. Nazywa się to zwykle projektami wielomodułowymi<br />
- a poszczególne pliki - modułami lub elementami składowymi<br />
projektu:<br />
* plik nagłówkowy - NAZWA.H<br />
* moduł główny - NAZWA.CPP (ten i tylko ten zawiera funkcję<br />
main())<br />
* moduły pomocnicze - NAZWA2.CPP, NAZWA3.CPP, itp<br />
* pliki z zasobami typu menu, okienka dialogowe, itp - NAZWA.RC,<br />
NAZWA.DLG<br />
* wreszcie plik instruktażowy - jak z tego wszystkiego zrobić<br />
końcową aplikację. W zależności od wersji kompilatora pliki<br />
instruktażowe mogą mieć nazwy: NAZWA.PRJ (Project - BORLAND),<br />
NAZWA.IDE, a dla programu MAKE - MAKEFILE, NAZWA.MAK, NAZWA.NMK,<br />
itp.<br />
W środowisku Windows występuje jeszcze zwykle w składzie<br />
projektów aplikacji tzw. plik definicji sposobu wykorzystania<br />
zasobów - NAZWA.DEF.<br />
________________________________________________________________<br />
[S!] void - czyli nijaki.<br />
________________________________________________________________<br />
Słowa kluczowe:<br />
void - pusty, wolny, nieokreślony, avoid - unikać.<br />
main - główny, główna.<br />
return - powrót, zwrot.<br />
Nazwa funkcji:<br />
- 52-
exit() - wyjście.<br />
________________________________________________________________<br />
Po nazwie funkcji main() NIE NALEŻY stawiać średnika (;).<br />
Przy pomocy tej funkcji program kontaktuje się z systemem<br />
operacyjnym. Parametry funkcji main, to te same parametry z<br />
którymi uruchamiamy nasz program w systemie DOS. Np. rozkaz<br />
FORMAT A:<br />
oznacza, że do programu przekazujemy parametr A:.<br />
Ponieważ w każdym programie oprócz nagłówka funkcji:<br />
main(void)<br />
podajemy również tzw. ciało funkcji, np.:<br />
{<br />
printf("wydrukuj cokolwiek");<br />
return 0;<br />
}<br />
jest to jednocześnie DEFINICJA FUNKCJI main().<br />
Zwróć uwagę, że funkcja printf() nie jest w powyższym<br />
przykładzie w żaden sposób ani deklarowana ani definiowana.<br />
Wiersz:<br />
printf("pisz!");<br />
stanowi WYWOŁANIE funkcji printf() z parametrem 'pisz!' -<br />
łańcuchem znaków, który należy wydrukować.<br />
W <strong>C++</strong> nawet jeśli nawiasy przeznaczone w funkcji na przekazanie<br />
jej argumentów są puste - muszą być obecne. Poprawne wywołanie<br />
funkcji w języku <strong>C++</strong> może mieć następującą formę:<br />
nazwa_funkcji();<br />
nazwa_funkcji(par1, par2, par3, .....);<br />
zmienna = nazwa_funkcji(par1, par2, ...);<br />
Funkcja w momencie jej wywołania uzyskuje przekazane jej<br />
parametry. Są to tzw. ARGUMENTY FUNKCJI. Aby to wszystko<br />
bardziej przypominało to, co znasz ze szkoły popatrzmy na<br />
analogię. W zapisie:<br />
y = sin(x) lub y = sin(90)<br />
x - oznacza argument funkcji, który może być zmienną (w szkole<br />
nazywałeś zmienne "niewiadomymi")<br />
y - oznacza wartość zwracaną "po zadziałaniu" funkcji<br />
sin() - oznacza nazwę funkcji. Zastosowanie funkcji będziemy w<br />
programach nazywać "wywołaniem funkcji".<br />
- 53-
Język <strong>C++</strong> operuje wyłącznie pojęciem FUNKCJI. W C ani w <strong>C++</strong> nie<br />
ma podziału na FUNKCJE i PROCEDURY.<br />
Każda funkcja może być w programie wywoływana wielokrotnie.<br />
Każde wywołanie funkcji może następować z innymi argumentami.<br />
Funkcja może w wyniku swojego działania zmieniać wartość jakiejś<br />
zmiennej występującej w programie. Mówimy wtedy, że funkcja<br />
ZWRACA wartość do programu. Funkcja main() jest funkcją<br />
szczególną, która "zwraca" wartość do systemu operacyjnego, w<br />
którym pracuje program. Zapis:<br />
main() lub int main()<br />
{ {<br />
return 5; exit(5);<br />
} }<br />
oznacza:<br />
1. Funkcja main jest bezparametrowa (nie przyjmuje żadnych<br />
argumentów z zewnątrz).<br />
2. Funkcja main zwraca jako wynik swojego działania liczbę<br />
całkowitą typu int (ang. INTeger - całkowita). Zwróć uwagę, że<br />
jest to domyślny sposób działania funkcji main(). Jeśli nie<br />
napiszemy przed funkcją main() słowa "int" - kompilator <strong>C++</strong> doda<br />
je sobie automatycznie. Jeśli świadomie nie zamierzamy zwracać<br />
do systemu operacyjnego żadnych informacji - musimy wyraźnie<br />
napisać tam "void".<br />
3. Funkcja zwróci do systemu DOS wartość 5. Zwróć uwagę na<br />
istotną różnicę formalną, Słowo "return" jest słowem kluczowym<br />
języka C, natomiast słowo "exit" jest nazwą funkcji exit().<br />
Zastosowanie tej funkcji w programie wymaga dołączenia pliku<br />
nagłówkowego z jej prototypem.<br />
Ponieważ nasz kurs języka <strong>C++</strong> rozpoczęliśmy od programu z<br />
funkcją printf() i zapewne będzie nam ona towarzyszyć jeszcze<br />
długo, pora poświęcić jej trochę uwagi.<br />
FUNKCJA printf().<br />
Jest to funkcja FORMATOWANEGO wyjścia na standardowe urządzenie<br />
wyjścia (ang. stdout - STandarD OUTput). Definicja - ściślej<br />
tzw. PROTOTYP tej funkcji znajduje się w pliku nagłówkowym<br />
STDIO.H. Wniosek praktyczny: Każdy program korzystający z<br />
funkcji printf() powinien zawierać dyrektywę preprocesora:<br />
#include <br />
zanim nastąpi wywołanie funkcji printf().<br />
[???] A JEŚLI ZAPOMNIAŁEM O ???<br />
________________________________________________________________<br />
Możesz nadać plikowi z tekstem żródłowym programu rozszerzenie<br />
.C zamiast .CPP. W kompilatorach <strong>Borland</strong>a powoduje to przy<br />
- 54-
domyślnych ustawieniach kompilatora wywołanie kompilatora C<br />
zamiast <strong>C++</strong>. C jest bardziej tolerancyjny i dokona kompilacji<br />
(wyświetli jedynie komunikat ostrzegawczy - Warning). Kompilator<br />
<strong>C++</strong> jest mniej tolerancyjny. Jeśli zapomnisz dołączyć odpowiedni<br />
plik nagłówkowy może pojawić się komunikat:<br />
Error: Function printf() should have a prototype in function<br />
main<br />
(Funkcja printf() powinna mieć prototyp)<br />
Więcej o zawartości i znaczeniu plików nagłówkowych *.h dowiesz<br />
się z następnych lekcji. Na razie postaraj się pomiętać o<br />
dołączeniu wskazanego w przykładzie pliku.<br />
________________________________________________________________<br />
[???] Skąd to wiadomo?<br />
________________________________________________________________<br />
Jeśli masz wątpliwości, jaki plik nagłówkowy należałoby dołączyć<br />
- najprościej zajrzeć do systemu pomocy - Help. Na pasku<br />
głównego menu w IDE masz napis Help. Menu Help możesz rozwinąć<br />
myszką lub naciskając kombinację klawiszy [Alt]+[H]. Jeśli w<br />
menu wybierzesz rozkaz Index (Spis) przeniesiesz się do okienka<br />
z alfabetycznym spisem haseł. Są tam słowa kluczowe, nazwy<br />
funkcji i jeszcze wiele innych interesujących rzeczy. Powinieneś<br />
teraz wykonać następujące czynności:<br />
* posługując się klawiszami kursora (ze strzałkami) odszukać w<br />
spisie nazwę funkcji<br />
albo<br />
* rozpocząć pisanie nazwy funkcji na klawiaturze (system Help<br />
sam wyszuka w spisie wypisaną w ten sposób nazwę)<br />
* nacisnąć [Enter]<br />
Przeniesiesz się do okienka opisu danej funkcji. Na samym<br />
początku w okienku każdej funkcji podana jest nazwa pliku<br />
nagłówkowego, w którym znajduje się prototyp funkcji. Nawet<br />
jeśli nie jesteś biegłym anglistą, łatwo rozpoznasz pliki<br />
nagłówkowe - po charakterystycznych rozszerzeniach .H (rzadziej<br />
.HPP. Charakterystyczne rozszerzenie *.H pochodzi od "plik<br />
nagłówkowy" - ang. Header file).<br />
________________________________________________________________<br />
Funkcja printf() zwraca wartość całkowitą typu int:<br />
* liczbę bajtów przesłanych na standardowe urządzenie wyjścia;<br />
* w przypadku wystąpienia błędu - kod znaku EOF.<br />
[S!]<br />
EOF - End Of File - znak końca pliku.<br />
EOL - End Of Line - znak końca linii.<br />
Indicator - znak, wskaźnik (nie mylić z pointerem !)<br />
- 55-
[???] SKĄD TO WIADOMO ?<br />
________________________________________________________________<br />
Kody EOF, EOL są tzw. predefiniowanymi stałymi. Ich szyfrowanie<br />
(przypisywanie tym identyfikatorom określonej stałej wartości<br />
liczbowej) dokonuje się z zastosowaniem preprocesora <strong>C++</strong>.<br />
To, że nie musisz się zastanawiać ile to właściwie jest EOF<br />
(zero ? czy -1 ?) zawdzięczamy też dołączanym plikom typu *.H, w<br />
których np. przy użyciu dyrektywy #define zostały PREDEFINIOWANE<br />
(zdefiniowane wstępnie) niektóre stałe. Jeśli jesteś bardzo<br />
dociekliwy, zajrzyj do wnętrza pliku STDIO.H (view, edit, type).<br />
Znajdziesz tam między innymi taki wiersz:<br />
#define EOF (-1) //End of file indicator<br />
________________________________________________________________<br />
Składnia prototypu (ang. syntax):<br />
int printf(const char *format [arg1, arg2,.....]);<br />
lub trochę prościej:<br />
printf(format, arg1, arg2,.....argn);<br />
Liczba argumentów może być zmienna.<br />
<strong>C++</strong> oferuje wiele funkcji o podobnym działaniu - np.:<br />
cprintf(), fprintf(), sprintf(), vprintf(), vsprintf(), itp.<br />
Ponieważ FORMAT brzmi może trochę obco, nazwijmy go WZORCEM. Jak<br />
wiesz, wszystkie informacje przechowywane są w pamięci komputera<br />
jako ciągi zer i jedynek. Jest to forma trochę niewygodna dla<br />
człowieka, więc zanim informacja trafi na ekran musi zostać<br />
zamieniona na postać dla nas wygodniejszą - np. na cyfry<br />
dziesiętne, litery itp.. Taki proces nazywany jest KONWERSJĄ, a<br />
podany w funkcji printf() FORMAT - WZORZEC to upraszczając,<br />
rozkaz dokonania takiej właśnie konwersii. Możesz więc zarządać<br />
przedstawienia liczby na ekranie w postaci np. SZESNASTKOWEJ lub<br />
DZIESIĘTNEJ - tak, jak Ci wygodniej. Wzorce konwersji w<br />
najprostszym przypadku mają postać %s, %d, %f, itp.:<br />
I tak:<br />
%s - wyprowadź łańcuch znaków (s - String - łańcuch)<br />
Przykład:<br />
printf("%s","jakis napis");<br />
ale także<br />
printf("Jakis napis");<br />
- 56-
ponieważ format "%s" jest formatem domyślnym dla funkcji<br />
printf().<br />
Przykład:<br />
printf("%39s","jakis napis");<br />
spowoduje uzupełnienie napisu spacjami do zadanej długości 39<br />
znaków (Sprawdź!). Funkcja printf() operuje tzw. POLEM<br />
WYJŚCIOWYM. Długość pola wyjściowego możemy określić przy pomocy<br />
liczb wpisanych pomiędzy znaki % oraz typ - np. s. Możemy także<br />
określić ilość cyfr przed i po przecinku.<br />
%c - wyprowadź pojedynczy znak (c - Character - znak)<br />
Przykład:<br />
printf("%c",'X');<br />
(spowoduje wydrukowanie litery X)<br />
%d - wyprowadź liczbę całkowitą typu int w postaci dziesiętnej<br />
€€€€€(d - Decimal - dziesiętny).<br />
Przykład:<br />
printf("%d", 1994);<br />
%f - wyprowadź liczbę rzeczywistą typu float w postaci<br />
dziesiętnej (f - Floating point - zmienny przecinek).<br />
Przykład:<br />
printf("%f", 3.1416);<br />
printf("%f3.2", 3.14159);<br />
%o - wyprowadź liczbę całkowitą typu int w postaci ósemkowej<br />
(o - Octal - ósemkowa).<br />
Przykład:<br />
printf("%o", 255);<br />
%x - wyprowadź liczbę całkowitą typu int w postaci szesnastkowej<br />
€€€€€(x - heXadecimal - szesnastkowa).<br />
%x lub %X - cyfry szesnastkowe a,b,c,d,e,f lub A,B,C,D,E,F.<br />
%ld - liczba całkowita "długa" - long int.<br />
%Lf - liczba rzeczywista poczwórnej precyzji typu long double<br />
float.<br />
%e - liczba w formacie wykładniczym typu 1.23e-05 (0.0000123)<br />
%g - automatyczny wybór formatu %f albo %e.<br />
- 57-
Po przytoczeniu przykładów uogólnijmy sposób zastosowania wzorca<br />
formatu:<br />
%[przełączniki][szerokość_pola][.precyzja][rozmiar]Typ<br />
Posługując się różnymi sposobami formatowania liczb możemy<br />
zażądać wydrukowania liczb w najwygodniejszej dla nas formie. W<br />
programie przykładowym dokonujemy zamiany liczb dziesiętnych na<br />
szesnastkowe.<br />
[P006.CPP]<br />
// Program przykladowy 10na16.CPP<br />
#include <br />
#include <br />
int liczba;<br />
int main()<br />
{<br />
clrscr();<br />
printf("Podaj liczbe dziesietna calkowita ? \n");<br />
scanf("%d", &liczba);<br />
printf("\nSzesnastkowo to wynosi: ");<br />
printf("%x",liczba);<br />
getch();<br />
return 0;<br />
}<br />
Ten program pozwala zamienić dziesiętne liczby całkowite na<br />
liczby szesnastkowe. Zakres dostępnych liczb wynika z<br />
zadeklarowanego typu int. Więcej na ten temat dowiesz się z<br />
następnych lekcji. Spróbujmy odwrotnie:<br />
[P007.CPP]<br />
// Program przykladowy 16na10.CPP<br />
//UWAGA: Sam dołącz pliki nagłówkowe<br />
int liczba;<br />
int main()<br />
{<br />
clrscr();<br />
printf("Podaj liczbe SZESNASTKOWA-np. AF - DUZE LITERY: \n");<br />
scanf("%X", &liczba);<br />
printf("%s","\nDziesietnie to wynosi: ");<br />
printf("%d",liczba);<br />
getch();<br />
return 0;<br />
}<br />
Myślę, że program 16NA10.CPP można pozostawić bez dodatkowego<br />
- 58-
komentarza. Zwróć uwagę, że funkcja scanf() "formatuje" dane<br />
wejściowe bardzo podobnie do funkcji printf(). Pewnie dziwi Cię<br />
trochę "dualny" zapis:<br />
liczba i &liczba.<br />
Zagadka zostanie niebawem wyjaśniona. W trakcie następnych<br />
Lekcji zajmiemy się dokładniej zmiennymi, i ich rozmieszczeniem<br />
w pamięci a na razie wracamy do funkcji printf().<br />
Jako się rzekło wcześniej - funkcja printf() może mieć wiele<br />
argumentów. Pozwala nam to przy pomocy jednego wywołania funkcji<br />
wyprowadzać złożone napisy.<br />
Przykład:<br />
printf("Iloczyn 3 %c 5 %8s %d", '*', "wynosi ",15);<br />
Działanie:<br />
"Iloczyn_3_ - wyprowadź jako łańcuch znaków.<br />
%c - tu wyprowadź pojedynczy znak - '*'.<br />
_5_ - wyprowadź jako łańcuch znaków.<br />
%8s - wyprowadź łańcuch "wynosi_" uzupełniając go z przodu<br />
spacjami do długości 8 znaków.<br />
%d - wyprowadź 15 jako liczbę dziesiętną.<br />
UWAGA: Znakiem podkreślenia w tekście książki "_" oznaczyłem<br />
spację, spacja to też znak.<br />
Przykład:<br />
printf("Iloczyn 3 %c 5 %9s %f", 'x', "wynosi ", 3*5);<br />
Zwróć uwagę, że tym razem kazaliśmy komputerowi samodzielnie<br />
policzyć ile wynosi nasz iloczyn, tzn. zastosowaliśmy jako<br />
argument funkcji printf() nie stałą, a WYRAŻENIE. Działanie<br />
możesz prześledzić przy pomocy programu przykładowego:<br />
[P008.CPP]<br />
// Program WYRAZ.CPP - Dołącz pliki nagłówkowe<br />
int main()<br />
{<br />
clrscr();<br />
printf("Skomplikowany napis:\n");<br />
printf("Iloczyn 3 %c 5 %8s %d", '*', "wyniosi ", 15);<br />
getch();<br />
printf("\nWyrazenie jako argument:\n");<br />
printf("Iloczyn 3 %c 5 %9s %d", 'x', "wynosi ", 3*5);<br />
printf("\n\n\n");<br />
printf("Przyjrzyj sie i nacisnij klawisz...");<br />
getch();<br />
return 0;<br />
- 59-
}<br />
Wyjaśnijmy jeszcze jedno "dziwactwo" - znaki sterujące<br />
rozmieszczeniem napisów na ekranie. Oto tabelka z najczęściej<br />
używanymi znakami specjalnymi:<br />
________________________________________________________________<br />
Znak Nazwa Działanie<br />
________________________________________________________________<br />
\n New Line Przejście na początek nowego wiersza<br />
\b BackSpace Cofnięcie kursora o jeden znak<br />
\f Form feed O stronicę w dół<br />
\r Carriage return Powrót na początek bież. wiersza<br />
\t Horizontal Tab Tabulacja pozioma<br />
\v Vertical Tab Tabulacja pionowa<br />
\a Sound a beep Pisk głośniczka<br />
\\ Displ. backslash Wyświetl znak \<br />
\' Display ' Wyświetl znak ' (apostrof)<br />
\" Display " Wyświetl znak " (cudzysłów)<br />
________________________________________________________________<br />
UWAGA: Trzy ostatnie "backlash-kody" pozwalają wyprowadzić na<br />
ekran znaki specjalne \ ' i ", co czasami się przydaje.<br />
Szczególnie \\ jest często przydatny.<br />
[Z]<br />
Spróbuj samodzielnie:<br />
1. Napisać i uruchomić program wykonujący konwersję liczb<br />
ósemkowych na dziesiętne i odwrotnie.<br />
2. Przy pomocy pojedynczego wywołania funkcji printf()<br />
wydrukować kilka złożonych napisów typu:<br />
* suma 2+4 to 6<br />
* działanie 5*7*27+6-873 daje wynik...( właśnie, ile?).<br />
3. Sprawdź działanie tabulacji pionowej \v. Ile to wierszy?<br />
[???] DYSKIETKA NIE JEST Z GUMY !!!<br />
________________________________________________________________<br />
Jeśli podczas kompilacji programów w okienku będzie się<br />
uporczywie, bez widocznego powodu pojawiał napis "Errors" -<br />
błędy, a w okienku komunikatów "Message" pojawi się napis:<br />
Fatal A:\PROGRAM.C: Error writing output file<br />
(Fatalny błąd podczas kompilacji pliku A:\PROGRAM.C: Błąd przy<br />
zapisie pliku wyjściowego),<br />
to znak, że na dyskietce zabrakło miejsca. Pora zmienić katalog<br />
wyjściowy kompilatora <strong>C++</strong>. Aby to zrobić należy:<br />
1. Rozwinąć menu Option - [Alt]-[O].<br />
2. Wybrać rozkaz Directories... - [D].<br />
3. Przejść do okienka "Output Directory" - 2 razy [Tab].<br />
4. Wpisać do okienka katalog z dysku stałego, np.: C:\<br />
5. Nacisnąć [Enter].<br />
- 60-
6. Powtórzyć kompilację programu, przy której nastąpiło<br />
przepełnienie dyskietki.<br />
7. Usunąć z dyskietki A: zbędne pliki *.EXE (TYLKO *.EXE !!!).<br />
Oczywiście lepiej posługiwać się własnym katalogiem na dysku<br />
stałym, ale dysk też niestety nie jest z gumy. Złośliwi twierdzą<br />
nawet, że każdy dysk jest za mały a każdy procesor zbyt wolny<br />
(to ponoć tylko kwestia czasu...).<br />
________________________________________________________________<br />
[!!!] Dla dociekliwych - Przykłady programów.<br />
________________________________________________________________<br />
Jeśli zajrzysz już do systemu Help, przwiń cierpliwie tekst<br />
opisu funkcji do końca. W większości funkcji na końcu<br />
umieszczony jest krótki "firmowy" program przykładowy.<br />
Nie musisz go przepisywać!<br />
W menu Edit IDE masz do dyspozycji rozkaz<br />
Edit | Copy Example (Skopiuj przykład)<br />
Przykład zostanie skopiowany do Schowka (Clipboard).<br />
Po wyjściu z systemu pomocy warto rozkazem<br />
File | New<br />
otworzyć nowe okno robocze a następnie rozkazem<br />
Edit | Paste<br />
(Wstaw)<br />
wstawić program przykładowy ze schowka. Możesz go teraz<br />
uruchamiać, modyfikować a nawet wstawić jako fragment do swojego<br />
programu.<br />
Podobnie jak większość edytorów tekstu zintegrowany edytor<br />
środowiska IDE pozwala manipulować fragmentami blokami tekstu i<br />
wykonywać typowe operacje edytorskie zarówno w obrębie<br />
pojedynczego okna, jak i pomiędzy różnymi okienkami. Służą do<br />
tego celu następujące operacje:<br />
* Select/Mark text block - zaznaczenie fragmentu tekstu.<br />
Możesz dokonać tego klawiszami- np.: [Shift]+[-->], bądź<br />
naciskając i przytrzymując lewy klawisz myszki i "przejeżdżając<br />
nad odpowiednim fragmentem tekstu". Wybrany fragment tekstu<br />
zostanie wyróżniony podświetleniem.<br />
* Edit | Cut - wytnij.<br />
Zaznaczony wcześniej fragment tekstu zostanie skopiowany do<br />
Schowka i jednocześnie usunięty z ekranu.<br />
* Edit | Copy - skopiuj.<br />
Zaznaczony wcześniej fragment tekstu zostanie skopiowany do<br />
Schowka i bez usuwania z ekranu.<br />
* Edit | Paste - wstaw.<br />
Zaznaczony wcześniej w Schowku fragment tekstu zostanie<br />
skopiowany na ekran począwszy od miejsca wskazanego w danej<br />
chwili kursorem.<br />
- 61-
LEKCJA 8. Jakich słów kluczowych używa <strong>C++</strong>.<br />
W trakcie tej lekcji dowiesz się:<br />
* Jakie znaczenie mają słowa kluczowe języka <strong>C++</strong>.<br />
* Jakie jeszcze dziwne słowa mogą pojawiać się w programach w<br />
pisanych <strong>C++</strong>.<br />
* Trochę więcej o wczytywaniu i wyprowadzaniu danych.<br />
* Co to jest i do czego służy zmienna.<br />
_______________________________________________________________<br />
Każdy język musi operować tzw. słownikiem - zestawem słów<br />
zrozumiałych w danym języku. Jak wiesz z doświadczenia, komputer<br />
jest pedantem i wymaga dodatkowo (my, ludzie, tego nie<br />
wymagamy), aby znaczenie słów było absolutnie jednoznaczne i<br />
precyzyjne. Aluzje, kalambury i zabawne niedomówienia są na<br />
razie w dialogu z komputerem niedopuszczalne. Pamięci<br />
asocjatywne (oparte na skojarzeniach), sieci neuronowe (neural<br />
networks), tworzone bardzo często właśnie przy pomocy <strong>C++</strong><br />
- systemy expertowe,<br />
- systemy z tolerancją błędów - np. OCR - systemy optycznego<br />
rozpoznawania pisma,<br />
- "rozmyta" arytmetyka i logika (fuzzy math)<br />
- logika większościowa i mniejszościowa<br />
- algorytmy genetyczne (genetic algorithms)<br />
i inne pomysły matematyków oraz informatyków rozpoczęły już<br />
proces "humanizowania" komputerowego myślenia. Powstała nawet<br />
specjalna "mutacja" neural C i neural <strong>C++</strong>, ale to temat na<br />
oddzielną książkę. Na razie traktujemy nasz komputer jako<br />
automat cyfrowy pozbawiony całkowicie wyobraźni i poczucia<br />
humoru, a język <strong>C++</strong>, jako środek porozumiewania się z tym<br />
"ponurakiem".<br />
Podobnie do słów języka naturalnego (rzeczowników, czasowników)<br />
i słowa języka programowania można podzielić na kilka grup<br />
różniących się przeznaczeniem. Takie niby - słowa czasem nazywa<br />
się również tokenami lub JEDNOSTKAMI LEKSYKALNYMI (leksykon -<br />
inaczej słownik) a sposoby tworzenia wyrażeń (expressions)<br />
nazywane są syntaktyką języka (stąd bierze się typowy komunikat<br />
o błędach "Syntax Error" - błąd syntaktyczny, czyli niewłaściwa<br />
składnia). Słownik języka <strong>C++</strong> składa się z:<br />
* Słów kluczowych<br />
* Identyfikatorów<br />
* Stałych liczbowych i znakowych<br />
* Stałych tekstowych (łańcuchów znaków - napisów)<br />
* Operatorów (umownych znaków operacji)<br />
* Znaków interpunkcyjnych<br />
* Odstępów<br />
UWAGA: Zarówno pojedyncza spacja czy ciąg spacji, tabulator<br />
poziomy, znak nowej linii, jak i komentarz dowolnej długości (!)<br />
są traktowane przez kompilator jak pojedyncza spacja.<br />
Od zarania dziejów informatyki twórcy uniwersalnych języków<br />
programowania starali się upodobnić słowa tych języków do<br />
zrozumiałych dla człowieka słów języka naturalnego - niestety -<br />
- 62-
angielskiego (swoją drogą, może to i lepiej, że <strong>C++</strong> nie<br />
wymyślili Japończycy...). Najważniejszą częścią słownika są tzw.<br />
SŁOWA KLUCZOWE (keywords).<br />
SŁOWA KLUCZOWE w <strong>C++</strong>.<br />
Oto pełna lista słów kluczowych Turbo <strong>C++</strong> v 1.0 z krótkim<br />
wyjaśnieniem ich znaczenia. Zaczynam od listy podstawowej wersji<br />
kompilatora, ponieważ rozważania o niuansach dotyczących kilku<br />
specyficznych słów kluczowych (np. friend, template) pozostawiam<br />
sobie na póżniej. Krótkie wyjaśnienie - jak to krótkie<br />
wyjaśnienie - pewnie nie wyjaśni wszystkiego od razu, ale na<br />
pewno pomoże zrozumieć znaczenie większości słów kluczowych.<br />
[S] Keywords - słowa kluczowe.<br />
asm<br />
Pozwala wstawić kod w ASEMBLERZE bezpośrednio do programu<br />
napisanego w C lub <strong>C++</strong>.<br />
auto - zmienna lokalna. Przyjmowane domyślnie.<br />
break - przerwij.<br />
case - w przypadku.<br />
cdecl - spec. konwencja nazewnictwa/przekazania parametrów<br />
zgodna ze standardem jęz. C.<br />
char - znak, typ zmiennej - pojedynczy bajt.<br />
class - klasa.<br />
const - stała, konstanta.<br />
continue - kontynuuj.<br />
default - przyjmij domyślnie.<br />
delete - skasuj obiekt.<br />
do - wykonaj.<br />
double - podwójna (długość/precyzja).<br />
else - w przeciwnym wypadku.<br />
enum - wylicz kolejno.<br />
_export - dotyczy tylko OS/2, ignorowany.<br />
extern - zewnętrzna.<br />
- 63-
far - dalekie. Wskaźnik - podwójne słowo (w zakresie do 1 MB).<br />
float - zmiennoprzecinkowy, rzeczywisty.<br />
for - dla (wskazanie zmiennej roboczej w pętli).<br />
friend - zaprzyjaźniona funkcja z dostępem do prywatnych i<br />
€€€€€€€€€€chronionych członków danej klasy.<br />
goto - skocz do (skok bezwarunkowy).<br />
huge - daleki, podobnie do far.<br />
if - jeżeli (pod warunkiem, że...).<br />
inline - funkcja z rozwiniętym wstawionym kodem<br />
int - typ zmiennej, liczba całkowita, dwa bajty<br />
interrupt - przerwanie.<br />
_loadds - podobne do huge, ustawia rejestr DS (Data Segment).<br />
long - długi.<br />
near - bliski, wskaźnik o dł. 1 słowa. Obszar max. 64 K.<br />
new - nowy, utwórz nowy obiekt.<br />
operator - operator, określa nowy sposób działania operatora.<br />
pascal - deklar. funkcji zgodnej ze standardem przekazywania<br />
parametrów przyjętym w Pascalu.<br />
private - prywatna, wewnętrzna, niedostępna z zewnątrz.<br />
protected - chroniona, część danych i funkcji, do których<br />
dostęp. jest ograniczony.<br />
public - publiczna, dostępna z zewnątrz.<br />
register - zmienną przechwaj nie w pamięci a w rejestrze CPU.<br />
return - powrót, zwrot wartości.<br />
_saveregs - save registers, zachowaj zawartość rejestrów a<br />
następnie odtwórz rejestry przed powrotem.<br />
_seg - segment.<br />
short - krótka (mała ilość cyfr).<br />
signed - ze znakiem (+/-).<br />
unsigned - bez znaku (+/-).<br />
- 64-
sizeof - podaj wielkość.<br />
static - statyczna.<br />
struct - struktura.<br />
switch - przełącz.<br />
this - ten, wstazanie bieżącego, własnego obiektu (tylko <strong>C++</strong>).<br />
typedef - definicja typu.<br />
union - unia, zmienna wariantowa.<br />
virtual - wirtualna, pozorna.<br />
void - nieokreślona.<br />
volatile - ulotna.<br />
while - dopóki.<br />
Panuje mnienanie, że język <strong>C++</strong> posługuje się stosunkowo skromnym<br />
zestawem słów kluczowych. To prawda, ale nie cała prawda o<br />
języku <strong>C++</strong>. Zauważyłeś zapewne, że nie ma tu:<br />
define, include, printf<br />
i innych znanych Ci już słów. To po prostu jeszcze nie cały<br />
słownik języka. Zdając sobie sprawę z nieprecyzyjności tego<br />
porównania możesz przyjąć, że to coś na kształt listy<br />
czasowników. A są przecież jeszcze i inne słowa - o innej roli i<br />
przeznaczeniu.<br />
[???]€A GDZIE SIĘ PODZIAŁY REJESTRY ???<br />
Nazwy rejestrów mikroprocesora Intel 80X86:<br />
_AX€€€€€€€_AL€€€€€€€_AH€€€€€€€_SI€€€€€€€_CS<br />
_BX€€€€€€€_BL€€€€€€€_BH€€€€€€€_SP€€€€€€€_DS<br />
_CX€€€€€€€_CL€€€€€€€_CH€€€€€€€_BP€€€€€€€_ES<br />
_DX€€€€€€€_DL€€€€€€€_DH€€€€€€€_DI€€€€€€€_SS<br />
_FLAGS<br />
Takie oznaczenia wynikają z architektury konkretnej rodziny<br />
mikroprocesorów, nie mogą stanowić uniwersalnego standardu<br />
języka <strong>C++</strong>. Efekt dostosowania <strong>C++</strong> do IBM PC to np. odnoszące<br />
się do modeli pamięci słowa kluczowe near, far i huge.<br />
Wymóg zgodności ze standardem ANSI C spowodował, że w <strong>C++</strong> nazwy<br />
rejestrów pozostają nazwami o zastrzeżonym znaczeniu, ale<br />
nazywają się PSEUDOZMIENNYMI REJESTROWYMI (ang.: Register<br />
Pseudovariables).<br />
- 65-
Próba użycia słowa o zastrzeżonym znaczeniu w jakiejkolwiek<br />
innej roli (np. jako nazwa Twojej zmiennej) może spowodować<br />
wadliwe działanie programu lub uniemożliwić kompilację. Unikaj<br />
przypadkowego zastosowania słów o zastrzeżonym znaczeniu!<br />
[???] A SKĄD MAM WIEDZIEC ?<br />
Listę nazw, które mają już nadane ściśle określone znaczenie w<br />
<strong>C++</strong> znajdziesz w Help. Dostęp do spisu uzyskasz przez:<br />
* Rozwinięcie menu Help [Alt]-[H];<br />
* Wybranie z menu Help rozkazu Index (spis).<br />
Wrócić do edytora IDE <strong>C++</strong> możesz przez [Esc].<br />
SŁOWA TYPOWE DLA PROGRAMÓW OBIEKTOWYCH.<br />
W porównaniu z klasycznym językiem C (wobec którego <strong>C++</strong> jest<br />
nadzbiorem - ang. superset), w nowoczesnych programach<br />
obiektowych i zdarzeniowych pisanych w <strong>C++</strong> mogą pojawiać się i<br />
inne słowa. Przyjrzyjmy się na trochę inną technikę<br />
programowania - bardziej charakterystyczną dla <strong>C++</strong>.<br />
Procesy wprowadzania i wyprowadzania danych do- i z- komputera<br />
nazywają się Input i Output - w skrócie I/O (lub bardziej<br />
swojsko We/Wy). Obsługa We/Wy komputera to sała obszerna wiedza,<br />
na początek będzie nam jednak potrzebne tylko kilka najbardziej<br />
istotnych informacji.<br />
PROBLEM ˙WEJŚCIA/WYJŚCIA W PROGRAMACH - trochę bardziej ogólnie.<br />
Operacje wejścia i wyjścia są zwykle kontrolowane przez<br />
pracujący właśnie program. Jeśli uruchomiłeś program, który nie<br />
korzysta z klawiatury i nie oczekuje na wprowadzenie przez<br />
użytkownika żadnych informacji - możesz naciskać dowolne<br />
klawisze - program i tak ma to w nosie. Podobnie, jeśli w<br />
programie nie przewidziano wykorzystania drukarki, choćbyś<br />
"wyłaził ze skóry", żadne informacje nie zostaną przesłane do<br />
drukarki, dla programu i dla użytkownika drukarka pozostanie<br />
niedostępna. Aby programy mogły zapanować nad Wejściem i<br />
Wyjściem informacji, wszystkie języki programowania muszą<br />
zawierać specjalne rozkazy przeznaczone do obsługi<br />
Wejścia/Wyjścia (ang. Input/Output commands, lub I/O<br />
instructions). Bez umiejętności obsługi We/Wy, czyli bez<br />
możliwości porozumiewania się ze światem zewnętrznym psu na budę<br />
zdałby się każdy język programowania. Każdy program musi w<br />
większym, bądź mniejszym stopniu pobierać informacje ze świata<br />
zewnętrznego do komputera i wysyłać informacje z komputera na<br />
zewnątrz.<br />
Podobnie, jak wszystkie uniwersalne języki programowania - język<br />
<strong>C++</strong> zawiera pewną ilość rozkazów przeznaczonych do zarządzania<br />
obsługą wejścia i wyjścia. Dla przykładu, możemy w języku <strong>C++</strong><br />
zastosować OBIEKT cout obsługujący strumień danych wyjściowych.<br />
Obiekt cout (skonstruowany przez producenta i zdefiniowany w<br />
- 66-
pliku nagłówkowym IOSTREAM.H) pozwala programiście<br />
przesłać dane tekstowe i/lub numeryczne do strumienia wyjściwego<br />
i umieścić tekst na ekranie monitora.<br />
Wczytaj plik źródłowy z programem COUT1.CPP lub wpisz<br />
samodzielnie następujący program przykładowy. Program drukuje<br />
tekst na ekranie monitora.<br />
[P009.CPP]<br />
#include <br />
#include <br />
Bajt Nr X 0100 0001 - kod ASCII litery A<br />
Bajt Nr X+1 0000 0000 - kod ASCII 0 - znak końca<br />
Wiesz już, że clrscr(); stanowi wywołanie gotowej funkcji (tzw.<br />
funkcji bibliotecznej). Informacja dotycząca tej funkcji (tzw.<br />
prototyp funkcji) znajduje się w pliku CONIO.H, dlatego<br />
dołączyliśmy ten plik nagłówkowy na początku programu dyrektywą<br />
#include. A cóż to za dziwoląg ten "cout" ?<br />
Po cout nie ma pary nawiasów okrągłych (gdyby to była<br />
funkcja - powinno być cout()) - nie jest to zatem wywołanie<br />
funkcji. ˙Strumień danych wyjściowych cout - JEST OBIEKTEM (ang.<br />
I/O stream object - obiekt: strumień Wejścia/Wyjścia). Ale nie<br />
przestrasz się. Popularne wyobrażenie, że programowanie<br />
obiektowe jest czymś bardzo skomplikowanym nie ma z prawdą<br />
więcej wspólnego, niż powszechny dość pogląd, że baba z pustym<br />
wiadrem jest gorsza od czarnego kota. W gruncie rzeczy jest<br />
to proste. Strumień to nic innego jak zwyczajny przepływ<br />
informacji od jednego urządzenia do innego. W tym przypadku<br />
strumień (przepływ) danych oznacza przesłanie informacji<br />
(tekstu) z pamięci komputera na ekran monitora. Trójkątne<br />
nawiasy (>) wskazują kierunek przepływu informacji.<br />
Przesyłanie następuje w naszym przypadku z pamięci do strumienia<br />
Pojawiło się tu ważne słowo - OBIEKT. Obiekt podobnie jak<br />
program komputerowy jest to grupa danych i funkcji działających<br />
wspólnie i przeznaczonych razem do wykonania jakichś zadań. Dla<br />
przykładu obiekt cout służy do obsługi przesyłania danych na<br />
ekran monitora. Słowo "obiekt" jest często używane w opisach<br />
nowoczesnych technik programowania - tzw. PROGRAMOWANIA<br />
OBIEKTOWEGO. <strong>Programowanie</strong> obiektowe, ta "wyższa szkoła jazdy"<br />
dla programistów z lat 80-tych jest już właściwie w naszych<br />
czasach normą. Zresztą widzisz sam - napisałeś program obiektowy<br />
i co - i nic strasznego się nie stało. Na początek musisz<br />
wiedzieć tylko tyle, że aby posługiwać się obiektami -<br />
strumieniami wejście i wyjścia - należy dołączyć w <strong>C++</strong> plik<br />
nagłówkowy IOSTREAM.H. Dlatego dyrektywa #include <br />
znajduje się na początku przykładowego programu.<br />
KILKA ARGUMENTÓW FUNKCJI w praktyce.<br />
Jak starałem się wykazać w przykładzie z sinusem, funkcja może<br />
otrzymac jako argument stałą - np. określoną liczbę, bądź<br />
zmienną (niewiadomą). Niektóre funkcje mogą otrzymywać w<br />
momencie ich wywołania (użycia w programie) więcej niż jeden<br />
argument. Rozważmy to dokładniej na przykładzie funkcji<br />
fprintf() zbliżonej w działaniu do printf(), lecz bardziej<br />
uniwersalnej. Funkcja fprintf() pozwala wyprowadzać dane nie<br />
tylko na monitor, ale także na drukarkę. Skoro urządzenia<br />
wyjścia mogą być różne, trzeba funkcji przekazać jako jeden z<br />
jej argumentów informację o tym - na które urządzenie życzymy<br />
sobie w danej chwili wyprowadzać dane.<br />
Słowo stdout jest pierwszą informację (tzw. parametrem, bądź<br />
argumentem funkcji) przekazanym do funkcji fprintf(). Słowo<br />
- 68-
stdout jest skrótem od Standard Output - standardowe wyjście.<br />
Oznacza to w skrócie typowe urządzenie wyjściowe podłączone do<br />
komputera ˙i umożliwiające wyprowadzenie informacji z komputera.<br />
W komputerach osobistych zgodnych ze standardem IBM PC tym<br />
typowym urządzeniem wyjściowym jest prawie zawsze ekran<br />
monitora.<br />
Tekst, który ma zostać wydrukowany na ekranie monitora jest<br />
drugą informacją przekazywaną do funkcji fprintf() - inaczej -<br />
stanowi drugi parametr funkcji. Tekst - łańcuch znaków - musi<br />
zostać ujęty w znaki cudzysłowu.<br />
A jeśli zechcesz wyprowadzić tekst na drukarkę?<br />
W <strong>C++</strong> zapisuje się to bardzo łatwo. Wystarczy słowo stdout<br />
(oznaczające monitor) zamienić na słowo stdprn. Słowo stdprn to<br />
skrót od Standard Printer Device - standardowa drukarka. Oto<br />
przykład praktycznego użycia funkcji fprintf(). Program przesyła<br />
tekst na drukarkę. Przed uruchomieniem programu pamiętaj o<br />
włączeniu drukarki.<br />
[P010.CPP]<br />
#include <br />
#include <br />
int main(void)<br />
{<br />
clrscr();<br />
fprintf(stdout, "Drukuje...\n");<br />
fprintf(stdprn, "Pierwsza proba drukowania\n");<br />
fprintf(stdprn, "Autor: ....................");<br />
fprintf(stdout, "Koniec drukowania.");<br />
fprintf(stdout, "Skonczylem, nacisnij cosik...");<br />
getch();<br />
return 0;<br />
}<br />
Gdyby w programie nie było wiersza:<br />
fprintf(stdout, "Drukuje...\n");<br />
- użytkownik przez pewien czas nie mógłby się zorientować,<br />
czym ˙właściwie zajmuje się komputer. Wszystko stałoby się jasne<br />
dopiero wtedy, gdy drukarka rozpoczęłaby drukowanie tekstów.<br />
Jest uznawane za dobre maniery praktyczne stosowanie dwóch<br />
prostych zasad:<br />
BZU - Bez Zbędnych Udziwnień<br />
DONU - Dbaj O Nerwy Użytkownika<br />
Jeśli efekty działania programu nie są natychmiast zauważalne,<br />
należy poinformować użytkownika CO PROGRAM ROBI. Jeśli<br />
użytkownik odnosi wrażenie, że komputer nic nie robi - ma zaraz<br />
- 69-
wątpliwości. Często próbuje wtedy wykonać reset komputera i<br />
wypowiada mnóstwo słów, których nie wypada mi tu zacytować.<br />
Nietrudno zgadnąć, że <strong>C++</strong> powinien posiadać także środki obsługi<br />
wejścia. W <strong>C++</strong> jest specjalny obiekt (ang. input stream object)<br />
o nazwie cin służący do pobierania od użytkownika tekstów i<br />
liczb. Zanim zajmiemy się dokładniej obiektem cin i obsługą<br />
strumienia danych wejściowych - powinieneś zapoznać się ze<br />
ZMIENNYMI (ang. variables).<br />
ZMIENNE.<br />
Gdy wprowadzisz jakieś informacje do komputera - komputer<br />
umieszcza je i przechowuje w swojej pamięci (ang. memory -<br />
pamięć). Pamięć komputera może być jego pamięcią stałą. Taka<br />
pamięć "tylko do odczytu" nazywa się ROM (read only memory - to<br />
właśnie "tylko do odczytu"). Pamięć o swobodnym dostępie, do<br />
której i komputer i Ty możecie zapisywać wszystko, co Wam się<br />
spodoba - nazywa się RAM (od Random Access Memory - pamięć o<br />
swobodnym dostępie). Pamięci ROM i RAM podzielone są na małe<br />
"komóreczki" nazywane Bajtami, Każdy bajt w pamięci ma swój<br />
numer. Ten numer nazywany jest adresem w pamięci. Ponieważ nie<br />
wszystko da się pomieścić w jednym bajcie (to tylko 8 bitów -<br />
miejsca wystarczy na zapamiętanie tylko jednej litery), bajty<br />
(zwykle kolejne) mogą być łączone w większe komórki - tzw. pola<br />
pamięci (ang. memory fields). Najczęściej łączy się bajty:<br />
2 Bajty = 16 bitów = Słowo (WORD)<br />
4 Bajty = 32 bity = Podwójne słowo (DOUBLE WORD - DWORD)<br />
W uproszczeniu możesz wyobrazić sobie pamięć komputera jako<br />
miliony pojedynczych komórek, a w każdej z komórek jakaś jedna<br />
wartość (ang. value) zakodowana w postaci ZER i JEDYNEK. Każda<br />
taka "szara" komórka ma numer-adres. Numeracja komórek<br />
rozpoczyna się nie od 1 lecz od zera (pierwsza ma numer 0).<br />
Ilość tych komórek w Twoim komputerze zależy od tego ile pamięci<br />
zainstalujesz (np. 4MB RAM to 4x1024x124x8 bitów - chcesz -<br />
policz sam ile to bitów). Przeliczając zwróć uwagę, że kilobajt<br />
(KB to nie 1000 - lecz 1024 bajty a megabajt - 1024 kB).<br />
Zastanówmy się, skąd program może wiedzieć gdzie, w której<br />
komórce zostały umieszczone dane i jak się do nich dobrać, gdy<br />
staną się potrzebne. Właśnie do takich celów potrzebne są<br />
programowi ZMIENNE (ang. variables).<br />
Dawno, dawno temu rozwiązywałeś zapewne zadania typu:<br />
3 + [ ] = 5<br />
Otóż to [ ] było pierwszym sposobem przedstawienia Ci zmiennej.<br />
Jak widać - zmienna to miejsce na wpisanie jakiejś (czasem<br />
nieznanej w danej chwili wartości). Gdy przeszedłeś do następnej<br />
- 70-
klasy, zadania skomplikowały się:<br />
3 + [ ] = 5<br />
147.968 + [ ] = 123876.99875<br />
Na różne zmienne może być potrzeba różna ilość miejsca i na<br />
kartce i w pamięci komputera. Gdy "zestarzałeś się" jeszcze<br />
trochę - te same zadania zaczęto Ci zapisywać tak:<br />
3 + x = 5<br />
147.968 + y = 123876.99875<br />
Jak widać, zmienne mogą posiadać także swoje nazwy -<br />
identyfikatory (z których już niestety nie wynika jasno, ile<br />
miejsca potrzeba do zapisania bieżącej wartości zmiennej).<br />
[???] Jak <strong>C++</strong> wskazuje adres w pamięci?<br />
Podobnie, jak w bajeczce o zabawie w chowanego kotka i myszki<br />
(myszka mówiła: "Gdybyś mnie długo nie mógł znaleść - będę<br />
czekać na czwartej półce od góry..."), niektórzy producenci gier<br />
komputerowych życzą sobie czasem przy uruchamianiu gry podania<br />
hasła umieszczonego:<br />
"W instrukcji na str. 124 w czwartym wierszu do góry"<br />
No cóż. Zamiast nazywać zmienne - niewiadome x, y, czy z, bądź<br />
rezerwować dla nich puste miejsce [ ], możemy jeszcze<br />
wskazać miejsce, w którym należy ich szukać. Takie wskazanie to<br />
trzeci sposób odwoływania się do danych. W <strong>C++</strong> może się to<br />
nazywać referencją do zmiennej lub wskazaniem adresu zmiennej w<br />
pamięci przy pomocy wskaźnika. Wskaźnik w <strong>C++</strong> nazywa się<br />
"pointerem". Pointerem można wskazać także funkcje - podając ich<br />
adres startowy (początek kodu funkcji w pamięci RAM).<br />
Zmienne możesz sobie wyobrazić jako przegródki w pamięci<br />
komputera zaopatrzone w nazwę - etykietkę. Ponieważ nazwy dla<br />
tych przegródek nadaje programista w programie - czyli Ty sam,<br />
możesz wybrać sobie prawie każdą, dowolną nazwę. Zwykle nazwy<br />
nadaje się w taki sposób, by program stał się bardziej czytelny<br />
i łatwiejszy do zrozumienia. Dla przykładu, by nie przepadły z<br />
pamięci komputera wyniki gier komputerowych często stosuje się<br />
zmienną o nazwie WYNIK (ang. Score). Za każdym razem, gdy<br />
zmienia się wynik gracza (ang. player's score) w pamięci<br />
komputera (w to samo miejsce) zostaje zapisana nowa liczba. W<br />
taki sposób pewien niewielki (a zawsze ten sam) fragment pamięci<br />
komputera przechowuje dane potrzebne do pracy programu.<br />
PRZYPISYWANIE ZMIENNYM KONKRETNEJ WARTOŚCI.<br />
Aby komputer mogł pobrać informacje od użytkownika, możesz<br />
zastosować w programie np. obiekt - strumień wejściowy - cin<br />
(ang. input stream object). Obiekt cin i zmienne chodzą zwykle<br />
- 71-
parami. Przy obiekcie cin musisz zawsze podać operator<br />
pobierania ze strumienia wejściowego >> i nazwę zmiennej. Zapis<br />
cin >> nazwa_zmiennej;<br />
oznacza ˙w <strong>C++</strong> : pobierz dane ze strumienia wejściowego i umieść<br />
w zmiennej o nazwie "nazwa_zmiennej".Te informacje, które<br />
zostaną ˙wczytane, <strong>C++</strong> przechowuje w przgródce oznaczonej nazwą,<br />
którą nadajesz zmiennej. Oto program przykładowy ilustrujący<br />
zapamiętywanie danych wprowadzonych przez użytkownika z<br />
klawiatury, wczytanych do programu przy pomocy obiektu cin i<br />
zapamiętanych w zadeklarowanej wcześniej zmiennej x:<br />
[P011.CPP]<br />
#include <br />
#include <br />
void main(void)<br />
{<br />
int x;<br />
cout > x;<br />
cout
itp...(szczegóły - patrz niżej).<br />
Zwykle nie musisz się przejmować tym, w którym miejscu<br />
kompilator rozmieścił Twoje dane. Wszystkie czynności <strong>C++</strong> wykona<br />
automatycznie. Aby jednak wszystko przebiegało poprawnie - zanim<br />
zastosujesz jakąkolwiek zmienną w swoim programie - musisz<br />
ZADEKLAROWAĆ ZMIENNĄ. Deklaracja zmiennej to informacja dla<br />
kompilatora, ile i jakich zmiennych będziemy stosować w<br />
programie. Deklaracja zawiera nie tylko nazwę zmiennej, ale<br />
również typ wartości, jakie ta zmienna może przybierać.<br />
Przykładem deklaracji jest wiersz:<br />
int x;<br />
Słowo kluczowe int określa typ danych. Tu oznacza to, że zmienna<br />
x może przechowywać jako wartości liczby całkowite (ang. INTeger<br />
- całkowity) o wielkości zawartej w przedziale - 32768...+32767.<br />
Po określeniu typu danych następuje w deklaracji nazwa zmiennej<br />
i średnik.<br />
[S] Variable Declaration - Dekaracja Zmiennej.<br />
Deklaracja zmiennej w <strong>C++</strong> to określenie typu wartości zmiennej i<br />
podanie nazwy zmiennej.<br />
Zwróć uwagę w przykładowym programie, że kierując kolejno dane<br />
do strumienia wyjściwego cout możemy je poustawiać w tzw.<br />
łańcuch (ang. chain). Przesyłanie danych do obiektu cout<br />
operatorem
RÓŻNYCH TYPÓW - mogą być liczbami, mogą także być tekstami.<br />
Uruchom program jeszcze raz i zamiast liczby naciśnij w<br />
odpowiedzi na pytanie klawisz z literą. Program wydrukuje jakieś<br />
bzdury. Dzieje się tak dlatago, że program oczekuje podania<br />
liczby i zakłada, że wprowadzone przez użytkownika dane są<br />
liczbą.<br />
[???] A jeśli użytkownik nie czyta uważnie???<br />
<strong>C++</strong> zakłada, że użytkownik wie co robi gdy podaje wartość<br />
zmiennej. Jeśli wprowadzone zostaną dane niewłaściwego typu -<br />
<strong>C++</strong> nie przerywa działania programu i nie ostrzega przed<br />
niebezpieczeństwem błędu. Sam dokonuje tzw. konwersji typów -<br />
tzn. przekształca dane na wartość typu zgodnego z zadeklarowanym<br />
w programie typem zmiennej. To programista musi dopilnować, by<br />
pobrane od użytkownika dane okazały się wartością odpowiedniego,<br />
oczekiwanego przez program typu, lub przewidzieć w programie<br />
sposób obsługi sytuacji błędnych.<br />
Można utworzyć zmienną przeznaczoną do przechowywania w pamięci<br />
tekstu - napisu. Aby to zrobić musimy zadeklarować coś<br />
jakościowo nowego tzw. TABLICĘ ZNAKOWĄ (ang. character array).<br />
Jest to nazwa, przy pomocy której komputer lokalizuje w pamięci<br />
zbiór znaków. Aby zadeklarować zmienną (tablicę) znakową w <strong>C++</strong><br />
musimy zacząć od słowa kluczowego char (ang. CHARacter - znak).<br />
Następnie podajemy nazwę zmiennej a po nazwie w nawiasach<br />
kwadratowych ilość znaków, z których może składać się zmienny<br />
tekst, który zamierzamy przechowywać w pamięci pod tą nazwą.<br />
W programie poniżej zmienna x nie jest już miejscem w pamięci<br />
służącym do przechowywania pojedynczej liczby. Tym razem nazwa<br />
(identyfikator zmiennej) x oznacza tablicę znakową, w której<br />
można przechowywać tekst o długości do 20 znaków. W <strong>C++</strong> ostatnim<br />
znakiem w łańcuchu znakowym (tekście) bądź w tablicy znakowej<br />
zwykle jest tzw. NULL CHARACTER - niewidoczny znak o kodzie<br />
ASCII 0 (zero). W <strong>C++</strong> znak ten podaje się przy pomocy szyfru<br />
'\0'. Przy pomocy tego znaku <strong>C++</strong> odnajduje koniec tekstu,<br />
łańcucha znaków, bądź koniec tablicy znakowej. Tak więc w<br />
tablicy x[20] w rzeczywistości można przechować najwyżej 19<br />
dowolnych znaków plus na końcu obowiązkowy NULL (wartownik).<br />
[P012.CPP]<br />
#include <br />
#include <br />
void main(void)<br />
{<br />
char x[20]; //
cin >> x;<br />
cout
LEKCJA 9. O SPOSOBACH ODWOŁYWANIA SIĘ DO DANYCH.<br />
________________________________________________________________<br />
W trakcie tej lekcji poznasz:<br />
* sposoby wyprowadzania napisów w różnych kolorach<br />
* sposoby zapamiętywania tekstów<br />
* sposoby odwoływania się do danyc i zmiennych przy pomocy ich<br />
nazw - identyfikatorów.<br />
________________________________________________________________<br />
Możemy teraz poświęcić chwilę na zagadnienie kolorów, które<br />
pojawiają się na monitorze. Po uruchomieniu program przykładowy<br />
poniżej wygeneruje krótki dźwięk i zapyta o imię. Po wpisaniu<br />
imienia program zapiszczy jeszcze raz i zapyta o nazwisko. Po<br />
wpisaniu nazwiska program zmieni kolor na ekranie monitora i<br />
wypisze komunikat kolorowymi literami. Różne kolory zobaczysz<br />
oczywiście tylko wtedy, gdy masz kolorowy monitor. Dla<br />
popularnego zestawu VGA mono będą to różne odcienie szarości.<br />
Tekst powinien zmieniać kolor i "migać" (ang. - blinking text).<br />
[P012.CPP]<br />
#include <br />
#include <br />
main()<br />
{<br />
char imie[20];<br />
char nazwisko[20];<br />
clrscr();<br />
cout > imie;<br />
cout > nazwisko;<br />
cout
Operator >> pobiera ze strumienia danych wejściowych cin wpisane<br />
przez Ciebie imię i zapisuje ten tekst do tablicy znakowej<br />
imie[20]. Po wypisaniu na ekranie następnego pytania następuje<br />
pobranie drugiego łańcucha znaków (ang. string) wpisanego przez<br />
Ciebie jako odpowiedź na pytanie o nazwisko i umieszczenie tego<br />
łańcucha w tablicy znakowej nazwisko[]. Wywołana następnie<br />
funkcja textcolor() powoduje zmianę roboczego koloru<br />
wyprowadzanego tekstu. Tekst nie tylko zmieni kolor, lecz także<br />
będzie "migać" (blink). Funkcja cprintf() wyprowadza na ekran<br />
końcowy napis. Funkcja cprintf() to Color PRINTing Function -<br />
funkcja drukowania w kolorze.<br />
Funkcja textcolor() pozwala na zmianę koloru tekstu<br />
wyprowadzanego na monitor. Można przy pomocy tej funkcji także<br />
"zmusić" tekst do migotania. Aby funkcja zadziałała - musimy<br />
przekazać jej ARGUMENT. Argument funkcji to numer koloru. Zwróć<br />
jednak uwagę, że zamiast prostego, zrozumiałego zapisu:<br />
textcolor(4); /* 4 oznacza kolor czerwony */<br />
mamy w programie podany argument w postaci wyrażenia (sumy dwu<br />
liczb):<br />
textcolor(4+128);<br />
// to samo, co: textcolor(132);<br />
Wbrew pierwszemu mylnemu wrażeniu te dwie liczby stanowią jeden<br />
argument funkcji. <strong>C++</strong> najpierw dokona dodawania 4+128 a dopiero<br />
uzyskany wynik 132 przekaże funkcji textcolor jako jej argument<br />
(parametr). Liczba 4 to kod koloru czerwonego, a zwiększenie<br />
kodu koloru o 128 powoduje, że tekst będzie migał.<br />
Numery (kody) kolorów, które możesz przekazać jako argumenty<br />
funkcji textcolor() podano w tabeli poniżej. Jeśli tekst ma<br />
migać - należy dodać 128 do numeru odpowiedniego koloru.<br />
Kod koloru przekazywany do funkcji textcolor().<br />
________________________________________________________________<br />
Kod Kolor (ang) Kolor (pol) Stała<br />
n<br />
(przykład)<br />
________________________________________________________________<br />
0 Black Czarny BLACK<br />
1 Blue Niebieski BLUE<br />
2 Green Zielony GREEN<br />
3 Cyan Morski CYAN<br />
4 Red Czerwony<br />
5 Magenta Fioletowy<br />
6 Brown Brązowy<br />
7 White Biały<br />
8 Gray Szary<br />
9 Light blue Jasno niebieski<br />
- 77-
10 Light green Jasno zielony<br />
11 Light cyan Morski - jasny<br />
12 Light red Jasno czerwony<br />
13 Light magenta Jasno fio;etowy (fiol-różowy)<br />
14 Yellow Żółty<br />
15 Bright white Biały rozjaśniony<br />
128 + n Blinking Migający BLINK<br />
________________________________________________________________<br />
[!!!]UWAGA:<br />
________________________________________________________________<br />
* W pliku CONIO.H są predefiniowane stałe (skrajna prawa kolumna<br />
- przykłady), które możesz stosować jako argumenty funkcji.<br />
Kolor tła możesz ustawić np. przy pomocy funkcji<br />
textbackground() - np. textbacground(RED);<br />
* Manipulując kolorem tekstu musisz pamiętać, że jeśli kolor<br />
napisu:<br />
- foreground color, text color<br />
i kolor tła:<br />
- background color<br />
okażą się identyczne - tekst zrobi się NIEWIDOCZNY. Jeśli każesz<br />
komputerowi pisać czerwonymi literami na czerwonym tle -<br />
komputer wykona rozkaz. Jednakże większość ludzi ma kłopoty z<br />
odczytywaniem czarnego tekstu na czarnym tle. Jest to jednak<br />
metoda stosowana czasem w praktyce programowania do kasowania<br />
tekstów i elementów graficznych na ekranie.<br />
________________________________________________________________<br />
Powołując się na nasze wcześniejsze porównanie (NIE TRAKTUJ GO<br />
ZBYT DOSŁOWNIE!),zajmiemy się teraz czymś, co trochę przypomina<br />
rzeczowniki w normalnym języku.<br />
O IDENTYFIKATORACH - DOKŁADNIEJ.<br />
Identyfikatorami (nazwami) mogą być słowa, a dokładniej ciągi<br />
liter, cyfr i znaków podkreślenia rozpoczynające się od litery<br />
lub znaku podkreślenia (_). Za wyjątkiem słów kluczowych, (które<br />
to słowa kluczowe - MUSZĄ ZAWSZE BYĆ PISANE MAŁYMI LITERAMI)<br />
można stosować i małe i duże litery. Litery duże i małe są<br />
rozróżniane. Przykład:<br />
[P013.CPP]<br />
#include <br />
#include <br />
float PI = 3.14159;
int main(void)<br />
{<br />
clrscr();<br />
printf("Podaj promien ?\n");<br />
scanf("%f", &r);<br />
printf("\nPole wynosi P = %f", PI*r*r );<br />
getch();<br />
return 0;<br />
}<br />
* Użyte w programie słowa kluczowe:<br />
int, float, void, return.<br />
* Identyfikatory<br />
- nazwy funkcji (zastrzeżone):<br />
main, printf, scanf, getch, clrscr.<br />
- nazwy zmiennych (dowolne):<br />
PI, r.<br />
* Dyrektywy preprocesora:<br />
# include<br />
Zwróć uwagę, że w wierszu:<br />
float PI = 3.14159;<br />
nie tylko DEKLARUJEMY, zmienną PI jako zmiennoprzecinkową, ale<br />
także od razu nadajemy liczbie PI jej wartość. Jest to tzw.<br />
ZAINICJOWANIE zmiennej.<br />
[Z]<br />
________________________________________________________________<br />
1. Uruchom program przykładowy. Spróbuj zamienić identyfikator<br />
zmiennej PI na pisane małymi literami pi. Powinien wystąpić<br />
błąd.<br />
________________________________________________________________<br />
Dla porównania ten sam program w wersji obiektowo-strumieniowej:<br />
[P013-1.CPP]<br />
#include <br />
#include <br />
const float PI = 3.14159;
}<br />
LITERAŁY.<br />
Literałem nazywamy reprezentujący daną NAPIS, na podstawie<br />
którego można jednoznacznie zidentyfikować daną, jej typ,<br />
wartość i inne atrybuty. W języku <strong>C++</strong> literałami mogą być:<br />
* łańcuchy znaków - np. "Napis";<br />
* pojedyncze znaki - np. 'X', '?';<br />
* liczby - np. 255, 3.14<br />
[!!!] Uwaga: BARDZO WAŻNE !!!<br />
________________________________________________________________<br />
* Rolę przecinka dziesiętnego spełnia kropka. Zapis Pi=3,14 jest<br />
nieprawidłowy.<br />
* Próba zastosowania przecinka w tej roli SPOWODUJE BŁĘDY !<br />
________________________________________________________________<br />
Liczby całkowite mogą być:<br />
* Dziesiętne (przyjmowane domyślnie - default);<br />
* Ósemkowe - zapisywane z zerem na początku:<br />
017 = 1*8 + 7 = 15 (dziesiętnie);<br />
* Szesnastkowe - zapisywane z 0x na początku:<br />
0x17 = 1*16 + 7 = 23 (dziesiętnie);<br />
0x100 = 16^2 + 0 + 0 = 256 .<br />
Liczby rzeczywiste mogą zawierać część ułamkową lub być zapisane<br />
w postaci wykładniczej (ang. scientific format) z literą "e"<br />
poprzedzającą wykładnik potęgi.<br />
Przykład:<br />
Zapis liczby€€€€€€€€Wartość dziesiętna<br />
.0123€€€€€€€€€€€€€€€0.0123<br />
123e4€€€€€€€€€€€€€€€123 * 10^4 = 1 230 000<br />
1.23e3€€€€€€€€€€€€€€€1.23 * 10^3 = 1230<br />
123e-4€€€€€€€€€€€€€€0.0123<br />
Literały składające się z pojedynczych znaków mają jedną z<br />
trzech postaci:<br />
* 'z' - gdzie z oznacza znak "we własnej osobie";<br />
* '\n' - symboliczne oznaczenie znaku specjalnego - np.<br />
sterującego - tu: znak nowej linii;<br />
* '\13' - nr znaku w kodzie ASCII.<br />
UWAGA:<br />
'\24' - kod Ósemkowy ! (dziesiętnie 20)<br />
'\x24' - kod SZESNASTKOWY ! (dziesiętnie 36)<br />
[S]€€SLASH, BACKSLASH.<br />
- 80-
€€€€€Kreska "/" nazywa się SLASH (czyt. "slasz") - łamane,<br />
ukośnik zwykły. Kreska "\" nazywa się BACKSLASH (czyt.<br />
"bekslasz") - ukośnik odwrotny.<br />
Uzupełnimy teraz listę symboli znaków z poprzedniej lekcji.<br />
Znak €€€€ÓSEMKOWO€€ASCII (10)€€€€€ZNACZENIE<br />
\a€€€€€€€'\7'€€€€€€7€€€€€€€€€€€€€€- sygn. dźwiękowy BEL<br />
\n€€€€€€€'\12'€€€€€10€€€€€€€€€€€€€- nowy wiersz LF<br />
\t€€€€€€€'\11'€€€€€9€€€€€€€€€€€€€€- tabulacja pozioma HT<br />
\v€€€€€€ '\13'€€€€€11€€€€€€€€€€€€€- tabulacja pionowa VT<br />
\b€€€€€€€'\10'€€€€€8€€€€€€€€€€€€€€- cofnięcie kursora o 1 znak<br />
\r€€€€€€€'\15'€€€€€13€€€€€€€€€€€€€- powrót do początku linii CR<br />
\f€€€€€€€'\14'€€€€€12€€€€€€€€€€€€€- nowa strona (form feed) FF<br />
\\€€€€€€€'\134'€€€€92€€€€€€€€€€€€€- poprostu znak backslash "\"<br />
\'€€€€€€€'\47'€€€€€39€€€€€€€€€€€€€- apostrof "'"<br />
\"€€€€€€€'\42'€€€€€34€€€€€€€€€€€€€- cudzysłów (")<br />
\0€€€€€€€'\0'€€€€€€0€€€€€€€€€€€€€€- NULL (znak pusty)<br />
Komputer przechowuje znak w swojej pamięci jako "krótką", bo<br />
zajmującą tylko jeden bajt liczbę całkowitą (kod ASCII znaku).<br />
Na tych liczbach wolno Ci wykonywać operacje arytmetyczne !<br />
(Od czego mamy komputer?) Przekonaj się o tym uruchamiając<br />
następujący program.<br />
[P014.CPP]<br />
# include //prototypy printf() i scanf()<br />
# include //prototypy clrscr() i getch()<br />
int liczba;<br />
//deklaracja zmiennej "liczba"<br />
int main(void)<br />
{<br />
clrscr();<br />
printf("Wydrukuje A jako \nLiteral znakowy:\tKod ASCII:\n");<br />
printf("%c", 'A');<br />
printf("\t\t\t\t%d", 'A');<br />
printf("\nPodaj mi liczbe ? ");<br />
scanf("%d", &liczba);<br />
printf("\n%c\t\t\t\t%d\n", 'A'+liczba, 'A'+liczba);<br />
scanf("%d", &liczba);<br />
printf("\n%c\t\t\t\t%d", 'A'+liczba, 'A'+liczba);<br />
getch();<br />
return 0;<br />
}<br />
Uruchom program kilkakrotnie podając różne liczby całkowite z<br />
zakresu od 1 do 100.<br />
Przyjrzyj się sposobowi formatowania wyjścia:<br />
%c, %d, \t, \n<br />
Jeśli pamiętasz, że kody ASCII kolejnych liter A,B,C... i<br />
- 81-
kolejnych cyfr 1, 2, 3 są kolejnymi liczbami, to zauważ, że<br />
wyrażenia:<br />
'5' + 1 = '6' oraz 'A' + 2 = 'C'<br />
(czytaj: kod ASCII "5" + 1 = kod ASCII "6")<br />
są poprawne.<br />
[!!!]Jak sprawdzić kod ASCII znaku?<br />
________________________________________________________________<br />
Można oczywście nauczyć się tabeli kodów ASCII na pamięć (dla<br />
początkowych i najważniejszych stronic kodowych - przede<br />
wszystkom od 0 do 852). Dla hobbystów - stronica kodowa 1250 i<br />
1252 też czasem się przydaje.<br />
(to oczywiście żart - autor nie zna ani jednego faceta o tak<br />
genialnej pamięci)<br />
Można skorzystać z edytora programu Norton Commander. W trybie<br />
Edit [F4] po wskazaniu kursorem znaku w górnym wierszu po prawej<br />
stronie zostanie wyświetlony jego kod ASCII.<br />
________________________________________________________________<br />
CZY PROGRAM NIE MÓGŁBY CHODZIĆ W KÓŁKO?<br />
Twoja intuicja programisty z pewnością podpowiada Ci, że gdyby<br />
zmusić komputer do pracy w pętli, to nie musiałbyś przykładowych<br />
programów uruchamiać wielokrotnie. Spróbujmy nakazać programowi<br />
przykładowemu chodzić "w kółko". To proste - dodamy do programu:<br />
* na końcu rozkaz skoku bezwarunkowego goto (idź do...),<br />
* a żeby wiedział dokąd ma sobie iść - na początku programu<br />
zaznaczymy miejsce przy pomocy umownego znaku - ETYKIETY.<br />
Zwróć uwagę, że pisząc pliki wsadowe typu *.BAT w języku BPL<br />
(Batch Programming Language - język programowania wsadowego)<br />
stawiasz dwukropek zawsze na początku etykiety:<br />
:ETYKIETA (BPL)<br />
a w języku <strong>C++</strong> zawsze na końcu etykiety:<br />
ETYKIETA: (C/<strong>C++</strong>)<br />
Przystępujemy do opracowania programu.<br />
[P015.CPP]<br />
# include <br />
short int liczba;<br />
int main(void)<br />
{<br />
clrscr();<br />
- 82-
printf("Wydrukuje A jako \nLiteral znakowy:\tKod ASCII:\n");<br />
printf("%c", 'A');<br />
printf("\t\t\t\t%d", 'A');<br />
etykieta:<br />
printf("\npodaj mi liczbe ? ");<br />
scanf("%d", &liczba);<br />
printf("\n%c\t\t\t\t%d\n", 'A'+liczba, 'A'+liczba);<br />
goto etykieta;<br />
return 0;<br />
}<br />
Skompiluj program do wersji *.EXE:<br />
Compile | Make<br />
(rozkazem Make EXE file z menu Compile). Musisz nacisnąć<br />
następujące klawisze:<br />
[Alt]-[C], [M]. (lub [F9])<br />
* Jeśli wystąpiły błędy, popraw i powtórz próbę kompilacji.<br />
* Uruchom program [Alt]-[R], [R] (lub [Ctrl]-[F9]).<br />
* Podaj kilka liczb: np. 1,2,5,7,8 itp.<br />
* Przerwij działanie programu naciskając kombinację klawiszy<br />
[Ctrl]+[Break] lub [Ctrl]+[C].<br />
* Sprawdź, jaki jest katalog wyjściowy kompilatora.<br />
- Rozwiń menu Options [Alt]-[O],<br />
- Otwórz okienko Directories... [D],<br />
- Sprawdź zawartość okienka tekstowego Output Directory.<br />
Teraz wiesz już gdzie szukać swojego programu w wersji *.EXE.<br />
- Uruchom program poza środowiskiem IDE.<br />
- Sprawdź reakcję programu na klawisze:<br />
[Esc], [Ctrl]-[C], [Ctrl]-[Break].<br />
Uruchom powtórnie kompilator <strong>C++</strong> i załaduj program rozkazem:<br />
BC A:\GOTOTEST.CPP<br />
Wykonaj od nowa kompilację programu [F9].<br />
[???] ... is up to date...<br />
________________________________________________________________<br />
Jeśli <strong>C++</strong> nie zechce powtórzyć kompilacji i odpowie Ci:<br />
€€€€€€€€€€€€€€€€€€€€Making<br />
A:\GOTOTEST.CPP<br />
€€€€€€€€€€€€€€€is up to date<br />
(Program w takiej wersji już skompilowałem, więcej nie będę!)<br />
nie przejmuj się. Dokonaj jakiejkolwiek pozornej zmiany w<br />
programie (np. dodaj spację lub pusty wiersz w dowolnym<br />
miejscu). Takich pozornych zmian wystarczy by oszukać <strong>C++</strong>. <strong>C++</strong><br />
nie jest na tyle inteligentny, by rozróżniać zmiany rzeczywiste<br />
w pliku źródłowym od pozornych.<br />
________________________________________________________________<br />
- 83-
Powtórz kompilację programu. Nie musisz uruchamiać programu.<br />
Zwróć uwagę tym razem na pojawiające się w okienku komunikatów<br />
ostrzeżenie:<br />
Warning: A:\GOTOTEST.CPP 14: Unreachable code in function main.<br />
(Uwaga: Kod programu zawiera takie rozkazy, które nigdy nie<br />
zostaną wykonane inaczej - "są nieosiągalne").<br />
O co chodzi? Przyjrzyj się tekstowi programu. Nawet jeśli po<br />
rozkazie skoku bezwarunkowego:<br />
goto etykieta;<br />
dopiszesz jakikolwiek inny rozkaz, to program nigdy tego rozkazu<br />
nie wykona. Właśnie o to chodzi. Program nie może nawet nigdy<br />
wykonać rozkazu "return 0", który dodaliśmy "z przyzwyczajenia".<br />
Pętla programowa powinna być wykonywana w nieskończoność. Taka<br />
pętla nazywa się pętlą nieskończoną (ang. infinite loop).<br />
Mimo to i w środowisku IDE (typowy komunikat: User break) i w<br />
środowisku DOS tę pętlę uda Ci się przerwać.<br />
Kto wobec tego przerwał działanie Twojego programu? Nieskończoną<br />
pętlę programową przerwał DOS. Program zwrócił się do systemu<br />
DOS, a konkretnie do którejś z DOS'owskich funkcji obsługi<br />
WEJŚCIA/WYJŚCIA i to DOS wykrył, że przycisnąłeś klawisze<br />
[Ctrl]-[C] i przerwał obsługę Twojego programu. Następnie DOS<br />
"wyrzucił" twój program z pamięci operacyjnej komputera i<br />
zgłosił gotowość do wykonania dalszych Twoich poleceń - swoim<br />
znakiem zachęty C:\>_ lub A:\>_.<br />
Spróbujmy wykonać taki sam "face lifting" i innych programów<br />
przykładowych, dodając do nich najprostszą pętlę. Zanim jednak<br />
omówimy szczegóły techniczne pętli programowych w <strong>C++</strong> rozważmy<br />
prosty przykład. Wyobraźmy sobie, że chcemy wydrukować na<br />
ekranie kolejne liczby całkowite od 2 do np. 10. Program<br />
powinien zatem liczyć ilość wykonanych pętli, bądź sprawdzać,<br />
czy liczba przeznaczona do drukowania nie stała się zbyt duża.<br />
W <strong>C++</strong> do takich konstrukcji używa się kilku bardzo ważnych słów<br />
kluczowych:<br />
[S] some important keywords - kilka ważnych słów kluczowych<br />
________________________________________________________________<br />
for - dla (znaczenie jak w Pascalu i BASICu)<br />
while - dopóki<br />
do - wykonuj<br />
if - jeżeli<br />
break - przerwij wykonywanie pętli<br />
continue - kontynuuj pętelkowanie<br />
goto - skocz do wskazanej etykiety<br />
________________________________________________________________<br />
- 84-
Nasz program mógłby przy zastosowaniu tych słów zostać napisany<br />
np. tak:<br />
[LOOP-1]<br />
#include <br />
void main()<br />
{<br />
int x = 2;<br />
petla:<br />
cout
{<br />
cout
[P016.CPP]<br />
// Przyklad FACELIFT.CPP<br />
// Program przykladowy 10na16.CPP / 16na10.CPP FACE LIFTING.<br />
# include <br />
int liczba;<br />
int main()<br />
{<br />
clrscr();<br />
printf("Kropka = KONIEC \n");<br />
for(;;)<br />
{<br />
printf("Podaj liczbe dziesietna calkowita ? \n");<br />
scanf("%d", &liczba);<br />
printf("Szesnastkowo to wynosi:\n");<br />
printf("%X",liczba);<br />
getch();<br />
printf("Podaj liczbe SZESNASTKOWA-np.DF- DUZE LITERY: \n");<br />
scanf("%X", &liczba);<br />
printf("%s","Dziesietnie to wynosi: ");<br />
printf("%d",liczba);<br />
if(getch() == '.') break;<br />
}<br />
return 0;<br />
}<br />
- Uruchom program Run, Run.<br />
- Dla przetestowania działania programu:<br />
* podaj kolejno liczby o różnej długości 1, 2, 3, 4, 5, 6<br />
cyfrowe;<br />
* zwróć uwagę, czy program przetwarza poprawnie liczby dowolnej<br />
długości?<br />
- Przerwij program naciskając klawisz z kropką [.]<br />
- Zapisz program na dysk [F2].<br />
- Wyjdź z IDE naciskając klawisze [Alt]-[X].<br />
Zwróć uwagę na dziwny wiersz:<br />
if(getch() == '.') break;<br />
<strong>C++</strong> wykona go w następującej kolejności:<br />
1) - wywoła funkcję getch(), poczeka na naciśnięcie klawisza i<br />
wczyta znak z klawiatury:<br />
getch()<br />
2) - sprawdzi, czy znak był kropką:<br />
(getch() == '.') ?<br />
3) - jeśli TAK - wykona rozkaz break i przerwie pętlę,<br />
if(getch() == '.') break;<br />
- jeśli NIE - nie zrobi nic i pętla "potoczy się" dalej.<br />
if(getch() != '.') ...--> printf("Podaj liczbe dziesietna...<br />
- 87-
[Z]<br />
________________________________________________________________<br />
2. Opracuj program pobierający znak z klawiatury i podający w<br />
odpowiedzi kod ASCII pobranego znaku dziesiętnie.<br />
3. Opracuj program pobierający liczbę dziesiętną i podający w<br />
odpowiedzi:<br />
* kod ósemkowy,<br />
* kod szesnastkowy,<br />
* znak o zadanym<br />
** dziesiętnie<br />
** szesnastkowo<br />
kodzie ASCII.<br />
_______________________________________________________________<br />
- 88-
LEKCJA 10. Jakie operatory stosuje <strong>C++</strong>.<br />
_______________________________________________________________<br />
Podczas tej lekcji:<br />
* Poznasz operatory języka <strong>C++</strong>.<br />
* Przetestujesz działanie niektórych operatorów.<br />
* Dowiesz się więcej o deklarowaniu i inicjowaniu zmiennych.<br />
_______________________________________________________________<br />
Słów kluczowych jest w języku <strong>C++</strong> stosunkowo niewiele, za to<br />
operatorów wyraźnie więcej niż np. w Basicu. Z kilku operatorów<br />
już korzystałeś w swoich programach. pełną listę operatorów<br />
wraz z krótkim wyjaśnieniem przedstawiam poniżej. Operatory <strong>C++</strong><br />
są podzielone na 16 grup i można je scharakteryzować:<br />
* priorytetem<br />
** najwyższy priorytet ma grupa 1 a najniższy grupa 16 -<br />
przecinek, np. mnożenie ma wyższy priorytet niż dodawanie;<br />
** wewnątrz każdej z 16 grup priorytet operatorów jest równy;<br />
* łącznością (wiązaniem).<br />
[S!] Precedence - kolejność, priorytet.<br />
________________________________________________________________<br />
Dwie cechy opertorów <strong>C++</strong> priorytet i łączność decydują o<br />
sposobie obliczania wartości wyrażeń.<br />
Precedence - kolejność, priorytet.<br />
Associativity - asocjatywność, łączność, wiązanie. Operator jest<br />
łączny lewo/prawo-stronnie, jeśli w wyrażeniu zawierającym na<br />
tym samym poziomie hierarchii nawiasów min. dwa identyczne<br />
operatory najpierw jest wykonywany operator lewy/prawy. Operator<br />
jest łączny, jeśli kolejność wykonania nie wpływa na wynik.<br />
________________________________________________________________<br />
Przykład:<br />
a+b+c+d = (a+d)+(c+b)<br />
[S]<br />
________________________________________________________________<br />
ASSIGN(ment) - Przypisanie.<br />
EQAL(ity) - Równy, odpowiadający.<br />
BITWISE - bit po bicie (bitowo).<br />
REFERENCE - odwołanie do..., powołanie się na..., wskazanie<br />
na... .<br />
Funkcje logiczne:<br />
OR - LUB - suma logiczna (alternatywa).<br />
AND - I - iloczyn logiczny.<br />
XOR (eXclusive OR) - ALBO - alternatywa wyłączająca.<br />
NOT - NIE - negacja logiczna.<br />
________________________________________________________________<br />
Oznaczenia łączności przyjęte w Tabeli:<br />
{L->R} €€€(Left to Right) z lewa na prawo.<br />
{L
Lista operatorów języka <strong>C++</strong>.<br />
________________________________________________________________<br />
Kategoria€| Operator€€€€€| €€€Co robi / jak działa<br />
----------|--------------|--------------------------------------<br />
1. Highest| ()€€€€€€€€€€€| * ogranicza wyrażenia,<br />
(Najwyższy|Parentheses | * izoluje wyrażenia warunkowe,<br />
priorytet)|€€€€€€€€€€€€€€| * wskazuje na wywołanie funkcji,<br />
{L->R}€€€|€€€€€€€€€€€€€€| grupuje argumenty funkcji.<br />
€€€€€€€€€€|--------------|--------------------------------------<br />
€€€€€€€€€€| []€€€€€€€€€€€| zawartość jedno- lub wielowymiarowych<br />
€€€€€€€€€€|Brackets | tablic<br />
€€€€€€€€€€|--------------|--------------------------------------<br />
€€€€€€€€€€| . |(direct component selector)<br />
€€€€€€€€€€| -> |(indirect, or pointer, selection)<br />
€€€€€€€€€€|€€€€€€€€€€€€€€| Bezpośrednie lub pośrednie wskazanie<br />
| | elementu unii bądź struktury.<br />
|--------------|--------------------------------------<br />
| :: | Operator specyficzny dla <strong>C++</strong>.<br />
| | Pozwala na dostęp do nazw GLOBALNYCH,<br />
| | nawet jeśli zostały "przysłonięte"<br />
| | przez LOKALNE.<br />
----------|--------------|--------------------------------------<br />
2. €€€€€€€| ! €€€€€€€€€€€| Negacja logiczna (NOT)<br />
Jednoar-€|--------------|------------------------------------<br />
gumentowe | ~ | Zamiana na kod KOMPLEMENTARNY bit po<br />
(Unary) | | bicie. Dotyczy liczb typu int.<br />
{L
----------|--------------|--------------------------------------<br />
4. Dostępu| .* | Operatory specyficzne dla <strong>C++</strong>.<br />
(Member |(dereference) | Skasowanie bezpośredniego wskazania<br />
access) | | na członka klasy (Class Member).<br />
{L->R} |--------------|--------------------------------------<br />
| ->* | Skasowanie pośredniego wskazania typu<br />
objektowe | | "wskaźnik do wskaźnika"<br />
----------|--------------|--------------------------------------<br />
5. Addy - | + | Dodawanie dwuargumentowe.<br />
tywne |--------------|--------------------------------------<br />
{L->R} | - | Odejmowanie dwuargumentowe.<br />
----------|--------------|--------------------------------------<br />
6. Przesu-| > | Binarne przesunięcie w prawo.<br />
{L->R} | | (bit po bicie)<br />
----------|--------------|--------------------------------------<br />
7. Relacji| < | Mniejsze niż...<br />
{L->R} |--------------|--------------------------------------<br />
| > | Większe niż....<br />
|--------------|--------------------------------------<br />
| = | Większe lub równe.<br />
----------|--------------|--------------------------------------<br />
8.Równości| == | Równe (równa się).<br />
{L->R} | != | Nie równe.<br />
----------|--------------|--------------------------------------<br />
9. | & | AND binarnie (Bitwise AND)<br />
{L->R} | | UWAGA: Druga rola "&".<br />
----------|--------------|--------------------------------------<br />
10. | ^ | XOR binarnie (Alternatywa wyłączna).<br />
{L->R} | | UWAGA: To nie potęga !<br />
----------|--------------|-------------------------------------<br />
11.{L->R} | | | OR binarnie (bit po bicie)<br />
----------|--------------|-------------------------------------<br />
12.{L->R} | && | Iloczyn logiczny (Logical AND).<br />
----------|--------------|-------------------------------------<br />
13.{L->R} | || | Suma logiczna (Logical OR).<br />
----------|--------------|--------------------------------------<br />
14. Oper. | ?: | Zapis a ? x : y oznacza:<br />
Warunkowy | | "if a==TRUE then x else y"<br />
Conditional | gdzie TRUE to logiczna PRAWDA "1".<br />
{L
|--------------|--------------------------------------<br />
| -= | Przypisz różnicę X-=5 ozn. "X:=X-5"<br />
|--------------|--------------------------------------<br />
| &= | Przypisz iloczyn binarny ( Bitwise<br />
| | AND)<br />
| | bit po bicie.<br />
|--------------|--------------------------------------<br />
| ^= | Przypisz XOR bit po bicie.<br />
|--------------|--------------------------------------<br />
| |= | Przypisz sumę log. bit po bicie.<br />
|--------------|--------------------------------------<br />
| = | j. w. o jeden bit w prawo.<br />
----------|--------------|--------------------------------------<br />
16. Prze- | , | Oddziela elementy na liście argu -<br />
cinek | | mentów funkcji,<br />
(Comma) | | Stosowany w specjalnych wyrażeniach<br />
{L->R} | | tzw. "Comma Expression".<br />
----------|--------------|-------------------------------------<br />
UWAGI:<br />
* Operatory # i ## stosuje się tylko w PREPROCESORZE.<br />
* Operatory > mogą w <strong>C++</strong> przesyłać tekst do obiektów cin i<br />
cout dzięki tzw. Overloadingowi (rozbudowie, przeciążeniu)<br />
operatorów. Takiego rozszerzenia ich działania dokonali już<br />
programiści producenta w pliku nagłówkowym IOSTREAM.H><br />
Gdyby okazało się, że oferowane przez powyższy zestaw operatory<br />
nie wystarczają Ci lub niezbyt odpowiadają, <strong>C++</strong> pozwala na tzw.<br />
OVERLOADING, czyli przypisanie operatorom innego, wybranego<br />
przez użytkownika działania. Można więc z operatorami robić<br />
takie same sztuczki jak z identyfikatorami. Sądzę jednak, że ten<br />
zestaw nam wystarczy, w każdym razie na kilka najbliższych<br />
lekcji.<br />
Podobnie, jak pieniądze na bezludnej wyspie, niewiele warta jest<br />
wiedza, której nie można zastosować praktycznie. Przejdźmy więc<br />
do czynu i przetestujmy działanie niektórych operatorów w<br />
praktyce.<br />
TEST OPERATORÓW JEDNOARGUMENTOWYCH.<br />
Otwórz plik nowego programu:<br />
* Open [F3],<br />
* Wpisz:<br />
A:\UNARY.CPP<br />
* Wybierz klawisz [Open] w okienku lub naciśnij [Enter].<br />
Wpisz tekst programu:<br />
- 92-
[P017.CPP ]<br />
// UNARY.CPP - operatory jednoargumentowe<br />
# include <br />
# include <br />
float x;<br />
void main(void)<br />
{<br />
clrscr();<br />
for(;;)<br />
{<br />
printf("\n Podaj liczbe...\n");<br />
scanf("%f", &x);<br />
printf("\n%f\t%f\t%f\n", x, +x, -x );<br />
printf("\n%f", --x );<br />
printf("\t%f", x );<br />
printf("\t%f", ++x);<br />
if(getch() = '.') break;<br />
};<br />
}<br />
Zwróć uwagę, że po nawiasie zamykającym pętlę nie ma tym razem<br />
żadnego rozkazu. Nie wystąpi także ostrzeżenie (Warning:) przy<br />
kompilacji.<br />
Uruchom program Run | Run. Popraw ewentualne błędy.<br />
Podając różne wartości liczby x:<br />
- dodatnie i ujemne,<br />
- całkowite i rzeczywiste,<br />
przeanalizuj działanie operatorów.<br />
Przerwij program naciskając klawisz [.]<br />
Zmodyfikuj w programie deklarację typu zmiennej X wpisując<br />
kolejno:<br />
- float x; (rzeczywista)<br />
- int x; €€€€€€(całkowita)<br />
- short int x; €€€€€(krótka całkowita)<br />
- long int x;€€€€€(długa całkowita)<br />
Zwróć uwagę, że zmiana deklaracji zmiennej bez JEDNOCZESNEJ<br />
zmiany formatu w funkcjach scanf() i printf() spowoduje<br />
komunikaty o błędach.<br />
Spróbuj samodzielnie dobrać odpowiednie formaty w funkcjach<br />
scanf() i printf(). Spróbuj zastosować zamiast funkcji printf()<br />
i scanf() strumienie cin i cout. Pamiętaj o dołączeniu<br />
właściwych plików nagłówkowych.<br />
Jeśli miałeś kłopot z dobraniem stosownych formatów, nie<br />
przejmuj się. Przyjrzyj się następnym przykładowym programom.<br />
- 93-
Zajmijmy się teraz dokładniej INKREMENTACJĄ, DEKREMENTACJĄ i<br />
OPERATORAMI PRZYPISANIA.<br />
1. Zamknij zbędne okna na ekranie. Pamuiętaj o zapisaniu<br />
programów na dyskietkę/dysk w tej wersji, która poprawnie działa<br />
lub w ostatniej wersji roboczej.<br />
2. Otwórz plik:<br />
ASSIGN.CPP<br />
3. Wpisz tekst programu:<br />
[P018.CPP]<br />
# include <br />
# include <br />
long int x;<br />
short int krok;<br />
char klawisz;<br />
int main()<br />
{<br />
clrscr();<br />
printf("Test operatora przypisania += \n");<br />
x=0;<br />
printf("Podaj KROK ? \n");<br />
scanf("%d",&krok);<br />
for(;;)<br />
{<br />
printf("\n%d\n", x+=krok);<br />
printf("[Enter] - dalej [K] - Koniec\n");<br />
klawisz = getch();<br />
if (klawisz=='k'|| klawisz=='K') goto koniec;<br />
}<br />
koniec:<br />
printf("\n Nacisnij dowolny klawisz...");<br />
getch();<br />
return 0;<br />
}<br />
W tym programie już sami "ręcznie" sprawdzamy, czy nie pora<br />
przerwać pętlę. Zamiast użyć typowej instrukcji break (przerwij)<br />
stosujemy nielubiane goto, gdyż jest bardziej uniwersalne i w<br />
przeciwieństwie do break pozwala wyraźnie pokazać dokąd<br />
następuje skok po przerwaniu pętli. Zwróć uwagę na nowe elementy<br />
w programie:<br />
* DEKLARACJE ZMIENNYCH:<br />
long int x; (długa, całkowita)<br />
short int krok; €€€€(krótka, całkowita)<br />
char klawisz;€€€€€(zmienna znakowa)<br />
- 94-
* INSTRUKCJĘ WARUNKOWĄ:<br />
if (KLAWISZ=='k'|| KLAWISZ=='K') goto koniec;<br />
(JEŻELI zmienna KLAWISZ równa się "k" LUB równa się "K"<br />
idź do etykiety "koniec:")<br />
* Warunek sprawdzany po słowie if jest ujęty w nawiasy.<br />
* Nadanie wartości zmiennej znakowej char klawisz przez funkcję:<br />
klawisz = getch();<br />
4. Skompiluj program. Popraw ewentualne błędy.<br />
5. Uruchom program. Podając różne liczby (tylko całkowite!)<br />
prześledź działanie operatora.<br />
6. Zapisz poprawną wersję programu na dysk/dyskietkę [F2].<br />
7. Jeśli masz już dość, wyjdź z TC - [Alt]-[X], jeśli nie,<br />
pozamykaj tylko zbędne okna i możesz przejść do zadań do<br />
samodzielnego rozwiązania -> [Z]!<br />
[Z]<br />
________________________________________________________________<br />
1. Do programu przykładowego wstaw kolejno różne operatory<br />
przypisania:<br />
*=, -=, /= itp.<br />
Prześledź działanie operatorów.<br />
2. W programie przykładowym zmień typ zmiennych:<br />
long int x; na float x;<br />
short int KROK;<br />
float KROK;<br />
Przetestuj działanie operatorów w przypadku liczb<br />
zmiennoprzecinkowych.<br />
3. Zapisz w języku <strong>C++</strong><br />
* negację iloczynu logicznego,<br />
* sumę logiczną negacji dwu warunków.<br />
________________________________________________________________<br />
TEST OPERATORÓW PRE/POST-INKREMENTACJI.<br />
W następnym programie zilustrujemy działanie wszystkich pięciu<br />
operatorów inkrementacji (dekrementacja to też inkrementacja<br />
tylko w przeciwną stronę).<br />
[P019.CPP]<br />
# include <br />
# include <br />
int b,c,d,e;<br />
int i;<br />
int STO = 100;<br />
void main(void)<br />
{<br />
clrscr();<br />
- 95-
printf("Demonstruje dzialanie \n");<br />
printf(" PREinkrementacji POSTinkrementacji");<br />
printf("\nNr€€€€--X€€€€€€++X€€€€€€€€€€€€X--€€€€€€€X++ \n");<br />
b = c = d = e = STO;<br />
for(i=1; i
Zapraszamy teraz debugger do pracy wydając mu polecenie "Wykonaj<br />
Inspekcję" [Alt]-[D] | Inspect. Pojawia się okienko dialogowe<br />
"Inspect".<br />
* Wpisz do okienka tekstowego nazwę zmiennej b i naciśnij<br />
[Enter].<br />
Pojawiło się okienko dialogowe "Inspecting b" zawierające<br />
fizyczny adres pamięci RAM, pod którym umieszczono zmienną b i<br />
wartość zmiennej b (zero; instrukcja przypisania nada jej<br />
wartość 100). Naciśnij [Esc]. Okienko zniknęło.<br />
[F7] - for(i=1; i
...<br />
i
LEKCJA 11. Jak deklarować zmienne. Co to jest wskaźnik.<br />
________________________________________________________________<br />
W trakcie tej lekcji:<br />
1. Dowiesz się więcej o deklaracjach.<br />
2. Poprawisz trochę system MS DOS.<br />
3. Dowiesz się co to jest wskaźnik i do czego służy.<br />
________________________________________________________________<br />
Więcej o deklaracjach.<br />
Deklarować można w języku <strong>C++</strong>:<br />
* zmienne;<br />
* funkcje;<br />
* typy (chodzi oczywiście o typy "nietypowe").<br />
Zmienne w języku <strong>C++</strong> mogą mieć charakter:<br />
* skalarów - którym przypisuje się nierozdzielne dane np.<br />
całkowite, rzeczywiste, wskazujące (typu wskaźnik) itp.<br />
* agregatów - którym przypisuje się dane typu strukturalnego np.<br />
obiektowe, tablicowe czy strukturowe.<br />
Powyższy podział nie jest tak całkiem precyzyjny, ponieważ<br />
pomiędzy wskaźnikami a tablicami istnieje w języku <strong>C++</strong> dość<br />
specyficzna zależność, ale więcej na ten temat dowiesz się z<br />
późniejszych lekcji.<br />
Zmienne mogą być:<br />
* deklarowane,<br />
* definiowane i<br />
* inicjowane.<br />
Stała to to taka zmienna, której wartość można przypisać tylko<br />
raz. Z punktu widzenia komputera niewiele się to różni, bo<br />
miejsce w pamięci i tak, stosownie do zadeklarowanego typu<br />
zarezerwować trzeba, umieścić w tablicy i zapamiętać sobie<br />
identyfikator i adres też. Jedyna praktyczna różnica polega na<br />
tym, że zmiennej zadeklarowanej jako stała, np.:<br />
const float PI = 3.142;<br />
nie można przypisać w programie żadnej innej wartości, innymi<br />
słowy zapis:<br />
const float PI = 3.14;<br />
jest jednocześnie DEKLARACJĄ, DEFINICJĄ i ZAINICJOWANIEM stałej<br />
PI.<br />
Przykład :<br />
- 99-
float x,y,z;€€€€€€€€€€€€€€€€€€€€€€€(DEKLARACJA)<br />
const float TEMP = 36.6;€€€€€€€€€€€(DEFINICJA)<br />
x = 42;€€€€€€€€€€€€€€€€€€€€€€€€€€€€€(ZAINICJOWANIE zmiennej)<br />
[S!] constant/variable - STAŁA czy ZMIENNA.<br />
________________________________________________________________<br />
const - (CONSTant) - stała. Deklaracja stałej, słowo kluczowe w<br />
języku C.<br />
var - (VARiable) - zmienna. W języku C przyjmowane domyślnie.<br />
Słowo var (stosowane w Pascalu) NIE JEST słowem kluczowym języka<br />
C ani <strong>C++</strong> (!).<br />
________________________________________________________________<br />
Skutek praktyczny:<br />
* Ma sens i jest poprawna deklaracja:<br />
const float PI = 3.1416;<br />
* Niepoprawna natomiast jest deklaracja:<br />
var float x;<br />
Jeśli nie zadeklarowano stałej słowem const, to "zmienna" (var)<br />
przyjmowana jest domyślnie.<br />
Definicja powoduje nie tylko określenie, jakiego typu<br />
wartościami może operować dana zmienna bądź funkcja, która<br />
zostaje od tego momentu skojarzona z podanym identyfikatorem,<br />
ale dodatkowo powoduje:<br />
* w przypadku zmiennej - przypisanie jej wartości,<br />
* W przypadku funkcji - przyporządkowanie ciała funkcji.<br />
Zdefiniujmy dla przykładu kilka własnych funkcji.<br />
Przykład:<br />
void UstawDosErrorlevel(int n) /* nazwa funkcji*/<br />
{<br />
exit(n); /* skromne ciało funkcji */<br />
}<br />
Przykład<br />
int DrukujAutora(void)<br />
{<br />
printf("\nAdam MAJCZAK AD 1993/95 - <strong>C++</strong> w 48 godzin!\n");<br />
printf("\n Wydanie II Poprawione i uzupełnione.")<br />
return 0;<br />
}<br />
Przykład<br />
void Drukuj_Pytanie(void)<br />
{<br />
printf("Podaj liczbe z zakresu od 0 do 255");<br />
printf("\nUstawie Ci ERRORLEVEL\t");<br />
}<br />
- 100-
W powyższych przykładach zwróć uwagę na:<br />
* sposób deklarowania zmiennej, przekazywanej jako parametr do<br />
funkcji - n i err;<br />
* definicje funkcji i ich wywołanie w programie (podobnie jak w<br />
Pascalu).<br />
Zilustrujemy zastosowanie tego mechanizmu w programie<br />
przykładowym. Funkcje powyższe są PREDEFINIOWANE w pliku<br />
FUNKCJE1.H na dyskietce dołączonej do książki. Wpisz i uruchom<br />
program:<br />
[P020.CPP]<br />
# include "stdio.h"<br />
# include "A:\funkcje1.h"<br />
int err;<br />
void main(void)<br />
{<br />
DrukujAutora();<br />
Drukuj_Pytanie();<br />
scanf("%d", &err);<br />
UstawDosErrorlevel(err);<br />
}<br />
Wykorzystajmy te funkcje praktycznie, by zilustrować sposób<br />
przekazywania informacji przez pracujący program do systemu DOS.<br />
Zmienna otoczenia systemowego DOS ERRORLEVEL może być z wnętrza<br />
programu ustawiona na zadaną - zwracaną do systemu wartość.<br />
[Z]<br />
________________________________________________________________<br />
1. Sprawdź, w jakim pliku nagłówkowym znajduje się prototyp<br />
funkcji exit(). Opracuj najprostszy program PYTAJ.EXE<br />
ustawiający zmienną systemową ERRORLEVEL według schematu:<br />
main()<br />
{<br />
printf("....Pytanie do użytkownika \n...");<br />
scanf("%d", &n);<br />
exit(n);<br />
}<br />
2. Zastosuj program PYTAJ.EXE we własnych plikach wsadowych typu<br />
*.BAT według wzoru:<br />
@echo off<br />
:LOOP<br />
cls<br />
echo 1. Wariant 1<br />
echo 2. Wariant 2<br />
echo 3. Wariant 3<br />
echo Wybierz wariant działania programu...1,2,3 ?<br />
- 101-
PYTAJ<br />
IF ERRORLEVEL 3 GOTO START3<br />
IF ERRORLEVEL 2 GOTO START2<br />
IF ERRORLEVEL 1 GOTO START1<br />
echo Chyba zartujesz...?<br />
goto LOOP<br />
:START1<br />
'AKCJA WARIANT 1<br />
GOTO KONIEC<br />
:START2<br />
'AKCJA WARIANT 2<br />
GOTO KONIEC<br />
:START3<br />
'AKCJA WARIANT 3<br />
:KONIEC<br />
'AKCJA WARIANT n - oznacza dowolny ciąg komend systemu DOS, np.<br />
COPY, MD, DEL, lub uruchomienie dowolnego programu. Do<br />
utworzenia pliku wsadowego możesz zastosować edytor systemowy<br />
EDIT.<br />
3. Skompiluj program posługując się oddzielnym kompilatorem<br />
TCC.EXE. Ten wariant kompilatora jest pozbawiony zintegrowanego<br />
edytora. Musisz uruchomić go pisząc odpowiedni rozkaz po<br />
DOS-owskim znaku zachęty C:\>. Zastosowanie przy kompilacji<br />
małego modelu pamięci pozwol Ci uzyskać swój program w wersji<br />
*.COM, a nie *.EXE. Wydaj rozkaz:<br />
c:\borlandc\bin\bcc -mt -lt c:\pytaj.cpp<br />
Jeśli pliki znajdują się w różnych katalogach, podaj właściwe<br />
ścieżki dostępu (path).<br />
________________________________________________________________<br />
[???] CO TO ZA PARAMETRY ???<br />
________________________________________________________________<br />
Przez swą "ułomność" - 16 bitową szynę i segmentację pamięci<br />
komputery IBM PC wymusiły wprowadzenie modeli pamięci:<br />
TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE. Więcej informacji na<br />
ten temat znajdziesz w dalszej części książki.<br />
Parametry dotyczą sposobu kompilacji i zastosowanego modelu<br />
pamięci:<br />
-mt - kompiluj (->*.OBJ) wykorzystując model TINY<br />
-lt - konsoliduj (->*.COM) wykorzystując model TINY i zatem<br />
odpowiednie biblioteki (do każdego modelu jest odpowiednia<br />
biblioteka *.LIB).<br />
Możesz stosować także:<br />
ms, mm, ml, mh, ls, lm, ll, lh.<br />
________________________________________________________________<br />
Po instalacji BORLAND <strong>C++</strong>/Turbo <strong>C++</strong> standardowo jest przyjmowany<br />
model SMALL. Zatem kompilacja, którą wykonujesz z IDE daje taki<br />
- 102-
sam efekt, jak zastosowanie kompilatora bcc/tcc w następujący<br />
sposób:<br />
tcc -ms -ls program.c<br />
Mogą wystąpić kłopoty z przerobieniem z EXE na COM tych<br />
programów, w których występują funkcje realizujące arytmetykę<br />
zmiennoprzecinkową (float). System DOS oferuje Ci do takich<br />
celów program EXE2BIN, ale lepiej jest "panować" nad tym<br />
problemem na etapie tworzenia programu.<br />
PODSTAWOWE TYPY DANYCH W JĘZYKU <strong>C++</strong>.<br />
Język C/<strong>C++</strong> operuje pięcioma podstawowymi typami danych:<br />
* char (znak, numer znaku w kodzie ASCII) - 1 bajt;<br />
* int (liczba całkowita) - 2 bajty;<br />
* float (liczba z pływającym przecinkiem) - 4 bajty;<br />
* double (podwójna ilość cyfr znaczących) - 8 bajtów;<br />
* void (nieokreślona) 0 bajtów.<br />
Zakres wartości przedstawiono w Tabeli poniżej.<br />
Podstawowe typy danych w <strong>C++</strong>.<br />
________________________________________________________________<br />
Typ Znak Bajtów Zakres wartości<br />
________________________________________________________________<br />
char signed 1 -128...+127<br />
int signed 2 -32768...+32767<br />
float signed 4 +-3.4E+-38 (dokładność: 7 cyfr)<br />
double signed 8 1.7E+-308 (dokładność: 15 cyfr)<br />
void nie dotyczy 0 bez określonej wartości.<br />
________________________________________________________________<br />
signed - ze znakiem, unsigned - bez znaku.<br />
Podstawowe typy danych mogą być stosowane z jednym z czterech<br />
modyfikatorów:<br />
* signed / unsigned - ze znakiem albo bez znaku<br />
* long / short - długi albo krótki<br />
Dla IBM PC typy int i short int są reprezentowane przez taki sam<br />
wewnętrzny format danych. Dla innych komputerów może być<br />
inaczej.<br />
Typy zmiennych w języku <strong>C++</strong> z zastosowaniem modyfikatorów<br />
(dopuszczalne kombinacje).<br />
________________________________________________________________<br />
Deklaracja Znak Bajtów Wartości Dyr. assembl.<br />
________________________________________________________________<br />
- 103-
char signed 1 -128...+127 DB<br />
int signed 2 -32768...+32767 DB<br />
short signed 2 -32768...+32767 DB<br />
short int signed 2 -32768...+32767 DB<br />
long signed 4 -2 147 483 648... DD<br />
+2 147 483 647<br />
long int signed 4 -2 147 483 648... DW<br />
+2 147 483 647<br />
unsigned char unsigned 1 0...+255 DB<br />
unsigned unsigned 2 0...+65 535 DW<br />
unsigned int unsigned 2 0...+65 535 DW<br />
unsigned short unsigned 2 0...+65 535 DW<br />
signed int signed 2 -32 768...+32 767 DW<br />
signed signed 2 -32 768...+32 767 DW<br />
signed long signed 4 -2 147 483 648... DD<br />
+2 147 483 647<br />
enum unsigned 2 0...+65 535 DW<br />
float signed 4 3.4E+-38 (7 cyfr) DD<br />
double signed 8 1.7E+-308 (15 cyfr) DQ<br />
long double signed 10 3.4E-4932...1.1E+4932 DT<br />
far * (far pointer, 386) 6 unsigned 2^48 - 1 DF, DP<br />
________________________________________________________________<br />
UWAGI:<br />
* DB - define byte - zdefiniuj bajt;<br />
DW - define word - zdefiniuj słowo (16 bitów);<br />
DD - double word - podwójne słowo (32 bity);<br />
DF, DP - define far pointer - daleki wskaźnik w 386;<br />
DQ - quad word - poczwórne słowo (4 * 16 = 64 bity);<br />
DT - ten bytes - dziesięć bajtów.<br />
* zwróć uwagę, że typ wyliczeniowy enum występuje jako odrębny<br />
typ danych (szczegóły w dalszej części książki).<br />
________________________________________________________________<br />
Ponieważ nie ma liczb ani short float, ani unsigned short float,<br />
słowo int może zostać opuszczone w deklaracji. Poprawne są zatem<br />
deklaracje:<br />
short a;<br />
unsigned short b;<br />
Zapis +-3.4E-38...3.4E+38 oznacza:<br />
-3.4*10^+38...0...+3.4*10^-38...+3.4*10^+38<br />
Dopuszczalne są deklaracje i definicje grupowe z zastosowaniem<br />
listy zmiennych. Zmienne na liście należy oddzielić przecinkami:<br />
int a=0, b=1, c, d;<br />
float PI=3.14, max=36.6;<br />
Poświęcimy teraz chwilę drugiej funkcji, którą już wielokrotnie<br />
- 104-
stosowaliśmy - funkcji wejścia - scanf().<br />
FUNKCJA scanf().<br />
Funkcja formatowanego wejścia ze standardowego strumienia<br />
wejściowego (stdin). Funkcja jest predefiniowana w pliku STDIO.H<br />
i korzystając z funkcji systemu operacyjnego wczytuje dane w<br />
postaci tekstu z klawiatury konsoli. Interpretacja pobranych<br />
przez funkcję scanf znaków nastąpi zgodnie z życzeniem<br />
programisty określonym przez zadany funkcji format (%f, %d, %c<br />
itp.). Wywołanie funkcji scanf ma postać:<br />
scanf(Format, Adres_zmiennej1, Adres_zmiennej2...);<br />
dla przykładu<br />
scanf("%f%f%f", &X1, &X2, &X3);<br />
wczytuje trzy liczby zmiennoprzecinkowe X1, X2 i X3.<br />
Format decyduje, czy pobrane znaki zostaną zinterpretowane np.<br />
jako liczba całkowita, znak, łańcuch znaków (napis), czy też w<br />
inny sposób. Od sposobu interpretacji zależy i rozmieszczenie<br />
ich w pamięci i późniejsze "sięgnięcie do nich", czyli odwołanie<br />
do danych umieszczonych w pamięci operacyjnej komputera.<br />
Zwróć uwagę, że podając nazwy (identyfikatory) zmiennych należy<br />
poprzedzić je w funkcji scanf() operatorem adresowym [&].<br />
Zapis:<br />
int X;<br />
...<br />
scanf("%d", &X);<br />
oznacza, że zostaną wykonane następujące działania:<br />
* Kompilator zarezerwuje 2 bajty pomięci w obszarze pamięci<br />
danych programu na zmienną X typu int;<br />
* W momencie wywołania funkcji scanf funkcji tej zostanie<br />
przekazany adres pamięci pod którym ma zostać umieszczona<br />
zmienna X, czyli tzw. WSKAZANIE DO ZMIENNEJ;<br />
* Znaki pobrane z klawiatury przez funkcję scanf mają zostać<br />
przekształcone do postaci wynikającej z wybranego formatu %d -<br />
tzn. do postaci zajmującej dwa bajty liczby całkowitej ze<br />
znakiem.<br />
[???] A JEŚLI PODAM INNY FORMAT ?<br />
________________________________________________________________<br />
<strong>C++</strong> wykona Twoje rozkazy najlepiej jak umie, niestety nie<br />
sprawdzając po drodze formatów, a z zer i jedynek zapisanych w<br />
pamięci RAM żaden format nie wynika. Otrzymasz błędne dane.<br />
________________________________________________________________<br />
Poniżej przykład skutków błędnego formatowania. Dołącz pliki<br />
- 105-
STDIO.H i CONIO.H.<br />
[P021.CPP]<br />
//UWAGA: Dołącz właściwe pliki nagłówkowe !<br />
void main()<br />
{<br />
float A, B;<br />
clrscr();<br />
scanf("%f %f", &A, &B);<br />
printf("\n%f\t%d", A,B);<br />
getch();<br />
}<br />
[Z]<br />
________________________________________________________________<br />
3 Zmień w programie przykładowym, w funkcji printf() wzorce<br />
formatu na %s, %c, itp. Porównaj wyniki.<br />
________________________________________________________________<br />
Adres w pamięci to taka sama liczba, jak wszystkie inne i wobec<br />
tego można nią manipulować. Adresami rządzą jednak dość<br />
specyficzne prawa, dlatego też w języku <strong>C++</strong> występuje jeszcze<br />
jeden specjalny typ zmiennych - tzw. ZMIENNE WSKAZUJĄCE (ang.<br />
pointer - wskaźnik). Twoja intuicja podpowiada Ci zapewne, że są<br />
to zmienne całkowite (nie ma przecież komórki pamięci o adresie<br />
0.245 ani 61/17). Pojęcia "komórka pamięci" a nie np. "bajt"<br />
używam świadomie, ponieważ obszar zajmowany w pamięci przez<br />
zmienną może mieć różną długość. Aby komputer wiedział ile<br />
kolejnych bajtów pamięci zajmuje wskazany obiekt (liczba długa,<br />
krótka, znak itp.), deklarując wskaźnik trzeba podać na co<br />
będzie wskazywał. W sposób "nieoficjalny" już w funkcji scanf<br />
korzystaliśmy z tego mechanizmu. Jest to zjawisko specyficzne<br />
dla języka <strong>C++</strong>, więc zajmijmy się nim trochę dokładniej.<br />
POJĘCIE ZMIENNEJ WSKAZUJĄCEJ I ZMIENNEJ WSKAZYWANEJ.<br />
Wskaźnik to zmienna, która zawiera adres innej zmiennej w<br />
pamięci komputera. Istnienie wskaźników umożliwia pośrednie<br />
odwoływanie się do wskazywanego obiektu (liczby, znaku, łańcucha<br />
znaków itp.) a także stosunkowo proste odwołanie się do obiektów<br />
sąsiadujących z nim w pamięci. Załóżmy, że:<br />
x - jest umieszczoną gdzieś w pamięci komputera zmienną<br />
całkowitą typu int zajmującą dwa kolejne bajty pamięci, a<br />
px - jest wskaźnikiem do zmiennej x.<br />
Jednoargumentowy operator & podaje adres obiektu, a zatem<br />
instrukcja:<br />
px = &x;<br />
- 106-
przypisuje wskaźnikowi px adres zmiennej x. Mówimy, że:<br />
px wskazuje na zmienną x lub<br />
px jest WSKAŹNIKIEM (pointerem) do zmiennej x.<br />
Jednoargumentowy operator * (naz. OPERATOREM WYŁUSKANIA)<br />
powoduje, że zmienna "potraktowana" tym operatorem jest<br />
traktowana jako adres pewnego obiektu. Zatem, jeśli przyjmiemy,<br />
że y jest zmienną typu int, to działania:<br />
y = x;<br />
oraz<br />
px = &x;<br />
y = *px;<br />
będą mieć identyczny skutek. Zapis y = x oznacza:<br />
"Nadaj zmiennej y dotychczasową wartość zmiennej x";<br />
a zapis y=*px oznacza:<br />
"Nadaj zmiennej y dotychczasową wartość zmiennej, której adres w<br />
pamięci wskazuje wskaźnik px" (czyli właśnie x !).<br />
Wskaźniki także wymagają deklaracji. Poprawna deklaracja w<br />
opisanym powyżej przypadku powinna wyglądać tak:<br />
int x,y;<br />
int *px;<br />
main()<br />
......<br />
Zapis int *px; oznacza:<br />
"px jest wskaźnikiem i będzie wskazywać na liczby typu int".<br />
Wskaźniki do zmiennych mogą zamiast zmiennych pojawiać się w<br />
wyrażeniach po PRAWEJ STRONIE, np. zapisy:<br />
int X,Y;<br />
int *pX;<br />
...<br />
pX = &X;<br />
.......<br />
Y = *pX + 1; €€€€€€/* to samo, co Y = X + 1 */<br />
printf("%d", *pX);€€€€€€€/* to samo, co printf("%d", X); */<br />
Y = sqrt(*pX);€€€€€€€€€€€/* pierwiastek kwadrat. z X */<br />
......<br />
są w języku <strong>C++</strong> poprawne.<br />
Zwróć uwagę, że operatory & i * mają wyższy priorytet niż<br />
operatory arytmetyczne, dzięki czemu<br />
* najpierw następuje pobranie spod wskazanego przez<br />
wskaźnik adresu zmiennej;<br />
- 107-
* potem następuje wykonanie operacji arytmetycznej;<br />
(operacja nie jest więc wykonywana na wskaźniku, a na<br />
wskazywanej zmiennej!).<br />
W języku <strong>C++</strong> możliwa jest także sytuacja odwrotna:<br />
Y = *(pX + 1);<br />
Ponieważ operator () ma wyższy priorytet niż * , więc:<br />
najpierw wskaźnik zostaje zwiększony o 1;<br />
potem zostaje pobrana z pamięci wartość znajdująca się pod<br />
wskazanym adresem (w tym momencie nie jest to już adres zmiennej<br />
X, a obiektu "następnego" w pamięci) i przypisana zmiennej Y.<br />
Taki sposób poruszania się po pamięci jest szczególnie wygodny,<br />
jeśli pod kolejnymi adresami pamięci rozmieścimy np. kolejne<br />
wyrazy z tablicy, czy kolejne znaki tekstu.<br />
Przyjrzyjmy się wyrażeniom, w których wskaźnik występuje po<br />
LEWEJ STRONIE. Zapisy:<br />
*pX = 0;€€€€€€€€€€€€i€€€€€€€€€X = 0;<br />
*pX += 1;€€€€€€€€€€€i€€€€€€€€€X += 1;<br />
(*pX)++;€€€€€€€€€€€€i€€€€€€€€€X++; /*3*/<br />
mają identyczne działanie. Zwróć uwagę w przykładzie /*3*/, że<br />
ze względu na priorytet operatorów<br />
() - najwyższy - najpierw pobieramy wskazaną zmienną;<br />
++ - niższy, potem zwiększmy wskazaną zmienną o 1;<br />
Gdyby zapis miał postać:<br />
*pX++;<br />
najpierw nastąpiłoby<br />
- zwiększenie wskaźnika o 1 i wskazanie "sąsiedniej" zmiennej,<br />
potem<br />
- wyłuskanie, czyli pobranie z pamięci zmiennej wskazanej przez<br />
nowy, zwiększony wskaźnik, zawartość pamięci natomiast, tj.<br />
wszystkie zmienne rozmieszczone w pamięci pozostałyby bez zmian.<br />
[???] JAK TO WŁAŚCIWIE JEST Z TYM PRIORYTETEM ?<br />
________________________________________________________________<br />
Wszystkie operatory jednoargumentowe (kategoria 2, patrz Tabela)<br />
mają taki sam priorytet, ale są PRAWOSTRONNIE ŁĄCZNE {L
Zwróć uwagę, że kolejność {L
clrscr();<br />
ptr1=&a;<br />
ptr2=&a;<br />
printf("Skok o 2Bajty Skok o 4Bajty");<br />
}<br />
for(i=0; i
77€€€€€€€€€€€€€28448<br />
88€€€€€€€€€€€€€8258<br />
99€€€€€€€€€€€€€27475<br />
10€€€€€€€€€€€€€2844<br />
Zwróć uwagę, że to deklaracja wskaźnika decyduje, co praktycznie<br />
oznacza operacja *(ptr + 1). W pierwszym przypadku wskaźnik<br />
powiększa się o 2 a w drugim o 4 bajty. Te odpowiednio 2 i 4<br />
bajty stanowią długość komórki pamięci lub precyzyjniej, pola<br />
pamięci przeznaczonego dla zmiennych określonego typu.<br />
Wartości pojawiające się w drugiej kolumnie po 99 są<br />
przypadkowe i u Ciebie mogą okazać się inne.<br />
<strong>C++</strong> pozwala wskaźnikom nie tylko wskazywać adres zmiennej w<br />
pamięci. Wskaźnik może również wskazywać na inny wskaźnik. Takie<br />
wskazania:<br />
int X; €€€int pX; €€int ppX;<br />
pX = &X;€€ppX = &pX;<br />
oznaczamy:<br />
*pX€€- pX wskazuje BEZPOŚREDNIO zmienną X;<br />
**ppX€- ppX skazuje POŚREDNIO zmienną X (jest wskaźnikiem do<br />
wskaźnika).<br />
***pppX - pppX wskazuje pośrednio wskaźnik do zmiennej X itd.<br />
[Z]<br />
________________________________________________________________<br />
4 Wybierz dowolne dwa przykładowe programy omawiane wcześniej i<br />
przeredaguj je posługując się zamiast zmiennych - wskaźnikami do<br />
tych zmiennych. Pamiętaj, że przed użyciem wskaźnika należy:<br />
* zadeklarować na jaki typ zmiennych wskazuje wskaźnik;<br />
* przyporządkować wskaźnik określonej zmiennej.<br />
5 Zastanów się, co oznacza ostrzeżenie wypisywane podczas<br />
uruchomienia programu przykładowego:<br />
Warning 8: Suspicious pointer conversion in function main.<br />
________________________________________________________________<br />
- 111-
LEKCJA 12. Wskaźniki i tablice w C i <strong>C++</strong>.<br />
________________________________________________________________<br />
W czasie tej lekcji:<br />
1. Dowiesz się więcej o zastosowaniu wskaźników.<br />
2. Zrozumiesz, co mają wspólnego wskaźniki i tablice w języku<br />
C/<strong>C++</strong>.<br />
________________________________________________________________<br />
WSKAŹNIKI I TABLICE W C i <strong>C++</strong>.<br />
W języku C/<strong>C++</strong> pomiędzy wskaźnikami a tablicami istnieje bardzo<br />
ścisły związek. Do ponumerowania elementów w tablicy służą tzw.<br />
INDEKSY. W języku C/<strong>C++</strong><br />
* KAŻDA OPERACJA korzystająca z indeksów może zostać wykonana<br />
przy pomocy wskaźników;<br />
* posługiwanie się wskaźnikiem zamiast indeksu na ogół<br />
przyspiesza operację.<br />
Tablice, podobnie jak zmienne i funkcje wymagają przed użyciem<br />
DEKLARACJI. Upraszczając problem - komputer musi wiedzieć ile<br />
miejsca zarezerwować w pamięci i w jaki sposób rozmieścić<br />
kolejne OBIEKTY, czyli kolejne elementy tablicy.<br />
[???] CO Z TYMI OBIEKTAMI ?<br />
________________________________________________________________<br />
OBIEKTEM w szerokim znaczeniu tego słowa jest każda liczba,<br />
znak, łańcuch znaków itp.. Takimi klasycznymi obiektami języki<br />
programowania operowały już od dawien dawna. Prawdziwe<br />
programowanie obiektowe w dzisiejszym, węższym znaczeniu<br />
rozpoczyna się jednak tam, gdzie obiektem może stać się także<br />
coś "nietypowego" - np. rysunek. Jest to jednak właściwy chyba<br />
moment, by zwrócić Ci uwagę, że z punktu widzenia komputera<br />
obiekt to coś, co zajmuje pewien obszar pamięci i z czym wiadomo<br />
jak postępować.<br />
________________________________________________________________<br />
Deklaracja:<br />
int A[12];<br />
oznacza:<br />
należy zarezerwować 12 KOLEJNYCH komórek pamięci dla 12 liczb<br />
całkowitych typu int (po 2 bajty każda). Jednowymiarowa tablica<br />
(wektor) będzie się nazywać "A", a jej kolejne elementy zostaną<br />
ponumerowane przy pomocy indeksu:<br />
- zwróć uwagę, że w C zaczynamy liczyć OD ZERA A NIE OD JEDYNKI;<br />
A[0], A[1], A[2], A[3], .... A[11].<br />
Jeśli chcemy zadeklarować:<br />
- indeks i;<br />
- wskaźnik, wskazujący nam początek (pierwszy, czyli zerowy<br />
- 112-
element) tablicy;<br />
- samą tablicę;<br />
to takie deklaracje powinny wyglądać następująco:<br />
int i;<br />
int *pA;<br />
int A[12];<br />
Aby wskaźnik wskazywał na początek tablicy A[12], musimy go<br />
jeszcze zainicjować:<br />
pA = &A[0];<br />
Jeśli poszczególne elementy tablicy są zawsze rozmieszczane<br />
KOLEJNO, to:<br />
*pA[0]<br />
oznacza:<br />
"wyłuskaj zawartość komórki pamięci wskazanej przez wskaźnik",<br />
czyli inaczej - pobierz z pamięci pierwszy (zerowy!) element<br />
tablicy A[]. Jeśli deklaracja typów elementów tablicy i<br />
deklaracja typu wskaźnika są zgodne i poprawne, nie musimy się<br />
dalej martwić ile bajtów zajmuje dany obiekt - element tablicy.<br />
Zapisy:<br />
*pA[0];€€€€€€€€*pA;€€€€€€€€€€€A[0]<br />
*(pA[0]+1)€€€€€*(pA+1)€€€€€€€€A[1]<br />
*(pA[0]+2)€€€€€*(pA+2)€€€€€€€€A[2]€€€€€€itd.<br />
są równoważne i oznaczają kolejne wyrazy tablicy A[].<br />
Jeśli tablica jest dwu- lub trójwymiarowa, początek tablicy<br />
oznacza zapis:<br />
A[0][0];<br />
A[0][0][0];<br />
itd.<br />
Zwróć uwagę, że wskaźnik do tablicy *pA oznacza praktycznie<br />
wskaźnik do POCZĄTKOWEGO ELEMENTU TABLICY:<br />
*pA == *pA[0]<br />
To samo można zapisać w języku <strong>C++</strong> w jeszcze inny sposób. Jeśli<br />
A jest nazwą tablicy, to zapis:<br />
*A<br />
oznacza wskazanie do początku tablicy A, a zapisy:<br />
*(A+1)€€€€€€€€€€€€*(pA+1)€€€€€€€€A[1]<br />
*(A+8)€€€€€€€€€€€€*(pA+8)€€€€€€€€A[8] itd.<br />
są równoważne. Podobnie identyczne znaczenie mają zapisy:<br />
- 113-
x = &A[i]€€€€€€€€x=A+i<br />
*pA[i]€€€€€€€€€*(A+i)<br />
Należy jednak podkreślić, że pomiędzy nazwami tablic (w naszym<br />
przykładzie A) a wskaźnikami istnieje zasadnicza różnica.<br />
Wskaźnik jest ZMIENNĄ, zatem operacje:<br />
pA = A;<br />
pA++;<br />
są dopuszczalne i sensowne. Nazwa tablicy natomiast jest STAŁĄ,<br />
zatem operacje:<br />
A = pA;€€€€€€€€€€€€€ŹLE !<br />
A++;€€€€€€€€€€€€€€€€ŹLE !<br />
są niedopuszczalne i próba ich wykonania spowoduje błędy !<br />
DEKLAROWANIE I INICJOWANIE TABLIC.<br />
Elementom tablicy, podobnie jak zmiennym możemy nadawać<br />
watrości. Wartości takie należy podawać w nawiasach klamrowych,<br />
a wielkość tablicy - w nawiasach kwadratowych.<br />
Przykład<br />
int WEKTOR[5];<br />
Tablica WEKTOR jest jednowymiarowa i składa się z 5 elementów<br />
typu int: WEKTOR[0]....WEKTOR[4].<br />
Przykład<br />
float Array[10][5];<br />
Tablica Array jest dwuwymiarowa i składa się z 50 elementów typu<br />
float: Array[0][0], Array[0][1]......Array[0][4]<br />
€€€€€€€Array[1][0], Array[1][1]......Array[1][4]<br />
€€€€€...........................................<br />
€€€€€ Array[9][0], Array[9][1]......Array[9][4]<br />
Przykład<br />
const int b[4]={1,2,33,444};<br />
Elementom jednowymiarowej tablicy (wektora) b przypisano<br />
wartośći: b[0]=1; b[1]=2; b[2]=33; b[3]=444;<br />
Przykład<br />
int TAB[2][3]={{1, 2, 3},{2, 4, 6}};<br />
€€€€€TAB[0][0]=1€€€€TAB[0][1]=2€€€€TAB[0][2]=3<br />
€€€€€TAB[1][0]=2€€€€TAB[1][1]=4€€€€TAB[1][2]=6<br />
Przykład : Tablica znakowa. Obie formy zapisu dają ten sam<br />
efekt.<br />
- 114-
char hej[5]="Ahoj";<br />
char hej[5]={'A', 'h', 'o', 'j'};<br />
€€€€€hej[0]='A'€€€€€hej[1]='h'€€€€€hej[2]='o' itp.<br />
Przykład : Tablica uzupełniona zerami przez domniemanie.<br />
float T[2][3]={{1, 2.22}, {.5}};<br />
kompilator uzupełni zerami do postaci:<br />
€€€€€T[0][0]=1€€€€€€T[0][1]=2.22€€€€€€€€T[0][2]=0<br />
€€€€€T[1][0]=0.5€€€€T[1][1]=0€€€€€€€€€€€T[1][2]=0<br />
Jeśli nawias kwadratowy zawierający wymiar pozostawimy pusty, to<br />
kompilator obliczy jego domniemaną zawartość w oparciu o podaną<br />
zawartość tablicy. Nie spowoduje więc błędu zapis:<br />
char D[]="Jakis napis"<br />
int A[][2]={{1,2}, {3,4}, {5,6}}<br />
Jeśli nie podamy ani wymiaru, ani zawartości:<br />
int A[];<br />
kompilator "zbuntuje się" i wykaże błąd.<br />
Dla przykładu, skompiluj program przykładowy. Zwróć uwagę na<br />
sposób zainicjowania wskaźnika.<br />
[P023.CPP]<br />
# include "stdio.h"<br />
# include <br />
int a[][2]={ {1,2},{3,4},{5,6},{7,8},{9,10},{11,12} };<br />
char b[]={ "Poniedzialek" };<br />
int i;<br />
int *pa;<br />
char *pb;<br />
void main()<br />
{<br />
pa = &a[0][0];<br />
pb = b; // lub pb = b[0];<br />
clrscr();<br />
for (i=0; i
...<br />
TAB3D[i][j][k];<br />
Jest w dobrym stylu panować nad swoimi danymi i umieszczać je w<br />
tzw. BUFORACH, czyli w wydzielonych obszarach pamięci o znanym<br />
adresie, wielkości i przeznaczeniu. W następnym programie<br />
przykładowym utworzymy taki bufor w postaci tablicy bufor[20] i<br />
zastosujemy zamiast funkcji scanf() czytającej bezpośrednio z<br />
klawiatury parę funkcji:<br />
gets() - GET String - pobierz łańcuch znaków z klawiatury do<br />
bufora;<br />
sscanf(bufor) - odczytaj z bufora (z pamięci).<br />
Aby uniknąć nielubianego goto stosujemy konstrukcję for - break.<br />
Dokładniej pętlę for omówimy w trakcie następnej lekcji.<br />
Ponieważ mam nadzieję, że "podstawową" postać pętli for<br />
pamiętasz z przykładów LOOP-n:<br />
for(i=1; i
}<br />
printf("Suma wynosi: %d\n", suma);<br />
printf("Srednia wynosi: %d\n", (suma / ile));<br />
getch();<br />
}<br />
Poniżej trochę bardziej "elegancka wersja" z zastosowaniem pętli<br />
typu while. Więcej o pętlach dowiesz się z następnej Lekcji.<br />
[P025.CPP]<br />
# include <br />
# include <br />
int liczba, ile=1, suma=0;<br />
void main()<br />
{<br />
char bufor[20];<br />
clrscr();<br />
printf("podaj liczby - ja oblicze SREDNIA i SUMA\n");<br />
printf("ZERO = KONIEC\n");<br />
gets(bufor);<br />
sscanf(bufor, "%d", &liczba);<br />
while (liczba != 0)<br />
{<br />
suma += liczba;<br />
gets(bufor);<br />
sscanf(bufor, "%d", &liczba);<br />
if(liczba == 0)<br />
printf("I to by bylo na tyle...\n");<br />
else<br />
ile++;<br />
}<br />
printf("Suma wynosi: %d\n", suma);<br />
printf("Srednia wynosi: %d\n", suma / ile);<br />
getch();<br />
}<br />
Program powyższy, choć operuje tablicą, robi to trochę jakby za<br />
kulisami. Utwórzmy zatem inną - bardziej "dydaktyczną" tablicę,<br />
której elementy byłyby łatwo rozpoznawalne.<br />
PRZYKŁADY TABLIC WIELOWYMIAROWYCH.<br />
Dzięki matematyce bardziej jesteśmy przyzwyczajeni do zapisu<br />
tablic w takiej postaci:<br />
€€€€€€€€€€a11€€a12€€a13€€a14€€a15€€a16<br />
€€€€€€€€€€a21€€a22€€a23€€a24€€a25€€a26<br />
€€€€€€€€€€a31€€a32€€a33€€a34€€a35€€a36<br />
€€€€€€€€€€a41€€a42€€a43€€a44€€a45€€a46<br />
- 117-
gdzie a i,j /** indeks**/ oznacza element tablicy zlokalizowany<br />
w:<br />
- wierszu i<br />
- kolumnie j<br />
Przypiszmy kolejnym elementom tablicy następujące wartości:<br />
€€€€€€€€€€11€€€12€€€13€€€14€€€15€€€16<br />
€€€€€€€€€€21€€€22€€€23€€€24€€€25€€€26<br />
€€€€€€€€€€31€€€32€€€33€€€34€€€35€€€36<br />
€€€€€€€€€€41€€€42€€€43€€€44€€€45€€€46<br />
Jest to tablica dwuwymiarowa o wymiarach 4WIERSZE X 6KOLUMN,<br />
czyli krócej 4X6. Liczby będące elementami tablicy są typu<br />
całkowitego. Jeśli zatem nazwiemy ją TABLICA, to zgodnie z<br />
zasadami przyjętymi w języku C/<strong>C++</strong> możemy ją zadeklarować:<br />
int TABLICA[4][6];<br />
Pamiętajmy, że <strong>C++</strong> liczy nie od jedynki a od zera, zatem<br />
TABLICA[0][0] = a11 = 11,<br />
TABLICA[2][3] = a34 = 34 itd.<br />
Znając zawartość tablicy możemy ją zdefiniować/zainicjować:<br />
int TABLICA[4][6]={{11,12,13,14,15,16},{21,22,23,24,25,26}<br />
{31,32,33,34,35,36},{41,42,43,44,45,46}};<br />
Taki sposób inicjowania tablicy, aczkolwiek pomaga wyjaśnić<br />
metodę, z punktu widzenia programistów jest trochę<br />
"nieelegancki". Liczbę przypisywaną danemu elementowi tablicy<br />
można łatwo obliczyć.<br />
TABLICA[i][j] = (i+1)*10 + (j+1);<br />
Przykładowo:<br />
TABLICA[2][5] = (2+1)*10 +(5+1) = 36<br />
Najbardziej oczywistym rozwiązaniem byłoby napisanie pętli<br />
int i, j;<br />
for (i=0; i
{31,32,33,34,35,36},{41,42,43,44,45,46}};<br />
# include <br />
# include <br />
int *pT;<br />
int i, j;<br />
void main()<br />
{<br />
clrscr();<br />
printf("OTO NASZA TABLICA \n");<br />
for (i=0; i
printf("i INKREMENTUJEMY wskaźnik *pT++ \n");<br />
pT=&TABLICA[0][0];<br />
for (i=0; i
}<br />
printf("\n");<br />
}<br />
printf("\n Inicjujemy i INKREMENTUJEMY wskaźnik *pT++ \n\n");<br />
pT=&T[0][0];<br />
for (k=0; k
pT =&T[0][0];<br />
printf("\t TABLICA znakowa (ineksy)\n\n");<br />
for (i=0; i
1. Posługując się wskaźnikiem i inkrementując wskaźnik z różnym<br />
krokiem - np. pT += 2; pT += 3 itp., zmodyfikuj programy<br />
przykładowe tak, by uzyskać wydruk tylko części tablicy.<br />
2. Spróbuj zastąpić inkrementację wskaźnika pT++ dekrementacją,<br />
odwracając tablicę "do góry nogami". Jak należałoby poprawnie<br />
zainicjować wskaźnik?<br />
3. Napisz program drukujący tabliczkę mnożenia w układzie<br />
szesnastkowym - od 1 * 1 do F * F.<br />
4. Wydrukuj nazwy dni tygodnia pionowo i wspak.<br />
5. Zinterpretuj następujące zapisy:<br />
int *pt_int;<br />
float *pt_float;<br />
int p = 7, d = 27;<br />
float x = 1.2345, Y = 32.14;<br />
void *general;<br />
pt_int = &p;<br />
*pt_int += d;<br />
general = pt_int;<br />
pt_float = &x;<br />
Y += 5 * (*pt_float);<br />
general = pt_float;<br />
const char *name1 = "Jasio"; // wskaźnik do STALEJ<br />
char *const name2 = "Grzesio"; // wskaźnik do STALEGO ADRESU<br />
________________________________________________________________<br />
- 123-
LEKCJA 13. Jak tworzyć w programie pętle i rozgałęzienia.<br />
_______________________________________________________________<br />
W trakcie tej lekcji:<br />
1. Dowiesz się znacznie więcej o pętlach.<br />
2. Przeanalizujemy instrukcje warunkowe i formułowanie warunków.<br />
_______________________________________________________________<br />
Zaczniemy tę lekcję nietypowo - od słownika, ponieważ dobrze<br />
jest rozumieć dokładnie co się pisze. Tym razem słownik jest<br />
trochę obszerniejszy. Pozwalam sobie przytoczyć niektóre słowa<br />
powtórnie - dla przypomnienia i Twojej wygody. Do organizacji<br />
pętli będą nam potrzebne następujące słowa:<br />
[S!] conditional expressions - wyrażenia warunkowe<br />
structural loops - pętle strukturalne<br />
________________________________________________________________<br />
if - jeżeli (poprzedza warunek do sprawdzenia);<br />
else - a jeśli nie, to (w przeciwnym wypadku...);<br />
for - dla;<br />
while - dopóki (dopóki nie spełnimy warunku);<br />
do - wykonaj, wykonuj;<br />
break - przerwij (wykonanie pętli);<br />
switch - przełącz;<br />
case - przypadek, wariant (jedna z możliwości);<br />
goto - idź do...<br />
default - domyślny, (automatyczny, pozostały);<br />
continue - kontynuuj (pętlę);<br />
________________________________________________________________<br />
UWAGA: W C/<strong>C++</strong> nie stosuje się słowa THEN.<br />
PĘTLA TYPU for.<br />
Ogólna postać pętli for jest następująca:<br />
for (W_inicjujące; W_logiczne; W_kroku) Instrukcja;<br />
gdzie skrót W_ oznacza wyrażenie. Każde z tych wyrażeń może<br />
zostać pominięte (patrz --> for(;;)).<br />
Wykonanie pętli for przebiega następująco:<br />
1. Wykonanie JEDEN raz WYRAŻENIA INICJUJĄCEGO.<br />
2. Obliczenie wartości LOGICZNEJ wyrażenia logicznego.<br />
3. Jeśli W_logiczne ma wartość PRAWDA (TRUE) nastąpi wykonanie<br />
Instrukcji.<br />
4. Obliczenie wyrażenia kroku.<br />
5. Powtórne sprawdzenie warunku - czy wyrażenie logiczne ma<br />
wartość różną od zera. Jeśli wyrażenie logiczne ma wartość zero,<br />
nastąpi zakończenie pętli.<br />
Warunek jest testowany PRZED wykonaniem instrukcji. Jeśli zatem<br />
nie zostanie spełniony warunek, instrukcja może nie wykonać się<br />
ANI RAZ.<br />
- 124-
Instrukcja może być INSTRUKCJĄ GRUPUJĄCĄ, składającą się z<br />
instrukcji prostych, deklaracji i definicji zmiennych lokalnych:<br />
{ ciąg deklaracji lub definicji;<br />
ciąg instrukcji; }<br />
Ogromnie ważny jest fakt, że <strong>C++</strong> ocenia wartość logiczną<br />
wyrażenia według zasady:<br />
0 - FALSE, FAŁSZ, inaczej ZERO LOGICZNE jeśli WYRAŻENIE == 0 lub<br />
jest fałszywe w znaczeniu logicznym;<br />
1 - TRUE, PRAWDA, JEDYNKA LOGICZNA, jeśli wyrażenie ma DOWOLNĄ<br />
WARTOŚĆ NUMERYCZNĄ RÓŻNĄ OD ZERA (!) lub jest prawdziwe w sensie<br />
logicznym.<br />
Przykład:<br />
"Klasycznie" zastosowana pętla for oblicza pierwiastki<br />
kwadratowe kolejnych liczb całkowitych.<br />
#include <br />
#include <br />
void main()<br />
{<br />
int n;<br />
for (n=0; n
€€€€€}<br />
getch();<br />
}<br />
UWAGA:<br />
Sprawdź, czy nawias (sqrt(n)-y)
const float d=1.2345;<br />
void main()<br />
{<br />
for (a=5,b=3.14,c=10; c; ++a,b*=d,c--)<br />
printf("\n%f\t%f\t%f", a,b,c);<br />
getch();<br />
}<br />
Zwróć uwagę, że zapisy warunku:<br />
if (c)...; i if (c != 0)...;<br />
są w <strong>C++</strong> równoważne.<br />
Przykład:<br />
Program będzie pisał kropki aż do naciśnięcia dowolnego<br />
klawisza, co wykryje funkcja kbhit(), będąca odpowiednikem<br />
KeyPressed w Pascalu. Zapis !kbhit() oznacza "NIE NACIŚNIĘTO<br />
KLAWISZA", czyli w buforze klawiatury nie oczekuje znak. Zwróć<br />
uwagę, że funkcja getch() może oczekiwać na klawisz w<br />
nieskończoność. Aby uniknąć kłopotliwych sytuacji, czasem<br />
znacznie wygodniej jest zastosować kbhit(), szczególnie, jeśli<br />
czekamy na DOWOLNY klawisz.<br />
void main()<br />
{<br />
for (; !kbhit(); printf("."));<br />
}<br />
Przykład:<br />
Wskaźnik w charakterze zmiennej roboczej w pętli typu for. Pętla<br />
powoduje wypisanie napisu.<br />
char *Ptr = "Jakis napis";<br />
void main()<br />
{<br />
for (; (*Ptr) ;)<br />
printf("%c",*Pt++);<br />
getch();<br />
}<br />
AUTOMATYCZNE GENEROWANIE TABLIC W PĘTLI for<br />
Na dyskietce znajdziesz jeszcze kilka przykładów FORxx.CPP<br />
użycia pętli. A teraz, zanim będziemy kontynuować naukę -<br />
przykładowy program do zabawy. Pętla for służy do wykrywania<br />
zgodności klawisza z elementami tablicy TABL[]. W tablicy D[]<br />
umieszczone zostały częstotliwości kolejnych dźwięków, które<br />
program oblicza sam, wykorzystując przybliżony współczynnik.<br />
[P030.CPP]<br />
- 127-
# include "conio.h"<br />
# include "dos.h"<br />
# include "math.h"<br />
# include "stdio.h"<br />
char TABL[27]={"zsxdcvgbhnjm,ZSXDCVGBHNJM
cout
void main(){<br />
float SUMA=0, n=0;<br />
clrscr();<br />
while (SUMA
if (Wyrażenie) Instrukcja;<br />
if (Wyrażenie) Instrukcja1 else Instrukcja2;<br />
Jeśli Wyrażenie ma wartość różną od zera (LOGICZNĄ bądź<br />
NUMERYCZNĄ !) to zostanie wykonana Instrukcja1, w przeciwnym<br />
razie wykonana zostanie Instrukcja2. Instrukcje mogą być<br />
instrukcjami grupującymi. Słowa kluczowe if i else mogą być<br />
stosowane wielokrotnie. Pozwala to tworzyć np. tzw. drzewa<br />
binarne.<br />
Przykład:<br />
void main()<br />
{<br />
float a;<br />
scanf("%f", &a);<br />
if (a0) if (a0) {if(a0 && ab)? MAX=a : MAX=b;<br />
inaczej:<br />
if (a>b) MAX=a; else MAX=b;<br />
INSTRUKCJE break i continue.<br />
Instrukcja break powoduje natychmiastowe bezwarunkowe<br />
opuszczenie pętli dowolnego typu i przejście do najbliższej<br />
instrukcji po zakończeniu pętli. Jeśli w pętli for opuścimy<br />
wyrażenie logiczne, to zostanie automatycznie przyjęte 1. Pętla<br />
będzie zatem wykonywana bezwarunkowo w nieskończoność. W<br />
przykładzie poniżej nieskończoną pętlę przerywa po podaniu z<br />
kalwiatury zera instrukcja break.<br />
- 131-
Przykład:<br />
float a, sigma=0;<br />
void main(){<br />
for (;;)<br />
{<br />
printf("\n Podaj liczbe do sumowania\n");<br />
scanf("%f", &a);<br />
if (a==0) break;<br />
sigma+=a;<br />
printf("\n SUMA: %f",sigma);<br />
}<br />
printf("Nastapil BREAK");<br />
getch();<br />
}<br />
Instrukcja continue.<br />
Instrukcja continue powoduje przedwczesne, bezwarunkowe<br />
zakończenie wykonania wewnętrznej instrukcji pętli i podjęcie<br />
próby realizacji następnego cyklu pętli. Próby, ponieważ<br />
najpierw zostanie sprawdzony warunek kontynuacji pętli. Program<br />
z przykładu poprzedniego zmodyfikujemy w taki sposób, by<br />
* jeśli liczba jest dodatnia - dodawał ją do sumy sigma;<br />
* jeśli liczba jest ujemna - nie robił nic, pomijał bieżącą<br />
pętlę przy pomocy rozkazu continue;<br />
(Ponieważ warunek wejściowy pętli jest zawsze spełniony, to<br />
pętlę zawsze uda się kontynuować.)<br />
* jeśli liczba równa się zero - przerywał pętlę instrukcją break<br />
Przykład:<br />
float a, sigma=0;<br />
void main()<br />
{<br />
for (;;)<br />
{<br />
printf("\n Sumuje tylko liczby dodatnie\n");<br />
scanf("%f", &a);<br />
if (a
przypadków - wariantów (case). Każdy wariant jest oznaczony przy<br />
pomocy stałej - tzw. ETYKIETY WYBORU. Wyrażenie przełączające<br />
może przyjmować wartości typu int. Ogólna postać istrukcji jest<br />
następująca:<br />
switch (selector)<br />
{<br />
case STAŁA1: Ciąg_instrukcji-wariant 1;<br />
case STAŁA2: Ciąg_instrukcji-wariant 2;<br />
...............................<br />
case STAŁAn: Ciąg_instrukcji-wariant n;<br />
default : Ostatni_ciąg_instrukcji;<br />
}<br />
Należy podkreślić, że po dokonaniu wyboru i skoku do etykiety<br />
wykonane zostaną również WSZYSTKIE INSTRUKCJE PONIŻEJ DANEJ<br />
ETYKIETY. Jeśli chcemy tego uniknąć, musimy dodać rozkaz break.<br />
[P033.CPP]<br />
# define pisz printf //dla przypomnienia<br />
# include <br />
void main()<br />
{<br />
int Numer_Dnia;<br />
pisz("\nPodaj numer dnia tygodnia\n");<br />
scanf("%d", &Numer_Dnia);<br />
switch(Numer_Dnia)<br />
{<br />
case 1: pisz("PONIEDZIALEK.");<br />
case 2: pisz("WTOREK");<br />
case 3: pisz("SRODA.");<br />
case 4: pisz("CZWARTEK.");<br />
case 5: pisz("PIATEK.");<br />
case 6: pisz("SOBOTA.");<br />
case 7: pisz("NIEDZIELA.");<br />
default: pisz("\n *********************");<br />
}<br />
}<br />
Zwróć uwagę, że w przykładzie wariant default zostanie wykonany<br />
ZAWSZE, nawet jeśli podasz liczbę większą niż 7.<br />
[P034.CPP]<br />
# define pisz printf<br />
# include <br />
void main()<br />
{<br />
int Numer_Dnia;<br />
pisz("\nPodaj numer dnia tygodnia\n");<br />
scanf("%d", &Numer_Dnia);<br />
switch(Numer_Dnia)<br />
- 133-
{<br />
case 1: pisz("PON."); break;<br />
case 2: pisz("WTOR"); break;<br />
case 3: pisz("SRO."); break;<br />
case 4: pisz("CZW."); break;<br />
case 5: pisz("PIO."); break;<br />
case 6: pisz("SOB."); break;<br />
case 7: pisz("NIEDZ."); break;<br />
default: pisz("\n ?????");<br />
}<br />
}<br />
Instrukcja break przerywa wykonanie. Wariant default zostanie<br />
wykonany TYLKO w przypadku podania liczby większej niż 7.<br />
INSTRUKCJA POWROTU return.<br />
Służy do zakończenia wykonania zawierającej ją funkcji i może<br />
mieć postać:<br />
return;<br />
return stała;<br />
return Wyrażenie;<br />
return (wyrażenie);<br />
Przykład:<br />
Definiujemy funkcję _dodaj() zwracającą, poprzez instrukcję<br />
return wartość przekazanego jej w momencie wywołania argumentu<br />
powiększoną o 5.<br />
float _dodaj(float x)<br />
{<br />
x+=5;<br />
return x;<br />
}<br />
Funkcja _dodaj() zwraca wartość i nadaje tę wartość zmiennej<br />
wynik zadeklarowanej nazewnątrz funkcji i znanej w programie<br />
głównym. A oto program w całości.<br />
[P035.CPP]<br />
float funkcja_dodaj(float x)<br />
{<br />
x += 5;<br />
return x;<br />
}<br />
float dana = 1, wynik = 0;<br />
void main()<br />
{<br />
clrscr();<br />
wynik = funkcja_dodaj(dana);<br />
- 134-
printf("%f", wynik);<br />
}<br />
INSTRUKCJA SKOKU BEZWARUNKOWEGO goto I ETYKIETY.<br />
Składnia instrukcji skoku goto jest następująca:<br />
goto Identyfikator_etykiety;<br />
UWAGA: Po każdej etykiecie musi wystąpić CO NAJMNIEJ JEDNA<br />
INSTRUKCJA. Jeśli etykieta oznacza koniec programu, to musi po<br />
niej wystąpić instrukcja pusta. Instrukcja goto nie cieszy się<br />
powodzeniem ani dobrą sławą (niesłusznie!). Ostrożne i umiejętne<br />
jej stosowanie jeszcze nikomu nie zaszkodziło. Należy tu<br />
zaznaczyć, że etykieta nie wymaga deklaracji.<br />
Przykład:<br />
Program poniżej generuje dźwięki i "odlicza".<br />
[P036.CPP]<br />
#include <br />
#include <br />
void main()<br />
{<br />
int czestotliwosc=5000, n=10, milisekundy=990;<br />
printf("\n");<br />
start:<br />
{<br />
sound(czestotliwosc);<br />
delay(milisekundy);<br />
nosound();<br />
czestotliwosc/=1.2;<br />
printf("%d\b", --n);<br />
if (n) goto start; //petle strukturalne zrob sam(a)<br />
}<br />
koniec: ;<br />
} // Tu jest instrukcja pusta.<br />
[S!] DOS API function names - nazwy funkcji z interfejsu DOS<br />
________________________________________________________________<br />
sound - dźwięk;<br />
delay - opóźnienie, zwłoka;<br />
nosound - bez dźwięku (wyłącz dźwięk);<br />
________________________________________________________________<br />
[Z]<br />
________________________________________________________________<br />
1. Biorąc pod uwagę, że iloraz częstotliwości kolejnych dźwięków<br />
jest stały tzn. Fcis/Fc=Ffis/Ff=....=const oraz, że oktawa to<br />
- 135-
podwojenie częstotliwości, opracuj program i oblicz<br />
częstotliwości poszczególnych dźwięków.<br />
2. Spróbuj zastosować w programie przykładowym kolejno pętle<br />
for, while, do...while.<br />
3. Zastosuj we własnym programie doświadczalnym instrukcję<br />
switch.<br />
- 136-
LEKCJA 14. Jak tworzyć i stosować struktury.<br />
________________________________________________________________<br />
W trakcie tej lekcji poznasz pojęcia:<br />
* Klasy zmiennej.<br />
* Struktury.<br />
* Pola bitowego.<br />
* Unii.<br />
Dowiesz się także więcej o operacjach logicznych.<br />
________________________________________________________________<br />
CO TO JEST KLASA ZMIENNEJ?<br />
W języku C i <strong>C++</strong> programista ma większy wpływ na rozmieszczenie<br />
zmiennych w pamięci operacyjnej komputera i w rejestrach<br />
mikroprocesora. Może to mieć decydujący wpływ na dostępność<br />
danych z różnych miejsc programu i szybkość działania programu.<br />
Należy podkreślić, że TYP ZMIENNEJ (char, int, float itp.)<br />
decyduje o sposobie interpretacji przechowywanych w pamięci zer<br />
i jedynek, natomiast KLASA ZMIENNEJ decyduje o sposobie<br />
przechowywania zmiennej w pamięci. W <strong>C++</strong> występują cztery klasy<br />
zmiennych.<br />
ZMIENNE STATYCZNE - static.<br />
Otrzymują stałą lokalizację w pamięci w momencie uruchamiania<br />
programu. Zachowują swoją wartość przez cały czas realizacji<br />
programu, chyba, że świadomie zażądamy zmiany tego stanu - np.<br />
instrukcją przypisania.<br />
Przykład deklaracji: static float liczba;<br />
W większości kompilatorów <strong>C++</strong> zmienne statyczne, które nie<br />
zostały jawnie zainicjowane w programie, otrzymują po<br />
zadeklarowaniu wartość ZERO.<br />
ZMIENNE AUTOMATYCZNE - auto.<br />
Otrzymują przydział miejsca w pamięci dynamicznie - na stosie<br />
procesora, w momencie rozpoczęcia wykonania tego bloku programu,<br />
w którym zmienne te zostały zadeklarowane. Przydzielenie pamięci<br />
nie zwalnia nas z obowiązku zainicjowania zmiennej (wcześniej<br />
wartość zmiennej jest przypadkowa). Zmienne automatyczne<br />
"znikają" po zakończeniu wykonywania bloku. Pamięć im<br />
przydzielona zostaje zwolniona. Przykład: auto long suma;<br />
ZMIENNE REJESTROWE - register.<br />
Zmienne rejestrowe są także zmiennymi lokalnymi, widocznymi<br />
tylko wewnątrz tego bloku programu, w którym zostały<br />
zadeklarowane. <strong>C++</strong> może wykorzystać dwa rejestry mikroprocesora<br />
- DI i SI do przechowywania zmiennych. Jeśli zadeklarujemy w<br />
- 137-
programie więcej zmiennych jako zmienne rejestrowe - zostaną one<br />
umieszczone na stosie. Znaczne przyspieszenie działania programu<br />
powoduje wykorzystanie rejestru do przechowywania np. licznika<br />
pętli.<br />
Przykład:<br />
register int i;<br />
.....<br />
for (i=1; i
int wiek;<br />
char pokrewienstwo[10]<br />
};<br />
Jeśli określimy już typ struktury - czyli rodzaj, wielkość i<br />
przeznaczenie poszczególnych pól struktury, możemy dalej tworzyć<br />
- deklarować i inicjować konkretne struktury danego typu.<br />
Przykład. Deklaracja zmiennych - struktur tego samego typu.<br />
struct Ludzie Moi, Twoi, Jego, Jej, Szwagra;<br />
Deklarację struktur można połączyć.<br />
Przykład. Połączona deklaracja struktur.<br />
struct Ludzie<br />
{ char pokrewienstwo[10];<br />
char Imiona[30];<br />
int wiek;<br />
} Moi, Twoi, Szwagra;<br />
Struktury statyczne<br />
* mają stałe miejsce w pamięci w trakcie całego programu;<br />
* są "widoczne" i dostępne w całym programie.<br />
Zadeklarujemy teraz typ struktury i zainicjujemy dwie struktury.<br />
Przykład. Zainicjowanie dwu struktur statycznych.<br />
struct Ludzie<br />
{ char pokrewienstwo[10];<br />
char Imiona[30];<br />
int wiek;<br />
};<br />
struct Ludzie Moi, Szwagra;<br />
static struct Ludzie Moi = { "Stryjek", "Walenty", 87 };<br />
static struct Ludzie Szwagra = { "ciotka", "Ala", 21 };<br />
Zapis<br />
static struct Ludzie Szwagra;<br />
oznacza:<br />
statyczna struktura typu "Ludzie" pod nazwą "Szwagra".<br />
Do struktury w całości możemy odwoływać się za pomocą jej nazwy<br />
a do poszczególnych elementów struktury poprzez nazwę struktury<br />
i nazwę pola struktury - ROZDZIELONE KROPKĄ ".". Zademonstrujmy<br />
to na przykładzie. Zwróć uwagę na różne sposoby przekazywania<br />
danych pomiędzy strukturami:<br />
- 139-
C4.Wiek=Czlowiek2.Wiek; - przekazanie zawartości pojedynczego<br />
pola numerycznego;<br />
C4=Czlowiek3; - przekazanie zawartości całej struktury Czlowiek3<br />
do C4.<br />
Przykład. Program manipulujący prostą strukturą.<br />
[P037.CPP]<br />
int main()<br />
{<br />
struct Ludzie<br />
{<br />
char Imie[20];<br />
int Wiek;<br />
char Status[30];<br />
char Tel_Nr[10];<br />
};<br />
static struct Ludzie<br />
Czlowiek1={"Ala", 7, "Ta, co ma Asa","?"},<br />
Czlowiek2={"Patrycja", 13, "Corka", "8978987"},<br />
Czlowiek3={"Krzysztof", 27, "Kolega z przedszkola", "23478"};<br />
struct Ludzie C4, C5;<br />
C4=Czlowiek3;<br />
C4.Wiek=Czlowiek2.Wiek;<br />
C5=Czlowiek1;<br />
clrscr();<br />
printf("%s %d %s\n", C4.Imie, C4.Wiek, C4.Status);<br />
printf("%s %s",C5.Imie, C5.Status);<br />
return 0;<br />
}<br />
Tablice mogą być elementami struktur, ale i odwrotnie - ze<br />
struktur, jak z cegiełek można tworzyć konstrukcje o wyższym<br />
stopniu złożoności - struktury struktur i tablice struktur.<br />
Jeśli tablica składa się z liczb typu int, to deklarujemy ją:<br />
int TABLICA[10];<br />
jeśli tablica składa się ze struktur, to deklarujemy ją:<br />
struct TABLICA[50];<br />
W przykładzie poniżej przedstawiono<br />
- 140-
* deklarację jednowymiarowej tablicy LISTA[50],<br />
* elementami tablicy są struktury typu SCzlowiek,<br />
* jednym z elementów każdej struktury SCzlowiek jest struktura<br />
"niższego rzędu" typu Adres;<br />
[P038.CPP]<br />
int main()<br />
{<br />
struct Adres<br />
{<br />
char Ulica[30];<br />
int Nr_Domu;<br />
int Nr_Mieszk;<br />
};<br />
struct SCzlowiek<br />
{<br />
char Imie[20];<br />
int Wiek;<br />
struct Adres Mieszkanie;<br />
};<br />
struct SCzlowiek LISTA[50];<br />
LISTA[1].Wiek=34;<br />
LISTA[1].Mieszkanie.Nr_Domu=29;<br />
printf("%d", LISTA[1].Mieszkanie.Nr_Domu);<br />
return 0;<br />
}<br />
Zapis<br />
printf("%d", LISTA[1].Mieszkanie.Nr_Domu<br />
oznacza:<br />
* wybierz element nr 1 z tablicy LISTA;<br />
(jak wynika z deklaracji tablicy, każdy jej element będzie miał<br />
wewnętrzną strukturę zorganizowaną tak, jak opisano w deklaracji<br />
struktury SCzlowiek);<br />
* wybierz ze struktury typu SCzlowiek pole Mieszkanie;<br />
(jak wynika z deklaracji, pole Mieszkanie będzie miało<br />
wewnętrzną organizację zgodną ze strukturą Adres);<br />
* ze struktury typu Adres wybierz pole Nr_Domu;<br />
* Wydrukuj zawartość pola pamięci interpretując ją jako liczbę<br />
typu int - w formacie %d.<br />
Słowo struktura tak doskonale pasuje, że chciałoby się<br />
powiedzieć:<br />
jeśli struktura struktur jest wielopoziomowa, to podobnie, jak<br />
przy wielowymiarowych tablicach, każdy poziom przy nadawaniu<br />
wartości musi zostać ujęty w dodatkową parę nawiasów klamrowych.<br />
- 141-
[???] A CO Z ŁAŃCUCHAMI ZNAKOWYMI ?<br />
________________________________________________________________<br />
Język <strong>C++</strong> oferuje do kopiowania łańcuchów znakowych specjalną<br />
funkcję strcpy(). Nazwa funkcji to skrót STRing CoPY (kopiuj<br />
łańcuch). Sposób wykorzystania tej funkcji:<br />
strcpy(Dokąd, Skąd); lub<br />
strcpy(Dokąd, "łańcuch znaków we własnej osobie");<br />
Szczegóły - patrz Lekcja o łańcuchach znakowych.<br />
________________________________________________________________<br />
STRUKTURY I WSKAŹNIKI.<br />
Wskaźniki mogą wskazywać strukturę w całości lub element<br />
struktury. Język C/<strong>C++</strong> oferuje specjalny operator -> który<br />
pozwala na odwoływanie się do elementów struktury. W przykładzie<br />
poniżej przedstawiono różne sposoby odwołania się do elementów<br />
trzech identycznych struktur STA, STB, STC.<br />
[P039.CPP]<br />
int main()<br />
{<br />
struct<br />
{<br />
char Tekst[20];<br />
int Liczba1;<br />
float Liczba2;<br />
} STA, STB, STC, *Pointer;<br />
STA.Liczba1 = 1;<br />
STA.Liczba2 = 2.2;<br />
strcpy(STA.Tekst, "To jest tekst");<br />
STB=STA;<br />
Pointer = &STC;<br />
Pointer->Liczba1 = 1;<br />
Pointer->Liczba2 = 2.2;<br />
strcpy(Pointer->Tekst, STA.Tekst);<br />
printf("\nLiczba1-STA Liczba2-STB Tekst-STC\n\n");<br />
printf("%d\t", STA.Liczba1);<br />
printf("%f\t", STB.Liczba2);<br />
printf("%s", Pointer->Tekst);<br />
return 0;<br />
}<br />
- 142-
Rozszyfrujmy zapis:<br />
strcpy(Pointer->Tekst, STA.Tekst);<br />
Skopiuj łańcuch znaków z pola Tekst struktury STA do pola Tekst<br />
struktury wskazywanej przez pointer. Prawda, że to całkiem<br />
proste?<br />
[???] CZY MUSIMY TO ROZDZIELAĆ ?<br />
________________________________________________________________<br />
Jak zauważyłeś, liczby moglibyśmy zapisywać także jako łańcuchy<br />
znaków, ale wtedy nie moglibyśmy wykonywać na tych liczbach<br />
działań. Konwersję liczba - łańcuch znaków lub odwrotnie łańcuch<br />
znaków - liczba wykonują w C specjalne funkcje np.:<br />
atoi() - Ascii TO Int.;<br />
itoa() - Int TO Ascii itp.<br />
Więcej informacji na ten temat i przykłady znajdziesz w dalszej<br />
części książki.<br />
________________________________________________________________<br />
Elementami struktury mogą być zmienne dowolnego typu, łądznie z<br />
innymi strukturami.<br />
Ciekawostka:<br />
________________________________________________________________<br />
Wskaźnik do deklarowanej struktury może być w języku C/<strong>C++</strong> jak<br />
jeden z jej WŁASNYCH elementów. Jeśli wskaźnik wchodzący w skład<br />
struktury wskazuje na WŁASNĄ strukturę, to nazywa się to<br />
AUTOREFERENCJĄ STRUKTURY.<br />
________________________________________________________________<br />
POLA BITOWE.<br />
Często zdarza się, że jakaś zmienna ma zawężony zakres wartości.<br />
Dla przykładu zmienne logiczne (tzw. flagi) to zawsze tylko 0<br />
lub 1. Wiek rzadko przekracza 255 lat a liczba dzieci zwykle nie<br />
jest większa niż 15. Nawet najbardziej niestali panowie nie<br />
zdążą ożenić się i rozwieść więcej niż 7 razy. Gdybyśmy zatem<br />
chcieli zapisać informacje<br />
* płeć 0 - mężczyzna, 1 - kobieta ( 1 bit );<br />
* wiek 0 - 255 lat (8 bitów);<br />
* ilość dzieci 0 - 15 (4 bity);<br />
* kolejny numer małżeństwa 0 - 7 (3 bity);<br />
to przecież wszystkie te informacje mogą nam się zmieścić w<br />
jednym szesnastobitowym rejestrze lub w dwu bajtach pamięci.<br />
Takie kilka bitów wydzielone i mające określone znaczenie to<br />
właśnie pole bitowe. <strong>C++</strong> pozwala także na uwzględnianie znaku w<br />
polach bitowych. Pola bitowe mogą być typu int i unsigned int<br />
- 143-
(czyli takie jak w przykładzie poniżej). Jeśli jakieś dane<br />
chcemy przechowywać w postaci pola bitowego, w deklaracji<br />
struktury sygnalizujemy to dwukropkiem. Stwarza to dwie istotne<br />
możliwości:<br />
* bardziej ekonomicznego wykorzystania pamięci;<br />
* łatwego dodatkowego zaszyfrowania danych.<br />
[P040.CPP]<br />
//Pamietaj o dolaczeniu plikow naglowkowych !<br />
int main()<br />
{<br />
struct USC {<br />
int Sex : 1;<br />
unsigned Wiek : 8;<br />
unsigned Dzieci : 4;<br />
unsigned Ktora : 3; } Facet;<br />
int bufor;<br />
clrscr();<br />
Facet.Sex = 0;<br />
printf("\n Ile ma lat ? : ");<br />
scanf("%d", &bufor); Facet.Wiek = bufor;<br />
printf("\n Ktore malzenstwo ? : ");<br />
scanf("%d", &bufor); Facet.Ktora = bufor;<br />
printf("\n Ile dzieci ? : ");<br />
scanf("%d", &bufor); Facet.Dzieci = bufor;<br />
printf("\n\n");<br />
if (Facet.Ktora) printf("Facet ma %d zone", Facet.Ktora);<br />
printf("\nPlec: Dzieci: Wiek (lat): \n\n");<br />
printf("%d\t%d\t%d", Facet.Sex, Facet.Dzieci, Facet.Wiek);<br />
getch();<br />
return 0;<br />
}<br />
Uruchom program i sprawdź co się stanie, jeśli Facet będzie miał<br />
np. 257 lat lub 123 żonę. Przekroczenie zadeklarowanego zakresu<br />
powoduje obcięcie części bitów.<br />
Aby uzyskać "wyrównanie" pola bitowego do początku słowa należy<br />
przed interesującym naspolem bitowym zdefiniować tzw. pole<br />
puste:<br />
* pole bitowe bez nazwy;<br />
* długość pola pustego powinna wynosić 0.<br />
Poniżej przedstawiam przykład pola bitowego zajmującego trzy<br />
kolejne słowa 16 bitowe. Dodanie pola pustego wymusza<br />
rozpoczęcie pola pole_IV od początku trzeciego słowa maszynowego<br />
(zakładamy, że pracujemy z komputerem 16 bitowym).<br />
- 144-
struct<br />
{<br />
unsigned pole_I:4;<br />
unsigned pole_II:10;<br />
unsigned pole_III:4;<br />
unsigned :0; /* to jest pole puste */<br />
unsigned pole_IV:5;<br />
} pole_przykladowe;<br />
Zwróć uwagę, że część bitów w drugim i trzecim słowie maszynowym<br />
nie zostanie wykorzystana.<br />
UNIE czyli ZMIENNE WARIANTOWE.<br />
Unie to specyficzne struktury, w których pola pamięci<br />
przeznaczone na objekty różnego typu nakładają się. Jeśli jakaś<br />
zmienna może być reprezentowana na kilka sposobów (wariantów) to<br />
sensowne jest przydzielenie jej nie struktury a unii. W danej<br />
chwili pole pamięci należące do unii może zawierać TYLKO JEDEN<br />
WARIANT. W przykładzie - albo cyfrę (która znakowo jest widziana<br />
jako znak ASCII o kodzie 2,3,4 itd.) albo napis. Do<br />
zadeklarowania unii służy słowo kluczowe union.<br />
[P041.CPP]<br />
#include "string.h"<br />
#include "stdio.h"<br />
int BUFOR, i;<br />
int main()<br />
{<br />
union<br />
{<br />
int Cyfra;<br />
char Napis[20];<br />
} Unia;<br />
for (i=1; i
printf(" %d\t\t\t%s", Unia.Cyfra, Unia.Napis);<br />
}<br />
return 0;<br />
}<br />
Pętla w przykładzie nie ma znaczenia. Służy tylko dla Twojej<br />
wygody - dzięki niej nie musisz uruchamiać programu<br />
przykładowego wielokrotnie. Podobnie zmienne BUFOR oraz i mają<br />
znaczenie pomocnicze. Zwróć uwagę, że nieprawidłowa<br />
interpretacja zawartości pola unii może spowodować wadliwe<br />
działanie programu.<br />
[Z]<br />
________________________________________________________________<br />
1. W programie przykładowym zamień unię na strukturę. Porównaj<br />
działanie.<br />
2 Przydziel na Wiek w strukturze Facet o jeden bit mniej. Ile<br />
lat może teraz mieć Facet ?<br />
3. Zmodyfikuj program przykładowy tak, by napis o liczbie<br />
mężów/żon zależał od płci - pola Sex.<br />
4. Zamieniwszy unię na strukturę w programie, sprawdź, czy<br />
wpływa to na wielkość pliku *.EXE.<br />
________________________________________________________________<br />
OPERACJE LOGICZNE.<br />
Zaczniemy od operacji logicznych na pojedynczych bitach liczb<br />
całkowitych. W <strong>C++</strong> mamy do dyspozycji następujące operatory:<br />
~€€€€Zaprzeczenie (NOT) ~0=1; ~1=0;<br />
|€€€€Suma (OR) 0|0=0; 0|1=1; 1|0=1; 1|1=1;<br />
&€€€€Iloczyn (AND) 0&0=0; 0&1=0; 1&0=0; 1&1=1;<br />
^€€€€Alternatywa wyłączna ALBO...ALBO (XOR)<br />
€€€€€0^0=0; 0^1=1; 1^0=1; 1^1=0;<br />
>2=2<br />
Miło byłoby pooglądać to trochę dokładniej w przykładowych<br />
programach, ale potrzebne nam do tego będą funkcje. Zajmijmy się<br />
więc uważniej funkcjami.<br />
- 146-
LEKCJA 15. Jak posługiwać się funkcjami.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się więcej o:<br />
* funkcjach i prototypach funkcji;<br />
* przekazywaniu argumentów funkcji;<br />
* współpracy funkcji ze wskaźnikami.<br />
_______________________________________________________________<br />
Aby przedstawić działanie operatorów logicznych opracujemy<br />
własną funkcję Demo() i zastosujemy ją w programie przykładowym<br />
[najważniejszy fragment].<br />
int Demo(int Liczba)<br />
{<br />
int MaxNr=15;<br />
for (; MaxNr>=0; MaxNr--)<br />
{<br />
if ((Liczba>>MaxNr)&1)<br />
printf("1");<br />
else<br />
printf("0");<br />
}<br />
return 0; //Funkcja nie musi nic zwracac<br />
}<br />
Funkcja przesuwa liczbę o kolejno 15, 14, 13 itd. bitów w prawo<br />
i sprawdza, czy 16, 15, 14 bit jest jedynką, czy zerem. Iloczyn<br />
logiczny z jedynką ( 0000000000000001 ) gwarantuje nam, że wpływ<br />
na wynik operacji będzie miał tylko ten jeden bit (patrz wyżej -<br />
jak działają operatory logiczne).<br />
[P042.CPP]<br />
# include <br />
int Demo(int Liczba)<br />
{<br />
int MaxNr=15;<br />
for (; MaxNr>=0; MaxNr--)<br />
if ((Liczba>>MaxNr)&1) printf("1");<br />
else printf("0");<br />
return 0;<br />
}<br />
char odp;<br />
int main()<br />
{<br />
int X, Y;<br />
clrscr();<br />
printf("\nPodaj dwie liczby calkowite od -32768 do +32767\n");<br />
printf("\nLiczby X i Y rozdziel spacja");<br />
printf("\nPo podaniu drugiej liczby nacisnij [Enter]");<br />
printf("\nLiczby ujemne sa w kodzie dopelniajacym");<br />
printf("\nskrajny lewy bit oznacza znak 0-Plus, 1-Minus");<br />
- 147-
for(;;)<br />
{<br />
printf("\n");<br />
scanf("%d %d", &X, &Y);<br />
printf("\nX:\t"); Demo(X);<br />
printf("\nY:\t"); Demo(Y);<br />
printf("\n~Y:\t"); Demo(~Y);<br />
printf("\nX&Y:\t"); Demo(X&Y);<br />
printf("\nX|Y:\t"); Demo(X|Y);<br />
printf("\nX^Y:\t"); Demo(X^Y);<br />
printf("\nY:\t"); Demo(Y);<br />
printf("\nY>>1:\t"); Demo(Y>>1);<br />
printf("\nY
np:<br />
itoa() - Integer TO Ascii - zamiana liczby typu int na łańcuch<br />
znaków ASCII;<br />
ltoa() - Long int TO Ascii - zamiana long int -> ASCII;<br />
atoi() - zamiana Ascii -> int;<br />
atol() - zamiana Asdii -> long int .<br />
Wszystkie wymienione funkcje przekształcając liczby na łańcuchy<br />
znaków potrzebują trzech parametrów:<br />
p1 - liczby do przekształcenia;<br />
p2 - bufora, w którym będą przechowywać wynik - łańcuch ASCII;<br />
p3 - podstawy (szesnastkowa, dziesiętna itp.).<br />
Jeśli chcemy korzystać z tych funkcji, powinniśmy dołączyć plik<br />
nagłówkowy z ich prototypami - stdlib.h (STandarD LIBrary -<br />
standardowa biblioteka). A oto przykład.<br />
[P043.CPP]<br />
# include "stdio.h"<br />
# include "stdlib.h"<br />
main()<br />
{<br />
int i;<br />
char B10[10], B2[20], B16[10]; //BUFORY<br />
for (i=1; i
odzaju typu zmiennej "bardziej pojemnego" rodzaju przed<br />
wykonaniem operacji;<br />
Po drugie:<br />
my sami możemy zmusić <strong>C++</strong> do zmiany typu FORSUJĄC typ świadomie<br />
w programie.<br />
W przykładzie poniżej podając w nawiasach żądany typ zmiennej<br />
forsujemy zmianę typu int na typ float.<br />
[P044.CPP]<br />
# include "stdio.h"<br />
void main()<br />
{<br />
int a=7;<br />
printf("%f", (float) a);<br />
}<br />
Konwersja typów nazywana bywa także "rzutowaniem" typów (ang.<br />
type casting). A oto kilka przykładów "forsowania typów":<br />
int a = 2;<br />
float x = 17.1, y = 8.95, z;<br />
char c;<br />
c = (char)a + (char)x;<br />
c = (char)(a + (int)x);<br />
c = (char)(a + x);<br />
c = a + x;<br />
z = (float)((int)x * (int)y);<br />
z = (float)((int)x * (int)y);<br />
z = (float)((int)(x * y));<br />
z = x * y;<br />
c = char(a) + char(x);<br />
c = char(a + int(x));<br />
c = char(a + x);<br />
c = a + x;<br />
z = float(int(x) * int(y));<br />
z = float(int(x) * int(y));<br />
z = float(int(x * y));<br />
z = x * y;<br />
FUNKCJE BIBLIOTECZNE I WŁASNE W JĘZYKU C/<strong>C++</strong> .<br />
Pojęcie funkcji obejmuje w C/<strong>C++</strong> zarówno pascalowe procedury,<br />
jak i basicowe podprogramy. Funkcji zdefiniowanych w <strong>C++</strong> przez<br />
prducenta jest bardzo dużo. Dla przykładu, funkcje arytmetyczne,<br />
które możesz wykorzystać do obliczeń numerycznych to np.:<br />
abs() - wartość bezwzględna,<br />
- 150-
cos() - cosinus, sin() - sinus, tan() - tangens,<br />
asin(), atan(), acos(), - funkcje odwrotne ARCUS SINUS...<br />
funkcje hiperboliczne: sinh(), cosh(), tanh(),<br />
wykładnicze i logarytmiczne:<br />
exp() - e^x<br />
log() - logarytm naturalny,<br />
log10() - logarytm dziesiętny.<br />
Jeśli skorzystasz z systemu Help i zajrzysz do pliku math.h<br />
(Help | Index | math.h), znajdziesz tam jeszcze wiele<br />
przydatnych funkcji.<br />
Funkcja może, ale nie musi zwracać wartość do programu -<br />
dokładniej do funkcji wyższego poziomu, z której została<br />
wywołana. W ciele funkcji służy do tego instrukcja return.<br />
Użytkownik może w <strong>C++</strong> definiować własne funkcje. Funkcja może<br />
być bezparametrowa. Oto przykład bezparametrowej funkcji,<br />
zwracającej zawsze liczbę całkowitą trzynaście:<br />
int F_Trzynascie()<br />
{<br />
return 13;<br />
}<br />
Poprawne wywołanie naszej funkcji w programie głównym miałoby<br />
postać:<br />
int main()<br />
{<br />
......<br />
int X;<br />
........ // Funkcja typu int nie musi byc deklarowana.<br />
X = F_Trzynascie();<br />
......<br />
}<br />
Jeśli funkcja musi pobrać jakieś parametry od programu (funkcji<br />
wyższego poziomu, wywołującej)? Zwróć uwagę, że program główny w<br />
C/<strong>C++</strong> to też funkcja - main(). Przykład następny pokazuje<br />
definicję funkcji obliczającej piątą potęgę pobranego argumentu<br />
i wywołanie tej funkcji w programie głównym.<br />
Przykład:<br />
int F_XdoPiatej(int argument)<br />
{<br />
int robocza; //automatyczna wewnetrzna zmienna funkcji<br />
robocza = argument * argument;<br />
robocza = robocza * robocza * argument;<br />
return (robocza);<br />
}<br />
int main()<br />
- 151-
{<br />
int Podstawa, Wynik, a, b;<br />
... /* Funkcja nie jest deklarowana przed uzyciem */<br />
Wynik = F_XdoPiatej(Podstawa);<br />
.....<br />
a = F_XdoPiatej(b);<br />
.....<br />
return 0;<br />
}<br />
Zwróć uwagę, że definiując funkcję podajemy nazwę i typ<br />
ARGUMENTU FORMALNEGO funkcji - tu: argument. W momencie<br />
wywołania na jego miejsce podstawiany jest rzeczywisty bieżący<br />
argument funkcji.<br />
Aby zapewnić wysoką dokładność obliczeń wymienione wyżej funkcje<br />
biblioteczne sqrt(), sin() itp. "uprawiają" arytmetykę na<br />
długich liczbach typu double. Funkcję taką przed użyciem w swoim<br />
programie MUSISZ ZADEKLAROWAĆ. Przykład:<br />
[P045.CPP]<br />
main()<br />
{<br />
double a, b;<br />
double sqrt();<br />
// tu skasuj deklaracje funkcji sqrt()<br />
// a otrzymasz bledny wynik !<br />
clrscr();<br />
printf("Podaj liczbe\n");<br />
scanf("%lf", &a);<br />
b = sqrt(a);<br />
printf("\n %Lf", (long double) b);<br />
getch();<br />
return 0;<br />
}<br />
PROTOTYPY FUNKCJI, czyli jeszcze o deklaracjach funkcji.<br />
Prototyp funkcji to taka deklaracja, która:<br />
* została umieszczona na początku programu poza funkcją main(),<br />
* zawiera deklarację zarówno typu funkcji, jak i typów<br />
argumentów.<br />
Przykład prototypu (funkcja2.cpp):<br />
double FUNKCJA( double X, double Y);<br />
main()<br />
{<br />
double A=0, B=3.14;<br />
printf("Wynik działania funkcji: \n");<br />
printf("%lf", FUNKCJA(A,B));<br />
- 152-
eturn 0; }<br />
double FUNKCJA(double X, double Y)<br />
{<br />
return ((1+X)*Y);<br />
}<br />
Prototyp mógłby równie dobrze wyglądać tak:<br />
double FUNKCJA(double, double);<br />
nazwy parametrów formalnych nie są istotne i można je pominąć.<br />
Jeśli prototyp funkcji wygląda tak:<br />
int Funkcja(int, char*, &float)<br />
oznacza to, że parametrami funkcji są wskaźniki do zmiennych,<br />
bądź referencje do zmiennych. Przy rozszyfrowywaniu takiej<br />
"abrakadabry" warto wiedzieć, że<br />
char* oraz char *<br />
int& oraz int &<br />
ma w tym przypadku identyczne znaczenie.<br />
W <strong>C++</strong> wolno nie zwracać wartości funkcjom typu void. To dlatego<br />
właśnie często rozpoczynaliśmy programy od<br />
void main()<br />
Skutek praktyczny: Jeśli w ciele funkcji typu void występuje<br />
instrukcja return (nie musi wystąpić) to instrukcja ta nie może<br />
mieć argumentów.<br />
Oto przykład prototypu, definicji i wywołania funkcji typu void:<br />
[P046.CPP]<br />
#include <br />
#include <br />
void RYSUJPROSTOKAT( int Wys, int Szer, char Wzorek);<br />
void main()<br />
{<br />
clrscr();<br />
RYSUJPROSTOKAT(5, 20, '€'); // klocek ASCII 176 - [Alt]-[176]<br />
getch();<br />
RYSUJPROSTOKAT(15, 15, '€'); //[Alt]-[177]<br />
getch();<br />
}<br />
void RYSUJPROSTOKAT( int Wys, int Szer, char Wzorek)<br />
{<br />
- 153-
int i, j; // automatyczne zmienne wewnętrzne funkcji<br />
for(i=1; i
{<br />
int Zmienna;<br />
clrscr();<br />
Zmienna = 7;<br />
//Zmienna funkcji main, rzeczywisty argument<br />
FUNKCJA( Zmienna); //Wywolanie funkcji<br />
printf("%d", Zmienna); //Wydruk wyniku<br />
}<br />
void FUNKCJA( int Argument) //Definicja funkcji<br />
{<br />
Argument = 10 * Argument + Argument;<br />
}<br />
FUNKCJA() jest jak widać trywialna. będzie zamieniać np. 2 na<br />
22, 3 na 33 itp. tylko w tym celu, by łatwo było stwierdzić, czy<br />
funkcja zadziałała czy nie.<br />
Rozbudujmy program tak by prześledzić kolejne stadia.<br />
[P047.CPP]<br />
void FUNKCJA( int ); //Prototyp<br />
int Zmienna;<br />
void main()<br />
{<br />
clrscr();<br />
printf("Stadium: \tZmienna Argument");<br />
printf("\nStadium 1\t%d\tnie istnieje\n", Zmienna);<br />
Zmienna = 7;<br />
printf("Stadium 2\t%d\tnie istnieje\n", Zmienna );<br />
FUNKCJA( Zmienna);<br />
printf("Stadium 3\t%d", Zmienna);<br />
// printf("%d", Argument);<br />
// taka proba sie NIE UDA !<br />
getch();<br />
}<br />
void FUNKCJA( int Argument) //Definicja funkcji<br />
{<br />
printf("jestesmy wewnatrz funkcji\n");<br />
printf("Nastapilo kopiowanie Zmienna -> Argument\n" );<br />
printf("\t\t%d\t%d\n", Zmienna, Argument);<br />
getch();<br />
Argument = 10*Argument + Argument;<br />
printf("\t\t%d\t%d\n", Zmienna, Argument);<br />
getch();<br />
}<br />
Próba wydrukowania zmiennej Argument gdziekolwiek poza wnętrzem<br />
FUNKCJI() nie uda się i spowoduje komunikat o błędzie. Oznacza<br />
- 155-
to, że POZA FUNKCJĄ zmienna Argument NIE ISTNIEJE. Jest tworzona<br />
na stosie jako zmienna automatyczna na wyłączny użytek funkcji,<br />
w której została zadeklarowana i znika po wyjściu z funkcji.<br />
Przy takiej organizacji funkcji i programu funkcja otrzymuje<br />
kopię zmiennej, na niej wykonuje swoje działania, natomiast<br />
zmienna (zmienne) wewnętrzna funkcji znika po wyjściu z funkcji.<br />
Problem przekazania parametrów pomiędzy funkcjami wywołującymi<br />
("wyższego rzędu" - tu: main) i wywoływanymi (tu: FUNKCJA) można<br />
rozwiązać przy pomocy<br />
* instrukcji return (zwrot do programu jednej wartości) lub<br />
* wskaźników.<br />
Możemy przecież funkcji przekazać nie samą zmienną, a wskaźnik<br />
do zmiennej (robiliśmy to już w przypadku funkcji scanf() -<br />
dlatego, że samej zmiennej jeszcze nie było - miała zostać<br />
dopiero pobrana, ale istniało już przeznaczone na tą nową<br />
zmienną - zarezerwowane dla niej miejsce. Mogł zatem istnieć<br />
wskaźnik wskazujący to miejsce). wskaźnik należy oczywiście<br />
zadeklarować. Nasz program przybrałby zatem nową postać.<br />
Wskaźnik do zmiennej nazwiemy *Argument.<br />
[P048.CPP]<br />
//Pamietaj o plikach naglowkowych !<br />
void FUNKCJA( int *Argument); //Prototyp<br />
int Zmienna;<br />
void main()<br />
{<br />
clrscr();<br />
printf("Stadium: \tZmienna Argument");<br />
printf("\nStadium 1\t%d\tnie istnieje\n", Zmienna);<br />
Zmienna = 7;<br />
printf("Stadium 2\t%d\tnie istnieje\n", Zmienna );<br />
FUNKCJA( &Zmienna); //Pobierz do funkcji ADRES Zmiennej<br />
printf("Stadium 3\t%d", Zmienna);<br />
// printf("%d", Argument);<br />
// taka proba sie NIE UDA !<br />
getch();<br />
}<br />
void FUNKCJA( int *Argument) // Definicja funkcji<br />
{<br />
printf("jestesmy wewnatrz funkcji\n");<br />
printf("Nastapilo kopiowanie ADRESOW a nie zmiennej\n" );<br />
printf("ADRESY:\t\t %X\t%X\n", &Zmienna, Argument);<br />
getch();<br />
*Argument = 10* *Argument + *Argument; /* DZIALANIE */<br />
printf("\t\t%d\t%d\n", Zmienna, *Argument);<br />
getch();<br />
}<br />
- 156-
W linii /* DZIALANIE */ mnożymy i dodajemy to, co wskazuje<br />
wskaźnik, czyli Zmienną. Funkcja działa zatem nie na własnej<br />
kopii zmiennej a bezpośrednio na zmiennej zewnętrznej. Zwróć<br />
uwagę na analogię w sposobie wywołania funkcji:<br />
FUNKCJA( &Zmienna );<br />
scanf( "%d", &Zmienna );<br />
A jeśli argumentem funkcji ma być tablica? Rozważ przykładowy<br />
program. Program zawiera pewną nadmiarowość (ku większej<br />
jasności mechanizmów).<br />
[P049.CPP]<br />
# include <br />
# include <br />
SUMA( int k, int Tablica[] )<br />
{<br />
int i, SumTab=0;<br />
for (i=0; i10)<br />
{ printf("TO ZA DUZO ! - max. 10");<br />
continue;<br />
}<br />
suma = SUMA( N,TAB );<br />
printf("\nTO JEST suma z progr. glownego %d", suma);<br />
printf("\n Jeszcze raz ? T/N");<br />
Odp = getch();<br />
}<br />
while (Odp!='N' && Odp!='n');<br />
return 0;<br />
}<br />
Kompilacja w <strong>C++</strong> jest wieloprzebiegowa (PASS 1, PASS 2), więc<br />
- 157-
definicja funkcji może być zarówno na początku jak i na końcu.<br />
A oto następny przykład. Operując adresem - wskaźnikiem do<br />
obiektu (tu wskaźnikami do dwu tablic) funkcja Wypelniacz()<br />
zapisuje pod wskazany adres ciąg identycznych znaków. Na końcu<br />
każdego łańcucha znaków zostaje dodany NUL - (\0) jako znak<br />
końca. Taki format zapisu łańcuchów znakowych nazywa się ASCIIZ.<br />
[P050.CPP]<br />
void Wypelniacz(char *BUFOR, char Znak, int Dlugosc);<br />
char TAB2D[5][10]; // Tablica 5 X 10 = 50 elementow<br />
char TAB_1D[50]; // Tablica 1 X 50 = 50 elementow<br />
int k;<br />
main()<br />
{<br />
clrscr();<br />
Wypelniacz( TAB_1D, 'X', 41); //Wypelnia X-ami<br />
printf("%s\n\n", TAB_1D);<br />
for (k=0; k
[P051.CPP]<br />
int BALAGAN[10];<br />
int PORZADEK[10]; // Tablica koncowa - uporzadkowana<br />
int k, *pointer , MAX=10000 ;<br />
int *Minimum(int Ilosc, int *TABL);<br />
main()<br />
{<br />
clrscr();<br />
printf("Podaj 10 liczb calkowitych od -10000 do 10000\n");<br />
for (k=0; k
takie funkcje, które<br />
* pobierają jeden argument typu double float;<br />
* zwracają do programu wartość typu double float. "<br />
Dostępne są dla nas zatem wszystkie standardowe funkcje<br />
arytmetyczne z pliku MATH.H (MATH pochodzi od MATHematics -<br />
matematyka.)<br />
[P052.CPP]<br />
# include <br />
# include <br />
double NASZA( double ); //Deklaracja zwyklej funkcji<br />
double (*Funkcja)(double ARG); //pointer do funkcji<br />
double Liczba, Wynik; //Deklaracje zmiennych<br />
int WYBOR;<br />
main()<br />
{<br />
clrscr();<br />
printf("Podaj Liczbe \n");<br />
scanf("%lf", &Liczba);<br />
printf("CO MAM ZROBIC ?\n");<br />
printf("1 - Sinus \n");<br />
printf("2 - Pierwiastek\n");<br />
printf("3 - Odwrotnosc 1/x\n");<br />
scanf("%d", &WYBOR);<br />
switch(WYBOR)<br />
{<br />
case 1: Funkcja=sin; break;<br />
case 2: Funkcja=sqrt; break;<br />
case 3: Funkcja=NASZA; break;<br />
}<br />
Wynik=Funkcja(Liczba); // Wywolanie wybranej funkcji<br />
printf("\n\nWYNIK = %lf", Wynik);<br />
getch();<br />
return 0;<br />
}<br />
double NASZA(double a)<br />
{<br />
printf("\n A TO NASZA PRYWATNA FUNKCJA\n");<br />
if (a!=0) a=1/a; else printf("???\n");<br />
return a;<br />
}<br />
main() - FUNKCJA SPECJALNA.<br />
Ta książka siłą rzeczy, ze względu na swoją skromną objętość i<br />
skalę zagadnienia o którym traktuje (autor jest zdania, że język<br />
C to cała filozofia nowoczesnej informatyki "w pigułce") pełna<br />
jest skrótów. Nie możemy jednak pozostawić bez, krótkiego<br />
- 160-
choćby, opisu pomijanego dyskretnie do tej pory problemu<br />
PRZEKAZANIA PARAMETRÓW DO PROGRAMU.<br />
Konwencja funkcji w języku C/<strong>C++</strong> wyraźnie rozgranicza dwa różne<br />
punkty widzenia. Funkcja pozwala na swego rodzaju separację<br />
świata wewnętrznego (lokalnego, własnego) funkcji od świata<br />
zewnętrznego. Nie zdziwi Cię więc zapewne, że i sposób widzenia<br />
parametrów przekazywanych programowi przez DOS i sposób widzenia<br />
"od wewnątrz" argumentów pobierabych przez funkcję main() jest<br />
diametralnie różny.<br />
To, co DOS widzi tak:<br />
PROGRAM PAR1 PAR2 PAR3 PAR4 PAR5 [...][Enter]<br />
funkcja main() widzi tak:<br />
main(int argc, char **argv, char **env)<br />
lub tak:<br />
main(int argc, char *argv[], char *env[])<br />
[???]CO TO JEST ???<br />
________________________________________________________________<br />
Zapisane zgodnie z obyczajami stosowanymi w prototypach funkcji:<br />
int argc - liczba całkowita (>=1, bo parametr Nr 1 to nazwa<br />
samego programu, za pośrednictwem której DOS wywołuje funkcję<br />
main). Liczba argumentów - parametrów może być zmienna.<br />
UWAGA: Język programowania wsadowego BPL przyjmuje nazwę<br />
programu za parametr %0 a <strong>C++</strong> uznaje ją za parametr o numerze<br />
argv[0], tym niemniej, nawet jeśli nie ma żadnych parametrów<br />
argc = 1.<br />
argv - to tablica zawierająca wskaźniky do łańcuchów tekstowych<br />
reprezentowanych w kodzie ASCIIZ - nazw kolejnych paramentrów, z<br />
którymi został wywołany program.<br />
Pierszy element tej tablicy to nazwa programu. Ostatni element<br />
tej tablicy, o numerze argv - 1 to ostatni niezerowy parametr<br />
wywołania programu.<br />
env - to także tablica zawierająca wskaźniki do łańcuchów<br />
znakowych w kodzie ASCIIZ reprezentujących parametry środowiska<br />
(environment variables). Wskaźnik o wartości NUL sygnalizuje<br />
koniec tablicy. W Turbo <strong>C++</strong> istnieje także predefiniowana<br />
zmienna globalna (::), przy pomocy której można uzyskać dostęp<br />
do środowiska operacyjnego - environ .<br />
________________________________________________________________<br />
Przykłady poniżej przedstawiają sposób wykorzystania parametrów<br />
wejściowych programu.<br />
- 161-
[P053.CPP]<br />
# include "stdio.h"<br />
# include "stdlib.h"<br />
main(int argc, char *argv[], char *env[])<br />
{<br />
printf("Parametry srodowiska DOS: \n");<br />
int i = 0;<br />
do<br />
{<br />
printf("%s \n", env[i]);<br />
i++;<br />
};<br />
while (env[i] != NULL);<br />
printf("Lista parametrow programu: \n");<br />
for(i=1; i
3 Spróbuj przeprowadzić rzutowanie typu we własnym programie.<br />
4 Przekaż wartość w programie przykładowym posługując się<br />
instrukcją:<br />
return (10*Argument + Argument);<br />
5 Rozszerz zestaw funkcji do wyboru w programie przykładowym.<br />
________________________________________________________________<br />
- 163-
LEKCJA 16. ASEMBLER TASM i BASM.<br />
________________________________________________________________<br />
W trakcie tej lekcji:<br />
* dowiesz się , jak łączyć <strong>C++</strong> z assemblerem<br />
* poznasz wewnętrzne formaty danych<br />
________________________________________________________________<br />
WEWNĘTRZNY FORMAT DANYCH I WSPÓŁPRACA Z ASSEMBLEREM.<br />
W zależności od wybranej wersji kompilatora <strong>C++</strong> zasady<br />
współpracy z asemblerem mogą się trochę różnić. Generalnie,<br />
kompilatory współpracują z tzw. asemblerami in-line (np. BASM),<br />
lub asemblerami zewnętrznymi (stand alone assembler np. MASM,<br />
TASM). Wstawki w programie napisane w assemblerze powinny zostać<br />
poprzedzone słowem asm (BORLAND/Turbo <strong>C++</strong>), bądź _asm (Microsoft<br />
<strong>C++</strong>). Przy kompilacji należy zatem stosownie do wybranego<br />
kompilatora przestrzegać specyficznych zasad współpracy. Np. dla<br />
BORLAND/Turbo <strong>C++</strong> można stosować do kompilacji BCC.EXE/TCC.EXE<br />
przy zachowaniu warunku, że TASM.EXE jest dostępny na dysku w<br />
bieżącym katalogu.<br />
Typowymi sposobami wykorzystania assemblera z poziomu <strong>C++</strong> są:<br />
* umieszczenie ciągu instrukcji assemblera bezpośrednio w<br />
źródłowym tekście programu napisanym w języku C/<strong>C++</strong>,<br />
* dołączeniu do programu zewnętrznych modułów (np. funkcji)<br />
napisanych w assemblerze.<br />
W <strong>C++</strong> w tekście źródłowym programu blok napisany w asemblerze<br />
powinien zostać poprzedzony słowem kluczowym asm (lub _asm):<br />
# pragma inline<br />
void main()<br />
{<br />
asm mov dl, 81<br />
asm mov ah, 2<br />
asm int 33<br />
}<br />
Program będzie drukował na ekranie literę "Q" (ASCII 81).<br />
JAK POSŁUGIWAĆ SIĘ DANYMI W ASEMBLERZE.<br />
Napiszemy w asemblerze program drukujący na ekranie napis "tekst<br />
- test". Rozpczynamy od zadeklarowania łańcucha znaków:<br />
void main()<br />
{<br />
char *NAPIS = "tekst - test$"; /* $ - ozn. koniec */<br />
- 164-
Umieściliśmy w pamięci łańcuch, będący w istocie tablicą<br />
składającą się z elementów typu char. Wskaźnik do łańcucha może<br />
zostać zastąpiony nazwą-identyfikatorem tablicy. Zwróć uwagę, że<br />
po łańcuchu znakowym dodaliśmy znak '$'. Dzięki temu możemy<br />
skorzystać z DOS'owskiej funkcji nr 9 (string-printing DOS<br />
service 9). Możemy utworzyć kod w asemblerze:<br />
asm mov dx, NAPIS<br />
asm mov ah, 9<br />
asm int 33<br />
Cały program będzie wyglądał tak:<br />
[P054.CPP]<br />
# pragma inline<br />
void main()<br />
{<br />
char *NAPIS = "\n tekst - test $";<br />
asm {<br />
MOV DX, NAPIS<br />
MOV AH, 9<br />
INT 33<br />
}<br />
}<br />
Zmienna NAPIS jest pointerem i wskazuje adres w pamięci, od<br />
którego rozpoczyna się łańcuch znaków. Możemy przesłać zmienną<br />
NAPIS bezpośrednio do rejestru i przekazać wprost przerywaniu<br />
Int 33. Program assemblerowski (tu: TASM) mógłby wyglądać np.<br />
tak:<br />
[P055.ASM]<br />
.MODEL SMALL ;To zwylke robi TCC<br />
.STACK 100H ;TCC dodaje standardowo 4K<br />
.DATA<br />
NAPIS DB 'tekst - test','$'<br />
.CODE<br />
START:<br />
MOV AX, @DATA<br />
MOV DS, AX ;Ustawienie segmentu danych<br />
ASM:<br />
MOV DX, OFFSET NAPIS<br />
MOV AH, 9<br />
INT 21H ;Drukowanie<br />
KONIEC:<br />
MOV AH, 4CH<br />
INT 21H ;Zakończenie programu<br />
END START<br />
Inne typy danych możemy stosować podobnie. Wygodną taktyką jest<br />
deklarowanie danych w tej części programu, która została<br />
- 165-
napisana w <strong>C++</strong>, aby inne fragmenty programu mogły się do tych<br />
danych odwoływać. Możemy we wstawce asemblerowskiej odwoływać<br />
się do tych danych w taki sposób, jakgdyby zostały zadeklarowane<br />
przy użyciu dyrektyw DB, bądź DW.<br />
WEWNĘTRZNE FORMATY DANYCH W <strong>C++</strong>.<br />
LICZBY CAŁKOWITE typów char, short int i long int.<br />
Liczba całkowita typu short int stanowi 16-bitowe słowo i może<br />
zostać zastosowana np. w taki sposób:<br />
[P056.CPP]<br />
#pragma inline<br />
void main()<br />
{<br />
char *napis = "\nRazem warzyw: $";<br />
int marchewki = 2, pietruszki = 5;<br />
asm {<br />
MOV DX, napis<br />
MOV AH, 9<br />
INT 33<br />
MOV DX, marchewki<br />
ADD DX, pietruszki<br />
ADD DX, '0'<br />
MOV AH, 2<br />
INT 33<br />
}<br />
}<br />
Zdefiniowaliśmy dwie liczby całkowite i łańcuch znaków - napis.<br />
Ponieważ obie zmienne (łańcuch znków jest stałą) mają długość<br />
jednego słowa maszynowego, to efekt jest taki sam, jakgdyby<br />
zmienne zostały zadeklarowane przy pomocy dyrektywy asemblera DW<br />
(define word). Możemy pobrać wartość zmiennej marchewki do<br />
rejestru instrukcją<br />
MOV DX, marchewki<br />
;marchewki -> DX<br />
W rejestrze DX dokonujemy dodawania obu zmiennych i wyprowadzamy<br />
na ekran sumę, posługując się funkcją 2 przerywania DOS 33<br />
(21H).<br />
W wyniku działania tego programu otrzymamy na ekranie napis:<br />
Razem warzyw: 7<br />
Jeczsze jeden szczegół techniczny. Ponieważ stosowana funkcja<br />
DOS pracuje w trybie znakowym i wydrukuje nam znak o kodzie<br />
ASCII przechowywanym w rejestrze, potrzebna jest manipulacja:<br />
- 166-
ADD DX, '0' ;Dodaj kod ASCII "zera" do rejestru<br />
Możesz sam sprawdzić, że po przekroczeniu wartości 9 przez sumę<br />
wszystko się trochę skomplikuje (kod ASCII zera - 48). Z równym<br />
skutkiem możnaby zastosować rozkaz<br />
ADD DX, 48<br />
Jeśli prawidłowo dobierzemy format danych, fragment programu<br />
napisany w asemblerze może korzystać z danych dokładnie tak<br />
samo, jak każdy inny fragment programu napisany w C/<strong>C++</strong>. Możemy<br />
zastosować dane o jednobajtowej długości (jeśli drugi, pusty<br />
bajt nie jest nam potrzebny). Zwróć uwagę, że posługujemy się w<br />
tym przypadku tylko "połówką" rejestru DL (L - Low - młodszy).<br />
[P057.CPP]<br />
#pragma inline<br />
void main()<br />
{<br />
const char *napis = "\nRazem warzyw: $";<br />
char marchewki = 2, pietruszki = 5;<br />
asm {<br />
MOV DX, napis<br />
MOV AH, 9<br />
INT 33<br />
MOV DL, marchewki<br />
ADD DL, pietruszki<br />
ADD DL, '0'<br />
MOV AH, 2<br />
INT 33<br />
}<br />
}<br />
W tej wersji zadeklarowaliśmy zmienne marchewki i pietruszki<br />
jako zmienne typu char, co jest równoznaczne zadeklarowaniu ich<br />
przy pomocy dyrektywy DB.<br />
Zajmijmy się teraz maszynową reprezentacją liczb typu unsigned<br />
long int (długie całkowite bez znaku). Ze względu na specyfikę<br />
zapisu danych do pamięci przez mikroprocesory rodziny Intel<br />
80x86 długie liczby całkowite (podwójne słowo - double word) np.<br />
12345678(hex) są przechowywane w pamięci w odwróconym szyku.<br />
Zamieniony miejscami zostaje starszy bajt z młodszym jak również<br />
starsze słowo z młodszym słowem. Liczba 12345678(hex) zostanie<br />
zapisana w pamięci komputera IBM PC jako 78 56 34 12.<br />
Gdy inicjujemy w programie zmienną<br />
long int x = 2;<br />
zostaje ona umieszczona w pamięci tak: 02 00 00 00 (hex).<br />
Młodsze słowo (02 00) jest umieszczone jako pierwsze. To właśnie<br />
- 167-
słowo zawiera interesującą nas informację, możemy wczytać to<br />
słowo do rejestru rozkazem<br />
MOV DX, X<br />
Jeśli będzie nam potrzebna druga połówka zmiennej - starsze<br />
słowo (umieszczone w pamięci jako następne), możemy zastosować<br />
pointer (czyli podać adres następnego słowa pamięci).<br />
[P058.CPP]<br />
# pragma inline<br />
void main()<br />
{<br />
unsigned long marchewki = 2, pietruszki = 5;<br />
const char *napis = "\nRazem warzyw: $";<br />
asm<br />
{<br />
MOV DX, napis<br />
MOV AH, 9<br />
INT 33<br />
MOV DX, marchewki<br />
ADD DX, pietruszki<br />
ADD DX, '0'<br />
MOV AH, 2<br />
INT 33<br />
}<br />
}<br />
W przypadku liczb całkowitych ujemnych <strong>C++</strong> stosuje zapis w<br />
kodzie komplementarnym. Aby móc manipulować takimi danymi każdy<br />
szanujący się komputer powinien mieć możliwość stosowania liczb<br />
ujemnych.<br />
Najstarszy bit w słowie, bądź bajcie (pierwszy z lewej) może<br />
spełniać rolę bitu znakowego. O tym, czy liczba jest ze znakiem,<br />
czy też bez decyduje wyłącznie to, czy zwracamy uwagę na ten<br />
bit. W liczbach bez znaku, obojętnie, czy o długości słowa, czy<br />
bajtu, ten bit również jest (i był tam zawsze!), ale<br />
traktowaliśmy go, jako najstarszy bit nie przydając mu poza tym<br />
żadnego szczególnego znaczenia. Aby liczba stała się liczbą ze<br />
znakiem - to my musimy zacząć ją traktować jako liczbę ze<br />
znakiem, czyli zacząć zwracać uwagę na ten pierwszy bit.<br />
Pierwszy, najstarszy bit liczby ustawiony do stanu 1 będzie<br />
oznaczać, że liczba jest ujemna - jeśli zechcemy ją potraktować<br />
jako liczbę ze znakiem.<br />
Filozofia postępowania z liczbami ujemnymi opiera się na<br />
banalnym fakcie:<br />
(-1) + 1 = 0<br />
Twój PC "rozumuje" tak: -1 to taka liczba, która po dodaniu 1<br />
- 168-
stanie się 0. Czy można jednakże wyobrazić sobie np.<br />
jednobajtową liczbę dwójkową, która po dodaniu 1 da nam w<br />
rezultacie 0 ? Wydawałoby się, że w dowolnym przypadku wynik<br />
powinien być conajmniej równy 1.<br />
A jednak. Jeśli ograniczymy swoje rozważania do ośmiu bitów<br />
jednego bajtu, może wystąpić taka, absurdalna tylko z pozoru<br />
sytuacja. Jeśli np. dodamy 255 + 1 (dwójkowo 255 = 11111111):<br />
1111 1111 hex FF dec 255<br />
+ 1 + 1 + 1<br />
___________ _____ _____<br />
1 0000 0000 100 256<br />
otrzymamy 1 0000 0000 (hex 100). Dla Twojego PC oznacza to, że w<br />
ośmiobitowym rejestrze pozostanie 0000 0000 , czyli po prostu 0.<br />
Nastąpi natomiast przeniesienie (carry) do dziewiątego (nie<br />
zawsze istniejącego sprzętowo bitu).<br />
Wystąpienie przeniesienia powoduje ustawienie flagi CARRY w<br />
rejestrze FLAGS. Jeśli zignorujemy flagę i będziemy brać pod<br />
uwagę tylko te osiem bitów w rejestrze, okaże się, że<br />
otrzymaliśmy wynik 0000 0000. Krótko mówiąc FF = (-1), ponieważ<br />
FF + 1 = 0.<br />
Aby odwrócić wszystkie bity bajtu, bądź słowa możemy w<br />
asemblerze zastosować instrukcję NOT. Jeśli zawartość rejestru<br />
AX wynosiła np. 0000 1111 0101 0101 (hex 0F55), to instrukcja<br />
NOT AX zmieni ją na 1111 0000 1010 1010 (hex F0AA). Dokładnie<br />
tak samo działa operator bitowy ~_AX w C/<strong>C++</strong>. W zestawie<br />
rozkazów mikroprocesorów rodziny Intel 80x86 jest także<br />
instrukcja NEG, powodująca zamianę znaku liczby (dokonując<br />
konwersji liczby na kod komplementarny). Instrukcja NEG robi to<br />
samo, co NOT, ale po odwróceniu bitów dodaje jeszcze jedynkę.<br />
Jeśli rejestr BX zawierał 0000 0000 0000 0001 (hex 0001), to po<br />
operacji NEG AX zawartość rejestru wyniesie 1111 1111 1111 1111<br />
(hex FFFF).<br />
Zastosujmy praktycznie uzupełnienia dwójkowe przy współdziałaniu<br />
asemblera z <strong>C++</strong>:<br />
[P059.CPP]<br />
#pragma inline<br />
void main()<br />
{<br />
const char *napis = "\nRazem warzyw: $";<br />
int marchewki = -2, pietruszki = 5;<br />
asm {<br />
MOV DX, napis<br />
MOV AH, 9<br />
- 169-
}<br />
INT 33<br />
MOV DX, marchewki<br />
NEG DX<br />
ADD DX, pietruszki<br />
ADD DX, '0'<br />
MOV AH, 2<br />
INT 33<br />
}<br />
Dzięki zamianie (-2) na 2 przy pomocy instrukcji NEG DX<br />
otrzymamy wynik, jak poprzednio równy 7.<br />
Przypomnijmy prezentację działania operatorów bitowych <strong>C++</strong>.<br />
Wykorzystaj program przykładowy do przeglądu bitowej<br />
reprezentacji liczb typu int (ze znakiem i bez).<br />
[P060.CPP]<br />
/* Program prezentuje format liczb i operatory bitowe */<br />
# include "iostream.h"<br />
# pragma inline<br />
void demo(int liczba)<br />
{<br />
int n = 15;<br />
for (; n >= 0; n--)<br />
if ((liczba >> n) & 1)<br />
cout
cin >> x >> y;<br />
cout
void drukuj(void) //Definicja funkcji<br />
{<br />
asm MOV DX, TEKST<br />
asm MOV AH, 9<br />
asm INT 33<br />
}<br />
Funkcja może oczywiście nie tylko zgłosić się napisem, ale także<br />
zrobić dla nas coś pożytecznego. W kolejnym programie<br />
przykładowym czyścimy bufor klawiatury (flush), co czasami się<br />
przydaje, szczególnie na starcie programów.<br />
[P062.CPP]<br />
# pragma inline<br />
char *TEKST = "\nBufor klawiatury PUSTY. $";<br />
void czysc_bufor();<br />
void main()<br />
{<br />
czysc_bufor();<br />
}<br />
//Też prototyp funkcji<br />
//Czyszczenie bufora klawiatury<br />
void czysc_bufor(void)<br />
{<br />
START:<br />
asm MOV AH, 11<br />
asm INT 33<br />
asm OR AL, AL<br />
asm JZ KOMUNIKAT<br />
asm MOV AH, 7<br />
asm INT 33<br />
asm JMP START<br />
KOMUNIKAT:<br />
asm MOV DX, TEKST<br />
asm MOV AH, 9<br />
asm INT 33<br />
}<br />
//Definicja funkcji<br />
Póki nie wystąpi problem przekazania parametrów, napisanie dla<br />
<strong>C++</strong> funkcji w asemblerze jest banalnie proste. Zwróć uwagę, że<br />
zmienne wskazywane w programach przez pointer *TEKST zostały<br />
zadeklarowane poza funkcją main() - jako zmienne globalne.<br />
Dzięki temu nasze funkcje drukuj() i czysc_bufor() mają dostęp<br />
do tych zmiennych.<br />
Spróbujemy przekazać funkcji parametr. Nazwiemy naszą funkcję<br />
wyswietl() i będziemy ją wywoływać przekazując jej jako argument<br />
znak ASCII przeznaczony do wydrukowania na ekranie:<br />
wyswietl('A'); . Pojawia się zatem problem - gdzie program<br />
- 172-
"pozostawia" argumenty przeznaczone dla funkcji przed jej<br />
wywołaniem? W Tabeli poniżej przedstawiono w skrócie "konwencję<br />
wywoływania funkcji" (ang. Function Calling Convention) języka<br />
<strong>C++</strong>.<br />
Konwencje wywołania funkcji.<br />
________________________________________________________________<br />
Język Argumenty na stos Postać Typ wart. zwrac.<br />
________________________________________________________________<br />
BASIC Kolejno offset adresu Return n<br />
<strong>C++</strong> Odwrotnie wartości Return<br />
Pascal Kolejno wartości Return n<br />
________________________________________________________________<br />
Return n oznacza liczbę bajtów zajmowanych łącznie przez<br />
wszystkie odłożone na stos parametry.<br />
W <strong>C++</strong> parametry są odkładane na stos w odwróconej kolejności.<br />
Jeśli chcemy, by parametry zostały odłożone na stos kolejno,<br />
powinniśmy zadeklarować funkcję jako "funkcję z Pascalowskimi<br />
manierami" - np.:<br />
pascal void nazwa_funkcji(void);<br />
Dodatkowo, w <strong>C++</strong> argumenty są przekazywane poprzez swoją<br />
wartość, a nie przez wskazanie adresu parametru, jak ma to<br />
miejsce np. w BASICU. Istnieje tu kilka wyjątków przy<br />
przekazywaniu do funkcji struktur i tablic - bardziej<br />
szczegółowo zajmiemy się tym w dalszej części książki.<br />
Rozbudujemy nasz przykładowy program w taki sposób, by do<br />
funkcji były przekazywane dwa parametry - litery 'A' i 'B'<br />
przeznaczone do wydrukowania na ekranie przez funkcję:<br />
# pragma inline<br />
void wyswietl(char, char);<br />
void main()<br />
{<br />
wyswietl('A', 'B');<br />
}<br />
//Prototyp funkcji<br />
//Wywolanie funkcji<br />
void wyswietl(char x, char y)<br />
{<br />
....<br />
//Definicja (implementacja)<br />
Parametry zostaną odłożone na stos:<br />
PUSH 'B'<br />
PUSH 'A'<br />
Każdy parametr (mimo typu char) zajmie na stosie pełne słowo.<br />
<strong>C++</strong> nie potrafi niestety układać na stosie bajt po bajcie.<br />
- 173-
Funkcja wyswietl() musi uzyskać dostęp do przekazanych jej<br />
argumentówów. Odwołamy się do zmiennych <strong>C++</strong> w taki sposób, jak<br />
robiłaby to każda inna funkcja w <strong>C++</strong>:<br />
[P063.CPP]<br />
# pragma inline<br />
void wyswietl(char, char); //Prototyp funkcji<br />
void main()<br />
{<br />
_AH = 2; //BEEEEE !<br />
wyswietl('A', 'B'); //Wywolanie funkcji<br />
}<br />
void wyswietl(char x, char y) //Definicja (implementacja)<br />
{<br />
_DH = 0; // To <strong>C++</strong> nie TASM, to samo, co asm MOV DH, 0<br />
_DL = x; // asm MOV DL, x<br />
asm INT 33<br />
_DH = 0; // asm MOV DH, 0<br />
_DL = y; // asm MOV DL, y<br />
asm INT 33<br />
}<br />
Aby pokazać jak dalece BORLAND <strong>C++</strong> jest elastyczny wymieszaliśmy<br />
tu w jednaj funkcji instrukcje <strong>C++</strong> (wykorzystując pseudozmienne)<br />
i instrukcje assemblera. Może tylko przesadziliśmy trochę<br />
ustawiając rejestr AH - numer funkcji DOS dla przerywania int 33<br />
przed wywołaniem funkcji wyswietl() w programie głównym. To<br />
brzydka praktyka (ozn. //BEEEE), której autor nie zaleca.<br />
Jak widzisz, przekazanie parametrów jest proste.<br />
- 174-
LEKCJA 17. TROCHĘ SZCZEGÓLÓW TECHNICZNYCH.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się więcej o szczegółach działania<br />
komputera widzianych z poziomu assemblera.<br />
________________________________________________________________<br />
LICZBY ZMIENNOPRZECINKOWE TYPU float.<br />
To, że <strong>C++</strong> przy wywołaniu funkcji jest "przyzwyczajony" do<br />
odkładania argumentów na stos zawsze po dwa bajty może nam<br />
sprawić trochę kłopotów, gdy zechcemy zastosować argument typu<br />
float, double, bądź long - znacznie przekraczający długością<br />
dwubajtowe słowo maszynowe.<br />
# include
dziesiętnych. Przesunięcie przecinka powoduje, że 12345.67 =<br />
1.234567 * 10^4. Aby wróciła do swojej starej "zwykłej" postaci<br />
(jest to tzw. "rozwinięcie" liczby - ang. expand) należy<br />
przesunąć przecinek o jedno miejsce w prawo - otrzymamy znowu<br />
11.1 . W liczbach dziesiętnych pierwsza cyfra może być różna<br />
(tylko nie zero), a w dowolnej poddanej normalizacji<br />
zmiennoprzecinkowej liczbie dwójkowej pierwszą cyfrą jest zawsze<br />
1. Skoro w formacie liczb zmiennoprzecinkowych pierwsza jedynka<br />
jest przyjmowana "z definicji" (ang. implicit), więc można ją<br />
pominąć. Zostanie nam zatem zamiast 1.11 tylko 11 i ta<br />
przechowywana część liczby jest nazywana jej częścią znaczącą<br />
(ang. significant). To jeszcze nie wszystko - powinien tam być<br />
wykładnik potęgi. Wystarczy zapamiętać wykładnik, bo podstawa<br />
jest zawsze ta sama - 2. Niestety wykładniki są przechowywane<br />
nie w sposób naturalny, a po dodaniu do nich tzw. przesunięcia<br />
(ang. offset lub bias). Pozwala to uniknąć kłopotów z<br />
określaniem znaku wykładnika potęgi.<br />
Dla liczb typu float offset wykładnika wynosi +127 a dla liczb<br />
double float +1023. Wrócmy do naszej przykładowej liczby. Jeśli<br />
nasza liczba 3.5 = 11.1(B) ma być zapisana w postaci<br />
zmiennoprzecinkowej - float, zapisany w pamięci wykładnik potęgi<br />
wyniesie:<br />
1 + 127 = 128 = 80 (hex)<br />
A teraz znak liczby. Pierwszy bit każdej liczby<br />
zmiennoprzecinkowej określa znak liczby (ang. sign bit). Liczby<br />
zmiennoprzecinkowe nie są przechowywane w postaci dwójkowych<br />
uzupełnień. Jeśli pierwszy bit - bit znaku równy jest 1 - liczba<br />
jest ujemna. natomiast jeżeli 0, liczba jest dodatnia. Jest to<br />
jedyna różnica pomiędzy dodatnimi a ujemnymi liczbami<br />
zmiennoprzecinkowymi. Nasza liczba 3.5 = 11.1 zostanie<br />
zakodowana jako:<br />
znak liczby - 0<br />
wykładnik potęgi - 1000 0000<br />
cyfry znaczące liczby - 110000000....<br />
Ponieważ wiemy, że mamy do dyspozycji dla liczb float 4 bajty<br />
(możesz to sprawdzić sizeof(float x=3.5)), uzupełnijmy brakujące<br />
do 32 bity zerami:<br />
3.5 = 0100 0000 0110 0000 0000 0000 0000 0000 = 40 60 00 00<br />
zapis 40600000 to oczywiście szesnastkowa postać naszej liczby.<br />
Jeśli teraz weźmiemy pod uwagę, że nasz PC zamieni miejscami<br />
starsze słowo z młodszym 00 00 40 60 a następnie w obrębie<br />
każdego słowa dodatkowo starszy bit z młodszym, to zrozumiemy,<br />
dlaczego nasza liczba "siedziała" w pamięci w zaszyfrowanej<br />
- 176-
postaci 00 00 60 40.<br />
Rozpatrzmy szkielet programu wykorzystującego funkcję z "długim"<br />
argumentem. Aby zapanować nad zapisem liczby zmiennoprzecinkowej<br />
do pamięci naszego PC możemy na poziomie assemblera postąpić np.<br />
tak:<br />
# include > 16;<br />
x1starsze = (int) x;<br />
_DX = x1starsze;<br />
_BX = x2mlodsze;<br />
asm {<br />
...... //Tu funkcja już może działać<br />
Forsując konwersję typu na (int), spowodujemy, że młodsze słowo<br />
zostanie przypisane zwyczajnej krótkiej zmiennej x2mlodsze.<br />
Następnie zawartość długiej zmiennej zostanie przesunięta o 16<br />
bitów w prawo (starsze słowo zostanie przesunięte na miejsce<br />
młodszego). Powtórzenie operacji przypisania spowoduje<br />
przypisanie zmiennej x1starsze starszej połówki słowa. Od tej<br />
chwili możemy odwołać się do tych zmiennych w naszym fragmencie<br />
napisanym w asemblerze. Postępujemy tak, by to <strong>C++</strong> martwił się o<br />
szczegóły techniczne i sam manipulował stosem i jednocześnie<br />
pilnował poprawności konwersji danych.<br />
ZWROT WARTOŚCI PRZEZ FUNKCJĘ.<br />
A teraz kilka słów o tym, co się dzieje, gdy funkcja zapragnie<br />
- 177-
zwrócić jakąś wartość do programu.<br />
Wykorzystanie przez funkcje rejestrów do zwrotu wartości.<br />
________________________________________________________________<br />
Typ wartości Funkcja używa rejestru (lub pary)<br />
________________________________________________________________<br />
signed char / unsigned char AL<br />
short<br />
AX<br />
int<br />
AX<br />
enum<br />
AX<br />
long<br />
para DX:AX (starsze słowo DX, młodsze<br />
AX)<br />
float AX = Adres (jeśli far to DX:AX)<br />
double AX = Adres (jeśli far to DX:AX)<br />
struct AX = Adres (jeśli far to DX:AX)<br />
near pointer AX<br />
far pointer DX:AX<br />
________________________________________________________________<br />
Zależnie od typu wartości zwracanej przez funkcję (określonej w<br />
prototypie funkcji), <strong>C++</strong> odczytuje zawartość odpowiedniego<br />
rejestru: AL, AX lub DX:AX. Jeśli funkcja ma np. zwrócić wartość<br />
o długości jednego bajtu, to przed wyjściem z funkcji należy ją<br />
"zostwić" w rejestrze AL. Jeśli wywołując funkcję <strong>C++</strong> oczekuje<br />
zwrotu wartości jednobajtowej, to po powrocie z funkcji<br />
automatycznie pobierze bajt z rejestru AL. Krótkie wartości<br />
(typu short int) są "pozostawiane" przez funkcję w AX, a długie<br />
w parze rejestrów: DX - starsze, AX - młodsze słowo.<br />
Zastosujmy to w programie. Funkcja będzie odejmować dwie liczby<br />
całkowite. Pobierze dwa argumenty typu int, wykona odejmowanie i<br />
zwróci wynik typu int (return (_AX)). Dla modelu pamięci small<br />
będzie to wyglądać tak:<br />
[P064.CPP]<br />
# include <br />
# pragma inline<br />
int funkcja(int, int);<br />
//Prototyp funkcji<br />
void main()<br />
{<br />
cout
}<br />
return (_AX);<br />
}<br />
//Zwróć zawartość rejestru AX<br />
Zwróć uwagę, że po return(_AX); stawiamy średnik, natomiast po<br />
instrukcjach assemblera nie:<br />
asm MOV AX, DX<br />
chyba, że chcemy umieścić kilka instrukcji assemblera w jednej<br />
linii (patrz niżej).<br />
<strong>C++</strong> i assembler są równoprawnymi partnerami. <strong>C++</strong> może odwoływać<br />
się do zmiennych i funkcji assemblera, jeśli zostały<br />
zadeklarowane, jako publiczne (public) oraz zewnętrzne<br />
(EXTeRNal) i vice versa. <strong>C++</strong> oczekuje, że zewnętrzne<br />
identyfikatory będą się rozpoczynać od znaku podkreślenia "_".<br />
Jeśli w programie pisanym w BORLAND <strong>C++</strong> zastosujemy zewnętrzne<br />
zmienne i funkcje, <strong>C++</strong> sam automatycznie doda do identyfikatorów<br />
znak podkreślenia. Turbo Assembler nie robi tego automatycznie i<br />
musimy zadbać o to "ręcznie". Przykładowo, współpraca pomiędzy<br />
programem P .CPP i modułem MODUL.ASM będzie przebiegać<br />
poprawnie:<br />
[P065.CPP]<br />
extern int UstawFlage(void);<br />
int Flaga;<br />
void main()<br />
{<br />
UstawFlage();<br />
}<br />
//Prototyp funkcji<br />
[MODUL.ASM]<br />
.MODEL SMALL<br />
.DATA<br />
EXTRN _Flaga:WORD<br />
.CODE<br />
PUBLIC _UstawFlage<br />
_UstawFlage PROC<br />
CMP [_Flaga], 0<br />
JNZ SKASUJ_Flage<br />
MOV [_Flaga], 1<br />
JMP SHORT KONIEC<br />
SKASUJ_Flage: MOV [_Flaga], 0<br />
KONIEC:<br />
RET<br />
_UstawFlage ENDP<br />
END<br />
Kompilacja może przebiegać oddzielnie wg schematu:<br />
- 179-
PROGRAM.CPP --> PROGRAM.OBJ<br />
MODUL.ASM --> MODUL.OBJ<br />
TLINK PROGRAM.OBJ MODUL.OBJ --> PROGRAM.EXE<br />
Lub możemy powierzyć tę pracę kompilatorowi, który sam wywoła<br />
TASM i TLINK:<br />
TCC PROGRAM.CPP MODUL.ASM<br />
W BORLAND <strong>C++</strong> 3.1 mamy do dyspozycji zintegrowany assembler<br />
(ang. in-line) - BASM. Ma on jednak w stosunku do<br />
"wolnostojącego" Turbo Assemblera pewne ograniczenia:<br />
* ma zawężony w stosunku do TASM zestaw dyrektyw (tylko DB, DD,<br />
DW, EXTRN);<br />
* nie pozwala na stosowanie składni typowej dla trybu "Ideal<br />
mode";<br />
* nie pozwala na zastosowanie makra;<br />
* nie pozwala stosować instrukcji charakterystycznych dla 386<br />
ani 486.<br />
Możesz stosować kilka rozkazów assemblera w jednej linii, ale<br />
powinieneś rozdzielać je wewnątrz linii średnikami:<br />
asm {<br />
POP AX; POP DX; POP DS<br />
IRET<br />
}<br />
Komentarz we wstawce assemblerowskiej musi zostać poprzedzony<br />
typowym dla C - /* (sam średnik, jak w TASM jest<br />
niedopuszczalny):<br />
asm {<br />
MOV DX, 1 ;TAK NIE MOŻNA W BASM !<br />
...<br />
asm {<br />
ADD AX, BX; /* Taki komentarz może być */<br />
[???] KŁOPOTY Z REJESTRAMI ?<br />
________________________________________________________________<br />
Jeśli zastosujesz rejestry DI i SI we wstawce assemblerowaj,<br />
kompilator <strong>C++</strong> nie będzie miał gdzie umieścić zmiennych klasy<br />
register z programu głónego. Zastanów się - co się bardziej<br />
opłaca.<br />
________________________________________________________________<br />
O WEKTORACH PRZERYWAŃ DOS<br />
Mikroprocesory Intel 80X86 rezerwują w pamięci naszych PC<br />
początkowe 1024 Bajty (adresy fizyczne 00000...00400 hex) na 256<br />
wektorów przerywań (każdy wektor składa się z dwu słów i może<br />
być traktowany jako DW, bądź far pointer). Następne 256 bajtów<br />
(00400...00500 hex) zajmuje BIOS, a kolejne 256 (00500...00600<br />
- 180-
hex) wykorzystuje DOS i Basic.<br />
Wektor to w samej rzeczy pełny adres początku procedury<br />
obsługującej przerywanie o danym numerze<br />
UWAGA:<br />
Wektor zapisywany jest w pamięci w odwrotnej kolejności:<br />
Adres pamięci: 0000:0000 [OFFSET Wekt. int 0]<br />
0000:0002 [SEGMENT int 0]<br />
0000:0004 [OFFSET Wekt. int 1]<br />
0000:0006 [SEGMENT int 1]<br />
0000:0008 [OFFSET int 2]<br />
.... ....<br />
Procesory 80X86 zamieniają jeszcze dodatkowo starszy bajt z<br />
młodszym.<br />
Posługując się systemowym debuggerem DEBUG możesz łatwo<br />
przejrzeć tablicę wektorów przerywań własnego komputera. Jeśli<br />
wydasz rozkaz:<br />
C:\DOS\DEBUG<br />
-D 0:0<br />
zobaczysz zawartość pierwszych 32 wektorów int #0...int#31,<br />
czyli pierwsze 128 bajtów pamięci:<br />
-d 0:0<br />
0000:0000 FB 91 32 00 F4 06 70 00-78 F8 00 F0 F4 06 70 00<br />
0000:0010 F4 06 70 00 54 FF 00 F0-53 FF 00 F0 53 FF 00 F0<br />
0000:0020 A5 FE 00 F0 87 E9 00 F0-23 FF 00 F0 23 FF 00 F0<br />
0000:0030 23 FF 00 F0 CE 02 00 C8-57 EF 00 F0 F4 06 70 00<br />
0000:0040 D1 0C BD 1B 4D F8 00 F0-41 F8 00 F0 74 07 70 00<br />
0000:0050 39 E7 00 F0 4A 08 70 00-2E E8 00 F0 D2 EF 00 F0<br />
0000:0060 00 00 FF FF FB 07 70 00-5D 0C 00 CA 9F 01 BD 1B<br />
0000:0070 53 FF 00 F0 A0 7C 00 C0-22 05 00 00 2F 58 00 C0<br />
Po zdeszyfrowaniu okaże się, że pierwszy wektor (przerywanie 0)<br />
wskazuje na adres startowy: 0032:91FB (adres absolutny 0951B).<br />
Generalnie możliwe są cztery sytuacje. Wektor może wskazywać:<br />
* adres startowy procedur ROM-BIOS: blok F - Fxxx:xxxx,<br />
* adres funkcji DOS,<br />
* adres funkcji działającego właśnie debuggera (DEBUG przejmuje<br />
obsługę niektórych przerywań), lub innego programu rezydującego<br />
w pamięci - np. NC.EXE,<br />
* wektor może być pusty - 00 00:00 00 jeśli dane przerywanie nie<br />
jest obsługiwane.<br />
Jeśli zechcesz sprawdzić, jak obsługiwane jest dane przerywanie<br />
możesz znów zastosować debugger, wydając mu rozkaz<br />
zdezasamblowania zawartości pamięci począwszy od wskazanego<br />
adresu:<br />
- 181-
-u 32:91FB<br />
0032:91FB BE6B47 MOV SI,476B<br />
0032:91FE 2E CS:<br />
0032:91FF 8B1E7E47 MOV BX,[477E]<br />
0032:9203 2E CS:<br />
0032:9204 8E16D73D MOV SS,[3DD7]<br />
0032:9208 BCA007 MOV SP,07A0<br />
0032:920B ˙˙˙˙˙˙˙˙˙˙˙E80200 ˙˙˙˙˙˙˙˙˙˙CALL ˙˙˙˙˙˙˙˙˙˙9210<br />
0032:920E EBDA JMP 91EA<br />
0032:9210 ˙˙˙˙˙˙˙˙˙˙˙16 ˙˙˙˙˙˙˙˙˙˙˙PUSH ˙˙˙˙˙˙˙˙˙˙˙SS<br />
0032:9211 07 POP ES<br />
0032:9212 ˙˙˙˙˙˙˙˙˙˙˙16 ˙˙˙˙˙˙˙˙˙˙˙PUSH ˙˙˙˙˙˙˙˙˙˙˙SS<br />
0032:9213 1F POP DS<br />
0032:9214 C606940308 MOV BYTE PTR [0394],08<br />
0032:9219 C606920316 MOV BYTE PTR [0392],16<br />
Z poziomu assemblera do wektora i odpowiednio do funkcji<br />
obsługującej przerywanie możesz odwołać się instrukcją INT<br />
numer.<br />
Zmienna numer może tu przyjmować wartości od 00 do FF. Jeśli<br />
wydasz taki rozkaz, komputer zapisze na stos (żeby sobie nie<br />
zapomnieć) zawartość rejestrów CS - bież. segment rozkazu, IP -<br />
bieżący offset rozkazu i FLAGS. Następnie wykona daleki (far<br />
jump) skok do adresu wskazanego przez wektor.<br />
Jeśli jednak część przerywań jest "niewykorzystana", lub w Twoim<br />
programie trzeba je obsługiwać inaczej - niestandardowo ? W<br />
BORLAND <strong>C++</strong> masz do dyspozycji specjalny typ funkcji: interrupt.<br />
Aby Twoja funkcja mogła stać się "handlerem" przerywania, możesz<br />
zadeklarować ją tak:<br />
void interrupt MojaFunkcja(bp, di, si, ds .....)<br />
Do funkcji klasy interrupt przekazywane są jako argumenty<br />
rejestry, nie musisz zatem stosować pseudozmiennych _AX, _FLAGS<br />
itp.. Jeśli zadeklarujesz funkcję jako handler przy pomocy słowa<br />
"interrupt", funkcja automatycznie zapamiętuje stan rejestrów:<br />
AX, BX, CX, DX, SI, DI, BP, ES i DS.<br />
Po powrocie z funkcji rejestry zostaną automatycznie odtworzone.<br />
Przykładem funkcji obsługującej przerywanie może być piszczek()<br />
posługujący się wbudowanym głośniczkiem i portem:<br />
# define us unsigned<br />
# include <br />
- 182-
# include <br />
void InstalujWektor(void interrupt (*adres)(), int numer_wekt);<br />
void interrupt Piszczek(us bp, us di, us si, us ds, us es,<br />
void main()<br />
{<br />
.....<br />
}<br />
....<br />
us ax, us bx, us cx, us dx);<br />
Po zadeklarowaniu prototypów dwu funkcji:<br />
Piszczek() - nasz handler przerywania;<br />
InstalujWektor() - funkcja instalująca nasz handler;<br />
możemy przystąpić do zdefiniowania oby funkcji. Posłużymy się<br />
zmiennymi<br />
nowe_bity, stare_bity. Wydawanie dźwięku polega na włączaniu i<br />
wyłączaniu głośniczka. Pusta pętla posłuży nam do zwłoki w<br />
czasie.<br />
void interrupt Piszczek(us bp, us di, us si, us ds, us es,<br />
us ax, us bx, us cx, us dx)<br />
{<br />
char nowe_bity, stare_bity, i;<br />
int n;<br />
unsigned char licznik = ax >> 8;<br />
stare_bity = inportb(0x61);<br />
for(nowe_bity = stare_bity, n = 0; n
cout
SI -> 0...31 ESI w tym (SI = 0..15)<br />
DI -> 0...31 EDI w tym (DI = 0..15)<br />
BP -> 0...31 EBP w tym (BP = 0..15)<br />
SP -> 0...31 ESP w tym (SP = 0..15)<br />
IP -> 0...31 EIP w tym (IP = 0..15)<br />
FLAGS -> 0...31 EFLAGS w tym (FLAGS = 0..15)<br />
Wszystkie "stare" połówki dostępne pod starą nazwą.<br />
Rejestry segmentowe pozostały 16 bitowe, ale jest ich o dwa<br />
więcej: CS, DS, ES, SS oraz nowe FS i GS.<br />
Nowe 32 bitowe rejestry działają według tych samych zasad:<br />
.386<br />
...<br />
MOV EAX, 1 ;zapisz 1 do rejestru EAX<br />
SUB EBX, EBX ;wyzeruj rejestr EBX<br />
ADD EBX, EAX ;dodaj (EAX)+(EBX) --> EBX<br />
Dostęp do starszej połowy rejestru można uzyskać np. poprzez<br />
przesuwanie (rotation):<br />
.386<br />
...<br />
MOV AX, Liczba_16_bitowa<br />
ROR EDX, 16<br />
MOV AX, DX<br />
ROR EDX, 16<br />
... itp.<br />
W assemblerze możesz stosować wobec procesora 386 nowe<br />
instrukcje (testowania nie istniejących wcześniej bitów,<br />
przenoszenia krótkich liczb do 32 bitowych rejestrów z<br />
uwzględnieniem zaku i uzupełnieniem zerami itp.):<br />
BSF, BSR, BTR, BTS, LFS, LGS, MOVZX, SETxx,<br />
BT, BTC, CDQ, CWDE, LSS, MOVSX, SHLD i SHRD.<br />
Przy pomocji instrukcji MOV w trybie uprzywilejowanym (tzw.<br />
most-privileged level 0 - tylko w trybie .386P) możesz dodatkowo<br />
uzyskać dostęp do specjalnych rejestrów mikroprocesora 80386.<br />
CR0, CR2, CR3,<br />
DR0, DR1, DR2, DR3, DR6, DR7<br />
TR6, TR7<br />
Występuje tu typ danych - FWORD - 48 bitów (6 bajtów). Obok<br />
znanych dyrektyw DB i DW pojawia się zatem nowa DF, a oprócz<br />
znajomych wskaźników BYTE PTR, WORD PTR pojawia się nowy FWORD<br />
PTR. Przy pomocy dyrektywy .387 możesz skorzystać z koprocesora.<br />
Jak wynika z zestawu dodatkowych insrukcji:<br />
FCOS, FSINCOS, FUCOMP, FPREM1, FUCOM, FUCOMPP, FSIN<br />
- 185-
warto dysponować koprocesorem, jeśli często korzystasz z<br />
grafiki, animacji i funkcji trygonometrycznych (kompilacji nie<br />
przyspieszy to niestety ani o 10% - tam odbywają się operacje<br />
stałoprzecinkowe).<br />
Zwróć uwagę, że procesory 386 i wcześniejsze wymagały instalacji<br />
dodatkowego układu 387 zawierającego koprocesor<br />
zmiennoprzecinkowy. Procesory 486 jeśli mają rozszerzenie DX -<br />
zawierają już koprocesor wewnątrz układu scalonego.<br />
________________________________________________________________<br />
- 186-
LEKCJA 18. O ŁAŃCUCHACH TEKSTOWYCH<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się,<br />
* jak manipulować łańcuchami tekstowymi i poznasz kilka<br />
specjalnych funkcji, które służą w <strong>C++</strong> właśnie do takich celów;<br />
* jak wykonują się operacje plikowo-dyskowe.<br />
________________________________________________________________<br />
OPERACJE NA ŁAŃCUCHACH TEKSTOWYCH.<br />
String, czyli łańcuch - to gupa znaków "pisarskich" (liter, cyfr<br />
i znaków specjalnych typu ?, !, _ itp.). Ponieważ <strong>C++</strong> nie ma<br />
odzielnego typu danych "string" - łańcuchy znaków to tablice<br />
złożone z pojedynczych znaków (typowe elementy typu char).<br />
Techniką obiektową można utworzyć klasę - nowy typ danych<br />
"string". W bibliotekach Microsoft <strong>C++</strong> istnieje predefiniowana<br />
klasa CString, ale zanim przejdziemy do programowania<br />
obiektowego i zdarzeniowego - rozważmy manipulowanie tekstami w<br />
sposób najprostszy.<br />
Maksymalną możliwą długość napisu należy podać wtedy, gdy w<br />
programie deklaruje się zmienną tekstową:<br />
char tekst1[40];<br />
Jest to poprawna deklaracja zmiennej tekstowej o nazwie<br />
(identyfikator) tekst1. Maksymalna długość tekstu, który można<br />
umieścić w tej zmiennej tekstowej to - 40 znaków (liter, cyfr,<br />
itp.). A jeśli chcę zastosować tylko pojedynczy znak zamiast<br />
całego napisu? To proste:<br />
char napis[1];<br />
Skoro długość łańcucha wynosi 1, to przecież nie jest żaden<br />
łańcuch! Informacja o długości (size - wielkość) wpisywana w<br />
nawiasy jest zbędna. Uproszczona wersja utworzenia zmiennej<br />
jednoznakowej i nadania zmiennej nazwy wygląda w tak:<br />
char znak;<br />
Nie jest to już jednak deklaracja zmiennej łańcuchowej - lecz<br />
deklaracja zmiennej znakowej. Łańcuch znaków (string) to grupa<br />
znaków (dokł. tablica znakowa) zakończona zwykle przez tzw.<br />
"wartownika" - znak NULL (zero). A pojedynczy znak to tylko<br />
pojedynczy znak. Nie ma tu miejsca (i sensu) dodawanie po<br />
pojedynczym znaku "wartownika" końca tekstu - zera.<br />
Gdy w deklaracjach zmiennych tekstowych rezerwujesz miejsce w<br />
pamięci dla napisów - zawsze możesz zażądać od kompilatora <strong>C++</strong><br />
zarezerwowania większej ilości miejsca - na zapas. Zawsze lepiej<br />
mieć zbyt dużo miejsca, niż zbyt mało.<br />
[???] LEPIEJ MIEĆ NIŻ NIE MIEĆ.<br />
- 187-
________________________________________________________________<br />
Upewnij się, czy kompilator zarezerwował (a Ty zadeklarowałeś)<br />
wystarczająco dużo miejsca dla Twoich tekstów. <strong>C++</strong> niestety nie<br />
sprawdza tego w trakcie działania programu. Jeśli będziesz<br />
próbował umieścić w pamięci tekst o zbyt dużej długości (dłuższy<br />
niż zadeklarowałeś w programie), <strong>C++</strong> posłusznie zapisze go do<br />
pamięci, ale może to spowodować nieprawidłowe działanie, bądź<br />
nawet "zawieszenie" programu.<br />
________________________________________________________________<br />
Inną przydatną w praktyce programowania cechą języka <strong>C++</strong> jest<br />
możliwość zadeklarowania zawartości zmiennej tekstowej w<br />
momencie zadeklarowania samej zmiennej. Takie nadanie<br />
początkowej wartości nazywa się zdefiniowaniem, bądź<br />
zainicjowaniem zmiennej. W programie zapisuje się to tak:<br />
char napis[] = "To jest jakis napis";<br />
Powoduje to przypisanie zmiennej tekstowej "napis" konkretnego<br />
łańcucha tekstowego "To jest jakiś napis". Zwróć uwagę, że w<br />
nawiasach nie podajemy ilości znaków, z których składa się<br />
tekst. Kompilator sam policzy sobie ilość znaków (tu 19) i<br />
zarezerwuje miejsce w pamięci dla napisu. Jeśli wolisz sam<br />
zadecydować, możesz zapisać deklarację tak:<br />
char napis[35] = "To jest jakis napis";<br />
Jeśli to zrobisz, kompilator <strong>C++</strong> zarezerwuje w pamięci miejsce<br />
dla 35 znaków, a nie dla 19.<br />
W programach często inicjuje się teksty posługując się nie<br />
tablicą znakową - lesz wskaźnikiem do tekstu. Deklaracja i<br />
zainicjowanie wskaźnika (wskaźnik wskazuje pierwszy element<br />
łańcucha znakowego) wygląda wtedy tak:<br />
char *p = "Jakis tam napis";<br />
Rzućmy okiem na kilka gotowych funkcji, które do manipulowania<br />
tekstami oferuje <strong>C++</strong>.<br />
ŁĄCZENIE TEKSTÓW.<br />
[S] String Concatenation - łączenie łańcuchów tekstowych.<br />
Zlepek/skrót. Słowo strcat w języku <strong>C++</strong> znaczy sklej.<br />
W praktycznych programach zapewne często pojawi się dwa lub<br />
więcej tekstów, które trzeba będzie połączyć w jeden napis.<br />
Wyobraźmy sobie, że imię i nazwisko użytkownika mamy zapisane<br />
jako dwa oddzielne łańcuchy tekstowe. Aby połączyć te dwa teksty<br />
w jeden trzeba przeprowadzić tzw. sklejanie (ang. concatenation)<br />
tekstów. W języku <strong>C++</strong> mamy w tym celu do dyspozycji specjalną<br />
- 188-
funkcję:<br />
strcat() - STRing conCATenation - sklejanie łańcuchów.<br />
Aby połączyć dwa łańcuchy tekstowe napis1 i napis2 w jeden<br />
należy zastosować tę funkcję w taki sposób:<br />
strcat(napis1, napis2);<br />
Funkcja strcat() zadziała w taki sposób, że łańcuch znaków<br />
napis2 zostanie dołączony do końca łańcucha napis1. Po<br />
zakończeniu działania funkcji zmienna napis1 zawiera "swój<br />
własny" napis i dołączony na końcu napis zawarty uprzednio w<br />
zmiennej napis2.<br />
Program poniżej przedstawia praktyczny przykład zastosowania<br />
funkcji strcat().<br />
[P066.CPP]<br />
#include <br />
#include <br />
#include //W tym pliku jest prototyp strcat()<br />
int main(void)<br />
{<br />
char imie[50], nazwisko[30];<br />
clrscr();<br />
cout > imie;<br />
cout > nazwisko;<br />
strcat(imie, " ");<br />
strcat(imie, nazwisko);<br />
cout
strcat(imie, nazwisko);
łańcucha pozwala mieć pewność, że tablica nie zawiera jakichś<br />
starych, zbędnych danych.<br />
Możliwość sprawdzenia, jaką długość ma łańcuch tekstowy może się<br />
to przydać np. do rozmieszczenia napisów na ekranie. Dla<br />
przykładu, pozycja na ekranie, od której rozpocznie się<br />
wyświetlanie napisu zależy od długości tekstu, który został<br />
wyświetlony wcześniej. Do określania długości tekstu masz w <strong>C++</strong><br />
do dyspozycji gotową funkcję:<br />
strlen() - STRing LENgth - długość łańcucha znakowego.<br />
Funkcję strlen() stosuje się w następujący sposób:<br />
unsigned int dlugosc;<br />
char tekst[...];<br />
...<br />
dlugosc = strlen(tekst);<br />
Funkcja ma jeden argument - napis, którego długość należy<br />
określić (tu: zmienna nazywa się tekst). Funkcja strlen() w<br />
wyniku swojego działania ZWRACA długość łańcucha tekstowego jako<br />
liczbę całkowitą bez znaku (nieujemną). Liczba zwrócona jako<br />
wynik przez funkcję strlen() może zostać użyta w dowolny sposób<br />
- jak każda inna wartość numeryczna.<br />
Funkcja strlen() nie podaje w odpowiedzi na wywołanie (mądrze<br />
nazywa się to "zwraca do programu wartość") długości łańcucha<br />
tekstowego, która została zadeklarowana (maksymalnej<br />
teoretycznej), lecz FAKTYCZNĄ DŁUGOŚĆ tekstu. Jeśli, dla<br />
przykładu, zadeklarujemy zmienną tekstową tak:<br />
char string1[30] = "Lubie <strong>C++</strong> ";<br />
zadeklarowana maksymalna długość łańcucha znakowego wynosi 30,<br />
natomiast faktyczna długość łańcucha znakowego wynosi 10 znaków.<br />
Jeśli wywołamy strlen() i każemy jej określić długość łańcucha<br />
znakowego string1:<br />
unsigned int dlugosc = strlen(string1);<br />
funkcja przypisze zmiennej dlugosc wartość 10 a nie 30.<br />
Jeśli wpisałeś poprzedni program program przykładowy do okienka<br />
edycyjnego - wystarczy dodać dwa nowe wiersze.<br />
[P067.CPP]<br />
#include <br />
#include <br />
#include <br />
- 191-
main()<br />
{<br />
char imie[50], nazwisko[20];<br />
int dlugosc;<br />
clrscr();<br />
cout > imie;<br />
cout > nazwisko;<br />
strcat(imie, " ");<br />
strcat(imie, nazwisko);<br />
cout
W tym przykładzie wywołujemy funkcję strncpy() przekazując jej<br />
przy wywołaniu trzy argumenty:<br />
tab_B - destination string - wynikowy łańcuch tekstowy (ten<br />
nowy, który powstanie);<br />
tabn_A - source string - łańcuch źródłowy (ten, z którego<br />
będziemy "obcinać" kawałek);<br />
3 - maksymalna liczba znaków, którą należy obciąć . Obcięte<br />
znaki utworzą "substring" - "BAB".<br />
Pobieranie i "wycinanie" znaków rozpocznie się od pierwszego<br />
znaku łańcucha źródłowego tab_A[80], więc funkcja wywołana w<br />
taki sposób:<br />
strncpy(string1, string2, 3);<br />
spowoduje pobranie pierwszych 3 znaków z łańcucha string2 i<br />
skopiowanie ich do łańcucha string1.<br />
Funkcja strcpy() (Uwaga! bez "n") powoduje skopiowanie całego<br />
łańcucha znaków. Sposób zastosowania funkcji jest podobny do<br />
przykładu z strncpy(), z tym, że nie trzeba podawać liczby<br />
całkowitej określającej ilość znaków do kopiowania. Jak<br />
wszystkie, to wszystkie (jak mawiała babcia), zatem wywołanie<br />
funkcji:<br />
strcpy(string1, string2);<br />
spowoduje skopiowanie całego łańcucha znaków zawartego w<br />
zmiennej string2 do zmiennej string1. Jeśli, dla przykładu,<br />
zmiennej string2 przypiszemy łańcuch tekstowy<br />
string2 = "BABCIA";<br />
to po zadziałaniu funkcji strcpy(string1, string2) zmiennej<br />
string1 zostanie przypisany dokładnie taki sam łańcuch.<br />
Rozważmy program przykładowy. Po uruchomieniu program poprosi o<br />
wpisanie łańcucha tekstowego. Wpisz dowolny tekst. Tekst<br />
powinien zawierać więcej niż 3 znaki. Po pobraniu<br />
wyjściowego/źródłowego tekstu od użytkownika, program pobierze z<br />
tego tekstu kilka mniejszych łańcuchów tekstowych typu<br />
"substring" i wyświetli je na ekranie.<br />
[P068.CPP]<br />
#include <br />
#include <br />
#include <br />
#include <br />
main()<br />
{<br />
- 193-
char napis1[80] = "";<br />
char napis2[80] = "";<br />
char napis3[80] = "";<br />
clrscr();<br />
cout
wprowadzać napisów zawierających spacje. Jeśli zastosowalibyśmy<br />
w programie<br />
cin >> string1;<br />
i wpisali tekst zawierający spacje, np.:<br />
To nie ważne, czy Polska...<br />
wczytane zostałyby tylko znaki To (do pierwszej spacji). Z kolei<br />
funkcja gets() pozwala wczytać wiersz tekstu zawierający dowolne<br />
znaki uznając za koniec znak CRLF (powrót karetki, zmiana<br />
wiersza) generowany po naciśnięciu [Entera]. Przeciwną,<br />
symetryczną funkcją do gets() jest funkcja puts() (ang. PUT<br />
String - wyprowadź wiersz tekstu). Prototypy funkcji gets() i<br />
puts() znajdują się w pliku nagłówkowym STDIO.H. Dlatego ten<br />
plik nagłówkowy został dołączony na początku dyrektywą #include.<br />
WYSZUKIWANIE TEKSTÓW.<br />
Wyobraźmy sobie, że mamy listę imion i chcemy na tej liście<br />
odszukać znajome imię np. Alfons. Specjalnie do takich celów <strong>C++</strong><br />
dysponuje funkcją:<br />
strstr() - STRing's subSTRing - część łańcucha tekstowego<br />
Aby wyszukać w większym tekście mniejszy fragment, powinniśmy<br />
wywołując funkcję przekazać jej dwie informacje:<br />
GDZIE SZUKAĆ - wskazać łańcuch tekstowy do przeszukiwania;<br />
i<br />
CZEGO SZUKAĆ - podać ten tekst, który nas interesuje i który<br />
funkcja powinna dla nas odnaleść.<br />
Funkcja strstr(), powinna zatem mieć dwa argumenty:<br />
char Lista[] = "Adam, Buba, Adolf, Magda";<br />
...<br />
gdzie = strstr(Lista, "Adolf");<br />
Funkcja strstr() wyszukuje pierwsze wystąpienie danego tekstu.<br />
Po wyszukaniu, funkcja powinna nam w jakiś sposób wskazać, gdzie<br />
znajduje się interesujący nas tekst. Jak wiesz, do wskazywania<br />
różnych interesujących rzeczy służą w <strong>C++</strong> WSKAŹNIKI (pointer). W<br />
przykładzie powyżej funkcja strstr() w wyniku swojego działania<br />
zwraca wskaźnik do szukanego tekstu "Alfons". Aby wskaźnik nam<br />
nie przepadł, trzeba go zapamiętać. Funkcja zatem przypisuje<br />
wskaźnik zmiennej "gdzie". W miejscu przeznaczonym dla tej<br />
zmiennej w pamięci będzie odtąd przechowywany wskaźnik,<br />
- 195-
wskazujący nam - gdzie w pamięci kmputera znajduje się<br />
interesujący nas tekst "Alfons\0".<br />
Aby komputer zarezerwował miejsce w pamięci dla wskaźnika,<br />
trzeba go o to "poprosić" na początku programu, deklarując, że w<br />
programie zamierzamy posługiwać się wskaźnikiem. Deklaracja<br />
wskaźnika do zmiennej tekstowej wygląda tak:<br />
char *wskaznik;<br />
Przykładowy program pniżej demonstruje sposób zadeklarowania<br />
wskaźnika i wyszukiwanie tekstu. Program nie oczekuje żadnej<br />
informacji wejściowej od użytkownika. Uruchom program i<br />
przeanalizuj wydruk na ekranie porównując go z tekstem programu.<br />
[P069.CPP]<br />
#include <br />
#include <br />
#include <br />
main()<br />
{<br />
char string1[] = "Ala, Magda, Adam, Alfons, Jasiek, Alfons, As";<br />
char *pointer;<br />
clrscr();<br />
cout
wyszukanego pierwszego wystąpienia zadanego łańcucha znaków.<br />
[P070.CPP]<br />
#include <br />
#include <br />
#include <br />
#include <br />
main()<br />
{<br />
char str1[80], str2[80];<br />
char *ptr;<br />
clrscr();<br />
cout
{<br />
char string1[80];<br />
clrscr();<br />
cout
LEKCJA 19: KILKA INNYCH PRZYDATNYCH FUNKCJI.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak zapisać teksty na dysku i<br />
jak jeszcze można nimi manipulować przy pomocy gotowych funkcji<br />
<strong>Borland</strong> <strong>C++</strong>.<br />
________________________________________________________________<br />
Program poniżej demonstruje zastosowanie trzech przydatnych<br />
funkcji:<br />
[P072.CPP]<br />
#include <br />
int main(void)<br />
{<br />
int i, x = 0, y = 0;<br />
clrscr();<br />
for (i = 1; i < 10; i++)<br />
{<br />
y = i;<br />
x = 5*i;<br />
textbackground(16-i);<br />
textcolor(i);<br />
gotoxy(x, y);<br />
cprintf("Wspolrzedne: x=%d y=%d", x, y);<br />
getch();<br />
}<br />
return 0;<br />
}<br />
textbackground() - ustaw kolor tła pod tekstem<br />
texcolor() - ustaw kolor tekstu<br />
gotoxy() - rozpocznij drukowanie tekstu od punktu o<br />
współrzędnych ekranowych<br />
x - numer kolumny (w normalnym trybie: 1-80)<br />
y - numer wiersza (w normalnym trybie: 1-25)<br />
[Z]<br />
________________________________________________________________<br />
1. Rozmieść na ekranie napisy i znaki semigraficzne tworzące<br />
rysunek tabelki.<br />
2. Opracuj program, w którym pojedyncze znaki, bądź napisy będą<br />
poruszać się po ekranie.<br />
3. Spróbuj przyspieszyć działanie swojego programu z<br />
poprzedniego zadania poprzez wstawkę w assemblerze.<br />
________________________________________________________________<br />
OPERACJE PLIKOWE - NIEOBIEKTOWO.<br />
W systemia DOS dane i programy są zgrupowane w pliki. Pliki<br />
(ang. file) mogą być TEKSTOWE i BINARNE. Najczęstszymi<br />
- 199-
operacjami na plikach są:<br />
* Utworzenie nowego pliku (ang. CREATE);<br />
* Odczyt z pliku (ang. READ);<br />
* Zapis do pliku (WRITE);<br />
* Otwarcie pliku (OPEN);<br />
* Zamknięcie pliku (CLOSE);<br />
* Wyszukanie danej w pliku (SEEK);<br />
W kontaktach z urządzeniami - np. z dyskiem pośredniczą DOS i<br />
BIOS. To system DOS wie, gdzie na dysku szukać pliku (katalogu)<br />
o podanej nazwie i w których sektorach dysku znajdują się<br />
fizycznie dane należące do danego pliku. Operacje z plikami<br />
opierają się o odwoływanie do systemu operacyjnego za<br />
pośrednictwem tzw. Deskryptora pliku (File Descriptor - numer<br />
identyfikacyjny pliku).<br />
Zestaw "narzędzi" potrzebnych nam do pracy to:<br />
IO.H - prototypy funkcji obsługi WEjścia/WYjścia (ang.<br />
Input/Output=IO);<br />
FCNTL.H - plik zawierający definicje wymienionych poniżej<br />
stałych:<br />
O_BINARY - otwarcie pliku w trybie binarnym;<br />
O_TEXT - otwarcie pliku w trybie tekstowym;<br />
O_RDONLY (Open for Read Only) - otwórz tylko do odczytu;<br />
O_WRONLY (...Write Only) - tylko dla zapisu;<br />
O_RDWR (Reading and Writing) dozwolony zapis i odczyt;<br />
STAT.H - zawiera definicje stałych<br />
S_IREAD - plik tylko do odczytu (przydatne dla funkcji creat);<br />
S_IWRITE - tylko zapis (przydatne dla funkcji creat);<br />
FUNKCJE:<br />
int open(p1, p2, p3) - trójparametrowa funkcja otwierająca plik;<br />
(parametry patrz przykład) zwraca do programu Wynik = -1<br />
(operacja zakończona niepowodzeniem - np. nie ma pliku)<br />
lub Wynik = File Descriptor - numer pliku przekazany przez DOS.<br />
int creat(p1, p2) - funkcja tworząca nowy plik;<br />
int read(...) - funkcja czytająca z pliku;<br />
int write(...) - funkcja zapisu do pliku;<br />
imt close(...) - zamknięcie pliku.<br />
Po uruchomieniu program otwiera automatycznie trzy standardowe<br />
pliki, związane z urządzeniami:<br />
0 - stdin - standardowy plik wejściowy (norm. klawiatura<br />
konsoli);<br />
1 - stdout - standardowy plik wyjściowy (norm. monitor);<br />
2 - stderr - standardowy plik wyjściowy - diagnostyczny<br />
(komunikaty o błędach).<br />
[S] STD...<br />
STandarD INput - standardowe wejście.<br />
- 200-
STD<br />
STD<br />
OUTput - standardowe wyjście.<br />
ERRors - plik diagnostyczny.<br />
//[P072-2.CPP]<br />
# include <br />
# include <br />
# include //Duze litery tylko dla podkreslenia<br />
# include <br />
# include <br />
char *POINTER;<br />
int IL_znakow, DLUG_pliku, TRYB_dostepu, Wynik, i;<br />
int Plik_1, Plik_2;<br />
char BUFOR[20] = {"TEKST DO PLIKU"};<br />
char STOS[3], ZNAK='X';<br />
main()<br />
{<br />
POINTER = &BUFOR[0];<br />
printf("Wloz dyskietke do A: i nacisnij cos...\n");<br />
Plik_1 = creat( "a:\\plik1.dat", S_IWRITE);<br />
if (Plik_1 == -1)<br />
printf("\n Nie udalo sie zalozyc plik1.dat...");<br />
Plik_2 = creat( "a:\\plik_2.dat", S_IWRITE);<br />
if (Plik_2 == -1)<br />
printf("\n Klops przy Plik2.dat");<br />
_fmode = O_BINARY; //Bedziemy otwierac w trybie binarnym<br />
Wynik = open( "a:\\plik1.dat", O_WRONLY );<br />
if (Wynik == -1)<br />
printf("\n Nie udalo sie otworzyc pliku...");<br />
IL_znakow = 15;<br />
//Ilosc znakow do zapisu<br />
Wynik =write( Plik_1, POINTER, IL_znakow );<br />
printf("Zapisalem %d znakow do pliku.", Wynik);<br />
close( Plik_1 );<br />
Plik_1 = open("a:\\Plik1.dat", O_RDONLY );<br />
Plik_2 = open("a:\\Plik_2.dat", O_WRONLY );<br />
POINTER = &STOS[0];<br />
for (i=1; ZNAK; i++) //Kopiuje plik + spacje<br />
{<br />
STOS[1] = ZNAK;<br />
write( Plik_2, POINTER, 2);<br />
- 201-
ead( Plik_1, &ZNAK, 1);<br />
}<br />
close(Plik_1); close(Plik_2);<br />
getch();<br />
return 0;<br />
}<br />
Przykładowy program wykonuje następujące czynności:<br />
1. Tworzy plik a:\plik1.dat (potrzebny dostęp do dyskietki a:).<br />
2. Tworzy plik a:\plik_2.dat.<br />
3. Otwiera plik a:\plik1.dat w trybie binarnym tylko do zapisu.<br />
(ZWRÓĆ UWAGĘ, że tryb binarny nie przeszkadza zapisać tekstu.)<br />
4. Dokonuje zapisu do pliku.<br />
5. Zamyka plik a:\plik1.dat.<br />
6. Otwiera plik1.dat w trybie binarnym tylko do odczytu.<br />
7. Otwiera plik_2.dat tylko do zapisu.<br />
8. Kopiuje plik1.dat do plik_2.dat dodając spacje.<br />
Zwróć uwagę na konstrukcję:<br />
for(i=1; ZNAK; i++)<br />
Wyjaśnienie. Póki jest znak wykonuj kopiowanie. Przypominam, że<br />
koniec to NUL - '\0'.<br />
Jeśli czytamy i piszemy po kolei - wszystko jest proste. Jeżeli<br />
natomiast chcemy wyszukać w pliku określone miejsce, to będzie<br />
nam jeszcze dodatkowo potrzebny mechanizm do określenia pozycji<br />
w pliku - tzw. WSKAŹNIK PLIKOWY. Pozycję można określać względem<br />
początku pliku:<br />
SEEK_SET - stała określająca pozycjonowanie względem początku<br />
pliku;<br />
SEEK_CUR - względem położenia bieżącego (ang. Current -<br />
bieżący);<br />
SEEK_END - określenie pozycji względem końca pliku;<br />
EOF - End Of File - znak końca pliku.<br />
Funkcja lseek():<br />
WSK_PLK = long int lseek( plik, o_ile, kierunek);<br />
służy do pozycjonowania w pliku.<br />
Liczba typu long int określająca pozycję w pliku nazywana jest<br />
WSKAŹNIKIEM PLIKOWYM ( w programie przykładowym została<br />
oznaczona long int WSK_PLK).<br />
W programie przykładowym wykonywane jest kolejno:<br />
* utworzenie na dysku pliku PROBA.DAT;<br />
* zapis do pliku wprowadzonych z klawiatury liczb całkowitych<br />
typu int;<br />
- 202-
* zamknięcie pliku;<br />
* otwarcie pliku do odczytu;<br />
* ustawienie wskaźnika na końcu pliku;<br />
* odczyt z pliku od końca;<br />
* wyprowadzenie odczytanych z pliku danych na ekran.<br />
[P073.CPP]<br />
# include "sys\stat.h"<br />
# include "conio.h"<br />
# include "stdio.h"<br />
# include "io.h"<br />
# include "fcntl.h"<br />
# define Cofnij_o_Zero 0<br />
# define dwa_bajty 2<br />
int Numer = 0;<br />
int Plik, L, M, i;<br />
long int Dlug_Pliku;<br />
main()<br />
{<br />
clrscr();<br />
creat("A:\PROBA.DAT", S_IWRITE);<br />
printf("\nPodaj liczbe rozna od zera, zero - KONIEC");<br />
_fmode=O_BINARY;<br />
Plik=open("A:\PROBA.DAT", O_WRONLY);<br />
do<br />
{<br />
printf("\n Nr liczby \t%d\t\t", Numer++);<br />
scanf("%d", &L);<br />
if (L) write(Plik, &L, 2);<br />
}<br />
while (L != 0);<br />
close(Plik);<br />
getch();<br />
printf("\n Teraz odczytam te liczby z pliku \n");<br />
Plik=open("A:\PROBA.DAT", O_RDONLY);<br />
Dlug_Pliku=lseek(Plik, 0, SEEK_END);<br />
for (i=Dlug_Pliku-dwa_bajty; i>=0; i-=2)<br />
{<br />
lseek(Plik, i, SEEK_SET);<br />
read(Plik, &M, dwa_bajty);<br />
printf("%d, ", M);<br />
}<br />
close(Plik);<br />
getch();<br />
return 0;<br />
}<br />
[Z]<br />
________________________________________________________________<br />
- 203-
Opracuj program wykonujący operacje na tekstach opisane<br />
wcześniej na łańcuchach tekstowych pobieranych z zewnętrznych<br />
plików dyskowych i umieszczanych w wynikowych plikach<br />
tekstowych.<br />
________________________________________________________________<br />
- 204-
LEKCJA 20. JEŚLI PROGRAM POWINIEN URUCHOMIĆ INNY PROGRAM...<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak w <strong>C++</strong> można programować<br />
* procesy potomne<br />
* pisać programy rezydujące w pamięci (TSR)<br />
________________________________________________________________<br />
O programach rezydentnych (TSR) i procesach potomnych.<br />
Warunek zewnętrznej zgodności z poprzednimi wersjami DOS<br />
wyraźnie hamuje ewolucję systemu MS DOS w kierunku "poważnych"<br />
systemów operacyjnych umożliwjających pracę wieloprogramową w<br />
trybie "multiuser", "multitasking" i "time sharing". Pewną<br />
namiastkę pracy wieloprocesowej dają nam już DOS 5/6 i Windows<br />
3.1. Można już otwierać wiele okien programów jednocześnie,<br />
można np. drukować "w tle", można wreszcie pisać rezydujące<br />
stale w pamięci programy klasy TSR (ang. Terminated and Stay<br />
Resident) uaktywniające się "od czasu do czasu".<br />
O bloku PSP.<br />
System DOS przydziela programom blok - "nagłówek" wstępny<br />
nazywany PSP (ang. Program Segment Prefix). Blok ten zawiera<br />
informacje o stanie systemu DOS w momencie uruchamiania programu<br />
(nazywanego tu inaczej procesem). Znajdują się tam informacje o<br />
bieżącym stanie zmiennych otoczenia systemowego (ang.<br />
environment variables) i parametrach uruchomieniowych. Blok PSP<br />
zajmuje 256 bajtów na początku kodu programu w zakresie adresów:<br />
CS:0000 ... CS:0100 (hex)<br />
Właściwy kod programu zaczyna się zatem od adresu CS:0100.<br />
Interpreter rozkazów systemu DOS ładuje programy do pamięci<br />
posługując się funkcją systemową nr 75 (4B hex). Wszystko jest<br />
proste dopóki mamy do czynienia z programem "krótkim" typu<br />
*.COM. Jeśli jednakże program uruchamiany jest w wersji<br />
"długiej" - *.EXE, dowolna może być nie tylko długość pliku, ale<br />
także początkowa zawartość rejestrów CS, SS, SP i IP. W plikach<br />
typu *.EXE początek bloku PSP wskazują rejestry DS (DS:0000) i<br />
ES. W <strong>Borland</strong> <strong>C++</strong> masz do dyspozycji specjalną funkcję getpsp()<br />
przy pomocy której możesz uzyskać dostęp do bloku PSP programu.<br />
Krótki przykład zastosowania tej funkcji poniżej:<br />
/* Przykład zastosowania funkcji getpsp(): */<br />
# include <br />
# include <br />
main()<br />
{<br />
static char TAB[128];<br />
char far *ptr;<br />
- 205-
int dlugosc, i;<br />
printf("Blok PSP: %u \n", getpsp());<br />
ptr = MK_FP(_psp, 0x80);<br />
dlugosc = *ptr;<br />
for (i = 0; i < dlugosc; i++)<br />
TAB[i] = ptr[i+1];<br />
printf("Parametry uruchomieniowe: %s\n", TAB);<br />
}<br />
W normalnych warunkach po wykonaniu "swojej roboty" program<br />
zostaje usunięty z pamięci operacyjnej (czym zajmuje się funkcja<br />
systemowa nr 76 - 4C (hex)). Aby tak się nie stało, program<br />
może:<br />
* uruchomić swój proces (program) potomny;<br />
* wyjść "na chwilę" do systemu DOS - tj. uruchomić jako swój<br />
proces potomny interpreter COMMAND.COM;<br />
* przekazać sterowanie programowi COMMAND.COM pozostając w<br />
pamięci w postaci "uśpionej" oczekując na uaktywninie.<br />
Poniżej kilka prostych przykładów uruchamiania jednych procesów<br />
przez inne w <strong>Borland</strong> <strong>C++</strong>:<br />
/* Funkcja execv(): uruchomienie programu "potomnego"*/<br />
# include <br />
# include <br />
# include <br />
void main(int argc, char *argv[])<br />
{<br />
int i;<br />
printf("Parametry uruchomieniowe:");<br />
for (i=0; i
void main()<br />
{<br />
printf("Wyjscie do DOS i wykonanie jednego rozkazu:\n");<br />
system("dir > c:\plik.dir");<br />
}<br />
/* Funkcje grupy spawn...() : spawnl() */<br />
# include <br />
# include <br />
# include <br />
void main()<br />
{<br />
int rezultat;<br />
rezultat = spawnl(P_WAIT, "program.exe", NULL);<br />
if (rezultat == -1)<br />
{<br />
perror(" Fiasko !");<br />
exit(1);<br />
}<br />
}<br />
/* Funkcja spawnle() */<br />
# include <br />
# include <br />
# include <br />
void main()<br />
{<br />
int rezultat;<br />
rezultat = spawnle(P_WAIT, "program.exe", NULL, NULL);<br />
if (rezultat == -1)<br />
{<br />
perror("Fiasko !");<br />
exit(1);<br />
}<br />
}<br />
Zagadnienie uruchamiania programów potomnych (ang. child<br />
process) przez programy macieżyste (ang. parent process) jest<br />
rozpracowane w <strong>C++</strong> dość dokładnie i zarazem obszernie. Istnieje<br />
wiele gotowych funkcji bibliotecznych, z usług których możesz tu<br />
skorzystać. Wszystko to nie jest jednak "prawdziwym" programem<br />
TSR. Przyjrzyjmy się zatem dokładniej dopuszcalnym przez system<br />
DOS sposobom zakończenia programu nie powodującym usunięcia<br />
programu z pamięci.<br />
Jeśli program rezydentny jest niewielki (kod < 64 K), możemy<br />
- 207-
zakończyć program posługując się przerywaniem INT 39 (27 hex).<br />
Jeśli natomiast zamierzamy posługiwać się dłuższymi programami,<br />
mamy do dyspozycji funkcję systemową nr 49 (31 hex). Należy tu<br />
zwrócić uwagę, że zakończenie programu w taki sposób (z<br />
pozostawieniem w pamięci) nie spowoduje automatycznego<br />
zamknięcia plików, a jedynie opróżnienie buforów. Programy<br />
rezydentne dzieli się umownie na trzy kategorie:<br />
[BP] - background process - procesy działające "w tle";<br />
[SV] - services - programy usługowe - np. PRINT;<br />
[PP] - pop up programs - uaktywniane przez określoną kombinację<br />
klawiszy;<br />
System DOS dysponuje tzw. przerywaniem multipleksowym<br />
(naprzemiennym) wykorzystywanym często przez programy<br />
rezydentne. Jest to przerywanie nr INT 47 (2F hex). MS DOS<br />
załatwia takie problemy funkcjami nr 37 (25 hex) - zapisanie<br />
wektora przerywania i 53 (35 hex) - odczytanie wektora<br />
przerywania.<br />
Z jakich funkcji <strong>C++</strong> można skorzystać?<br />
W <strong>C++</strong> masz do dyspozycji parę funkcji getvect() i setvect()<br />
(ang. GET/SET VECTor - pobierz/ustaw wektor przerywania).<br />
Poniżej krótkie przykłady zastosowań tych funkcji.<br />
/* Opcja: Options | Compiler | Code generation | Test Stack<br />
Overflow powinna zostać wyłączona [ ] (off) */<br />
# include "stdio.h"<br />
# include "dos.h"<br />
# include "conio.h"<br />
/* INT 28 (1C hex) - Przerywanie zegarowe */<br />
void interrupt ( *oldhandler)(void);<br />
int licznik = 0;<br />
void interrupt handler(void)<br />
{<br />
/* Inkrementacja globalnej zmiennej licznik */<br />
licznik++;<br />
/* Wywolujemy stary "handler" zegara */<br />
oldhandler();<br />
}<br />
void main()<br />
{<br />
/* Zapamiętaj poprzedni wektor przerywania 28 */<br />
oldhandler = getvect(28);<br />
/* Zainstaluj nową funkcje obslugi przerywania */<br />
setvect(28, handler);<br />
- 208-
* Inkrementuj licznik */<br />
for (; licznik < 10; ) printf("licznik: %d\n",licznik);<br />
//odtworz stara funkcje obslugi przerywania: interrupt handler<br />
setvect(28, oldhandler);<br />
}<br />
# include <br />
# include <br />
void interrupt nowa_funkcja(); // prototyp funkcji - handlera<br />
void interrupt (*oldfunc)(); /* interrupt function pointer */<br />
int warunek = 1;<br />
main()<br />
{<br />
printf("\n [Shift]+[Print Screen] = Quit \n");<br />
printf("Zapamietaj, i nacisnij cosik....");<br />
while(!kbhit());<br />
/* zapamietaj stary wektor */<br />
oldfunc = getvect(5);<br />
/* INT 5 to przerywanie Sys Rq, albo Print Screen */<br />
/* zainstaluj nowa funkcje obslugi: interrupt handler */<br />
setvect(5, nowa_funkcja);<br />
while (warunek) printf(".");<br />
/* Odtworz stary wektor przerywania */<br />
setvect(5, oldfunc);<br />
printf("\n Udalo sie... nacisnij cosik...");<br />
while(!kbhit());<br />
}<br />
/* Definicja nowego handlera */<br />
void interrupt nowa_funkcja()<br />
{<br />
warunek = 0;<br />
/* jesli warunek == 0, petla zostanie przerwana*/<br />
}<br />
Jeśli nasz program zamierza korzystać z przerywania<br />
multipleksowego INT 47 (2F hex), należy pamiętać, że przerywanie<br />
to wykorzystują także inne programy systemowe. Rozróżniać te<br />
programy można przy pomocy identyfikatorów (podaję dziesiętnie):<br />
01 - PRINT.EXE<br />
06 - ASSIGN.COM<br />
- 209-
16 - SHARE.EXE (10 hex)<br />
26 - ANSI.SYS<br />
67 - HIMEM.SYS<br />
72 - DOSKEY.COM<br />
75 - TASK SWITCHER<br />
173 - KEYB.COM<br />
174 - APPEND.EXE<br />
176 - GRAFTABL.COM<br />
183 - APPEND.EXE<br />
Identyfikator programu TSR jest przekazywany za pośrednictwem<br />
rejestru AH.<br />
System DOS jest na razie systemem w zasadzie jednozadaniowym i<br />
jednoużytkownikowym, w którym zasoby są przydzielane procesom<br />
kolejno (ang. serially reusable resources). Aby uchronić się<br />
przed potencjalnym konfliktem, powinniśmy upewnić się, czy DOS<br />
"nic nie robi". Często stosowaną "sztuczką techniczną" jest<br />
zastosowanie flag ErrorMode i InDos systemu oraz wykorzystanie<br />
mechanizmów przerywań nr 36 i 40 (24 i 28 hex). Przydatną<br />
informacją jest także identyfikator programu - PID. Na taką<br />
ewntualność <strong>Borland</strong> <strong>C++</strong> dysponuje makrem getpid zdefiniowanym w<br />
pliku nagłówkowym :<br />
# define getpid() (_psp)<br />
Inną przydatną funkcją może okazać się keep() (ang. keep<br />
resident - pozostań rezydentny). Oto krótki przykład<br />
zastosowania tej funkcji - znów z wykorzystaniem przerywań<br />
zegarowych.<br />
# include <br />
# define INTR 0x1C /* przerywanie INT 28 */<br />
# define ATTR 0x7900<br />
/* ograniczenie wielkości sterty (heap length) i stosu (stack<br />
length): */<br />
extern unsigned _heaplen = 1024;<br />
extern unsigned _stklen = 512;<br />
void interrupt ( *oldhandler)(void);<br />
void interrupt handler(void)<br />
{<br />
unsigned int (far *ekran)[80];<br />
static int licznik;<br />
// Adres pamieci dla monitora barwnego: B800:0000.<br />
// Dla monitora monochromatycznego: B000:0000.<br />
ekran = MK_FP(0xB800,0);<br />
// piloksztaltna zmiana licznika w przedziale 0 ... 9<br />
- 210-
licznik++;<br />
licznik %= 10;<br />
ekran[0][79] = licznik + '0' + ATTR;<br />
// wywołaj stara funkcje obslugi - old interrupt handler:<br />
oldhandler();<br />
}<br />
void main()<br />
{<br />
oldhandler = getvect(INTR);<br />
// zainstaluj nowa funkcje interrupt handler<br />
setvect(INTR, handler);<br />
/* _psp - to adres początku programu, SS:SP to adres stosu,<br />
czyli koniec programu. Biorac pod uwage przesuniecie<br />
SEGMENT/OFFSET o jedna tetrade: SS:SP = SS + SP/16; */<br />
keep(0, (_SS + (_SP/16) - _psp));<br />
}<br />
Kilka istotnych drobiazgów technicznych.<br />
W <strong>Borland</strong> <strong>C++</strong> masz do dyspozycji predefiniowane struktury<br />
BYTEREGS (rejestry jednobajtowe - "połówki") i WORDREGS<br />
(rejestry dwubajtowe). Możesz po tych strukturach dziedziczyć i<br />
np. taką metodą wbudować je do swoich własnych klas. Nic nie<br />
stoi na przeszkodzie, by utworzyć np. klasę<br />
class REJESTRY : public WORDREGS<br />
{<br />
...<br />
};<br />
czy też własną strukturę:<br />
struct REJESTRY : WORDREGS { ... };<br />
Definicje tych struktur w <strong>Borland</strong> <strong>C++</strong> wyglądają następująco:<br />
struct BYTEREGS<br />
{<br />
unsigned int al, ah, bl, bh, cl, ch, dl, dh;<br />
};<br />
struct WORDREGS<br />
{<br />
unsigned int ax, bx, cx, dx, si, di, cflag, flags;<br />
};<br />
Rejestry segmentowe mają własną strukturę:<br />
- 211-
struct SREGS<br />
{<br />
unsigned int es, cs, ss, ds;<br />
};<br />
Pole WORDREGS::cflag odpowiada stanowi flagi przeniesienia (ang.<br />
Carry Flag) rejestru flags mikroprocesora, a pole<br />
WORDREGS::flags odpowiada stanowi całości rejestru (w wersji 16<br />
- bitowej). Ponieważ rejestry mogą być widziane alternatywnie<br />
jako podzielone na miezależne połówki - lub jako całość, to<br />
właśnie "albo - albo" wyraża w <strong>C++</strong> unia. W <strong>Borland</strong> <strong>C++</strong> taka<br />
predefiniowana unia nazywa się REGS:<br />
union REGS<br />
{<br />
struct WORDREGS x;<br />
struct BYTEREGS h;<br />
};<br />
Z tych predefiniowanych struktur danych korzystają m. in.<br />
funkcje int86() intdosx() i int86x() ("x" pochodzi od eXtended -<br />
rozszerzony). Oto krótkie przykłady zastosowania tych funkcji.<br />
# include <br />
# include <br />
# include <br />
# define INT_NR 0x10 // 10 hex == 16 (Nr przerywania) VIDEO<br />
void UstawKursor(int x, int y)<br />
{<br />
union REGS regs;<br />
regs.h.ah = 2; // ustaw kursor<br />
regs.h.dh = y; // Wspolrzedne kursora na ekranie<br />
regs.h.dl = x;<br />
regs.h.bh = 0; // Aktywna stronica ekranu --> video page 0<br />
int86(INT_NR, ®s, ®s);<br />
}<br />
void main()<br />
{<br />
clrscr();<br />
UstawKursor(30, 12);<br />
printf("Tekst - Test");<br />
while(!kbhit());<br />
}<br />
# include <br />
# include <br />
# include <br />
- 212-
void main()<br />
{<br />
char nazwapliku[40];<br />
union REGS inregs, outregs;<br />
struct SREGS segregs;<br />
printf("\nPodaj nazwe pliku: ");<br />
gets(nazwapliku); // gets() == GET String<br />
inregs.h.ah = 0x43;<br />
inregs.h.al = 0x21;<br />
inregs.x.dx = FP_OFF(nazwapliku);<br />
segregs.ds = FP_SEG(nazwapliku);<br />
int86x(0x21, &inregs, &outregs, &segregs);<br />
printf("\n Atrybuty pliku: %X\n", outregs.x.cx);<br />
}<br />
# include <br />
# include <br />
int SkasujPlik(char far*) // Prototyp<br />
void main()<br />
{<br />
int error;<br />
err = SkasujPlik("PLIK.DAT");<br />
if (!error) printf("\nSkasowalem plik PLIK.DAT");<br />
else<br />
printf("\nNie moge skasowac pliku PLIK.DAT");<br />
}<br />
int SkasujPlik(char far *nazwapliku)<br />
{<br />
union REGS regs; struct SREGS sregs;<br />
int wynik;<br />
regs.h.ah = 0x41; // Funkcja kasowania pliku<br />
regs.x.dx = FP_OFF(nazwapliku);<br />
sregs.ds = FP_SEG(nazwapliku);<br />
wynik = intdosx(®s, ®s, &sregs);<br />
return(regs.x.cflag ? wynik : 0);<br />
// Jesli CF == 1, nastapilo fiasko operacji<br />
}<br />
I wreszcie na zakończenie szczegóły techniczne działania funkcji<br />
systemowej nr 49 (31 hex) odpowiedzialnej za obsługę programów<br />
rezydujących w pamięci (załadowanie procesu z pozostawieniem w<br />
pamięci).<br />
1. Wywołanie funkcji:<br />
AL = kod powrotu (ang. return code);<br />
AH = 0031 (hex) - nr funkcji;<br />
DX = długość programu TSR w paragrafach - Size/16 [Bajtów];<br />
2. Działanie:<br />
* funkcja nie zamyka plików, lecz opróżnia bufory;<br />
- 213-
* funkcja odtwarza wektory przerywań nr 34, 35, 36 (hex 21, 22,<br />
23);<br />
* proces macieżysty może uzyskać kod powrotu przy pomocy funkcji<br />
nr 77 (4D hex).<br />
Wykorzystanie struktury SDA (ang. Swappable Data Area - obszar<br />
wymiennych danych) nie jest praktyką zalecaną.<br />
Tworząc programy rezydentne bądź bardzo ostrożny i pamiętaj o<br />
jednej z podstawowych zasad - NIE JESTEŚ (tzn Twój program nie<br />
jest) SAM.<br />
________________________________________________________________<br />
- 214-
LEKCJA 21: KILKA PROCESÓW JEDNOCZEŚNIE.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak to zrobić, by Twój PC mógł<br />
wykonywać kilka rzeczy jednocześnie.<br />
________________________________________________________________<br />
Procesy współbieżne.<br />
Sprzęt, czyli PC ma możliwości zdecydowanie pozwalające na<br />
techniczną realizację pracy wielozadaniowej. Nie ma też żadnych<br />
przeciwskazań, by zamiast koprocesora umożliwić w PC instalację<br />
drugiego (trzeciego) równoległego procesora i uprawiać na PC<br />
poważne programowanie współbieżne. Po co? To proste. Wyobraź<br />
sobie Czytelniku, że masz procesor pracujący z częstotliwością<br />
25 MHz (to 25 MILIONÓW elementarnych operacji na sekundę!).<br />
Nawet, jeśli wziąć pod uwagę, że niektóre operacje (dodawanie,<br />
mnożenie, itp.) wymagają wielu cykli - i tak można w<br />
uproszczeniu przyjąć, że Twój procesor mógłby wykonać od<br />
kilkuset tysięcy do kilku milionów operacji w ciągu sekundy.<br />
Jeśli pracujesz np. z edytorem tekstu i piszesz jakiś tekst -<br />
znacznie ponad 99% czasu Twój procesor czeka KOMPLETNIE<br />
BEZCZYNNIE (!) na naciśnięcie klawisza. Przecież Twój komputer<br />
mogłby w tym samym czasie np. i formatować dyskietkę (dyskietka<br />
też jest powolna), i przeprowadzać kompilację programu, i<br />
drukować dokumenty, i przeprowadzić defragmentację drugiego<br />
dysku logicznego, itp. itd..<br />
Nawet taka pseudowspółbieżność realizowana przez DOS, Windows,<br />
czy sieć jest ofertą dostatecznie atrakcyjną, by warto było<br />
przyjrzeć się mechanizmom PSEUDO-współbieżności w C i <strong>C++</strong>.<br />
Współbieżność procesów, może być realizowana na poziomie<br />
* sprzętowym (architektura wieloprocesorowa),<br />
* systemowym (np. Unix, OS/2),<br />
* nakładki (np. sieciowej - time sharing, token passing)<br />
* aplikacji (podział czasu procesora pomiędzy różne<br />
funkcje/moduły tego samego pojedynczego programu).<br />
My zajmiemy się tu współbieżnością widzianą z poziomu aplikacji.<br />
Funkcje setjmp() (ang. SET JuMP buffer - ustaw bufor<br />
umożliwiający skok do innego procesu) i longjmp() (ang. LONG<br />
JuMP - długi skok - poza moduł) wchodzą w skład standardu C i w<br />
związku z tym zostały "przeniesine" do wszystkich kompilatorów<br />
<strong>C++</strong> (nie tylko Borlanada).<br />
Porozmawiajmy o narzędziach.<br />
Zaczniemy od klasycznego zestawu narzędzi oferowanego przez<br />
<strong>Borland</strong>a. Aby zapamiętać stan przerwanego procesu stosowana jest<br />
w C/<strong>C++</strong> struktura PSS (ang. Program Status Structure) o nazwie<br />
jmp_buf (JuMP BUFfer - bufor skoku). W przypadku współbieżności<br />
- 215-
wielu procesów (więcej niż dwa) stosuje się tablicę złożoną ze<br />
struktur typu<br />
struct jmp_buf TablicaBuforow[n];<br />
Struktura służy do przechowywania informacji o stanie procesu<br />
(rejestrach procesora w danym momencie) i jest predefiniowana w<br />
pliku SETJMP.H:<br />
typedef struct<br />
{<br />
unsigned j_sp, j_ss, j_flag, j_cs;<br />
unsigned j_ip, j_bp, j_di, j_es;<br />
unsigned j_si, j_ds;<br />
} jmb_buf[1];<br />
Prototypy funkcji:<br />
int setjmp(jmp_buf bufor);<br />
void longjmp(jmp_buf bufor, int liczba);<br />
W obu przypadkach jmp_buf bufor oznacza ten sam typ bufora<br />
(niekoniecznie ten sam bufor - może ich być wiele), natomiast<br />
int liczba oznacza tzw. return value - wartość zwracaną po<br />
powrocie z danego procesu. Liczba ta może zawierać informację, z<br />
którego procesu nastąpił powrót (lub inną przydatną w<br />
programie), ale nie może być ZEREM. Jeśli funkcja longjmp()<br />
otrzyma argument int liczba == 0 - zwróci do programu wartość 1.<br />
Wartość całkowita zwracana przez funkcję setjmp() przy pierwszym<br />
wywołaniu jest zawsze ZERO a przy następnych wywołaniach (po<br />
powrocie z procesu) jest równa parametrowi "int liczba"<br />
przekazanemu do ostatnio wywołanej funkcji longjmp().<br />
Przyjrzyjmy się temu mechanizmowi w praktyce. Wyobraźmy sobie,<br />
że chcemy realizować współbieżnie dwa procesy - proces1 i<br />
proces2. Proces pierwszy będzie naśladował w uproszczeniu<br />
wymieniony wyżej edytor tekstu - pozwoli na wprowadzanie tekstu,<br />
który będzie powtarzany na ekranie. Proces drugi będzie<br />
przesuwał w dolnej części ekranu swój numerek - cyferkę 2 (tylko<br />
po to, by było widać, że działa). Program główny wywołujący oba<br />
procesy powinien wyglądać tak:<br />
...<br />
void proces1(void);<br />
void proces2(void);<br />
int main(void)<br />
{<br />
clrscr();<br />
- 216-
proces1();<br />
proces2();<br />
return 0;<br />
}<br />
Ależ tu nie ma żadnej współbieżności! Oczywiście. Aby<br />
zrealizować współbieżność musimy zadeklarować bufor na bieżący<br />
stan rejestrów i zastosować funkcje setjmp():<br />
#include <br />
void proces1(void);<br />
void proces2(void);<br />
jmp_buf bufor1;<br />
int main(void)<br />
{<br />
clrscr();<br />
if(setjmp(bufor1) != 0) proces1(); //Powrót z procesu2 był?<br />
proces2();<br />
return 0;<br />
}<br />
Po wywołaniu funkcji setjmp() zostanie utworzony bufor1, w<br />
którym zostanie zapamiętany stan programu. Funkcja, jak zawsze<br />
przy pierwszym wywołaniu zwróci wartość ZERO, więc warunek<br />
if(setjmp(bufor1) != 0) ...<br />
nie będzie spełniony i proces1() nie zostanie wywołany. Program<br />
pójdzie sobie dalej i uruchomi proces2():<br />
void proces2(void)<br />
{<br />
for(;;)<br />
{<br />
gotoxy(10,20);<br />
printf("PROCES 2: ");<br />
for(int i = 1; i
clrscr();<br />
if(setjmp(bufor1)) proces1();<br />
proces2();<br />
return 0;<br />
}<br />
while(kbhit())<br />
{<br />
gotoxy(1,1);<br />
printf("PROCES1, Pisz tekst:<br />
gotoxy(pozycja,2);<br />
znak = getch();<br />
printf("%c", znak);<br />
pozycja++;<br />
}<br />
if(znak == '.') exit (0);<br />
}<br />
[Kropka - Koniec]");<br />
void proces2(void)<br />
{<br />
for(;;)<br />
{<br />
gotoxy(10,20);<br />
printf("PROCES 2: ");<br />
for(int i = 1; i
char znak;<br />
int pozycja = 1;<br />
int main(void)<br />
{<br />
clrscr();<br />
if(setjmp(bufor1)) proces1();<br />
if(setjmp(bufor2)) proces2();<br />
proces3();<br />
return 0;<br />
}<br />
void proces1(void)<br />
{<br />
while(kbhit())<br />
{<br />
gotoxy(1,1);<br />
printf("PROCES1, Pisz tekst: [Kropka - Koniec]");<br />
gotoxy(pozycja,2);<br />
znak = getch();<br />
printf("%c", znak);<br />
pozycja++;<br />
}<br />
if(znak == '.') exit (0);<br />
}<br />
void proces2(void)<br />
{<br />
for(;;)<br />
{<br />
gotoxy(10,20);<br />
printf("PROCES 2: ");<br />
for(int i = 1; i
longjmp(bufor2,2);<br />
}<br />
}<br />
Procesy odbywają się z różną prędkością. Kolejność uruchamiania<br />
procesów będzie:<br />
- proces3()<br />
- proces2()<br />
- proces1()<br />
Po uruchomieniu programu zauważysz, że proces pierwszy (pisania)<br />
został spowolniony. Można jednak temu zaradzić przez ustawienie<br />
flag i priorytetów. Jeśli dla przykładu uważamy, że pisanie jest<br />
ważniejsze, możemy wykrywać zdarzenie - naciśnięcie klawisza w<br />
każdym z mniej ważnych procesów i przerywać wtedy procesy mniej<br />
ważne. Wprowadzanie tekstu w przykładzie poniżej nie będzie<br />
spowolnione przez pozostałe procesy.<br />
[P077.CPP]<br />
#include <br />
#include <br />
#include <br />
#include <br />
#include <br />
void proces1(void);<br />
void proces2(void);<br />
void proces3(void);<br />
jmp_buf BuforStanu_1, BuforStanu_2;<br />
char znak;<br />
int pozycja = 1;<br />
int main(void)<br />
{<br />
clrscr();<br />
if(setjmp(BuforStanu_1)) proces1();<br />
if(setjmp(BuforStanu_2)) proces2();<br />
proces3();<br />
return 0;<br />
}<br />
void proces1(void)<br />
{<br />
while(kbhit())<br />
{<br />
gotoxy(1,1);<br />
printf("PROCES1, Pisz tekst: [Kropka - Koniec]");<br />
gotoxy(pozycja,2);<br />
znak = getch();<br />
- 221-
printf("%c", znak);<br />
pozycja++;<br />
}<br />
if(znak == '.') exit (0);<br />
}<br />
void proces2(void)<br />
{<br />
for(;;)<br />
{<br />
gotoxy(10,20);<br />
printf("PROCES 2: ");<br />
for(int i = 1; i
powrocie z procesu identyfikować - z którego procesu nastąpił<br />
powrót i podejmować stosowną decyzję np. przy pomocy instrukcji<br />
switch:<br />
switch(setjmp(bufor))<br />
{<br />
case 1 : proces2();<br />
case 2 : proces3();<br />
.....<br />
default : proces0();<br />
}<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
* Zmienne sterujące przełączaniem procesów powinny być zmiennymi<br />
globalnymi, bądź statycznymi. Także dane, które nie mogą ulec<br />
nadpisaniu bezpieczniej potraktować jako globalne.<br />
________________________________________________________________<br />
W przypadku wielu procesów celowe jest utworzenie listy, bądź<br />
kolejki procesów. Przydatny do tego celu bywa mechanizm tzw.<br />
"łańcuchowej referencji". W obiektach klasy PozycjaListy należy<br />
umieścić pole danych - strukturę i pointer do następnego<br />
procesu, któremu (zgodnie z ustalonym priorytetem) należy<br />
przekazać sterowanie:<br />
static jmp_buf Bufor[m];<br />
...<br />
class PozycjaListy<br />
{<br />
public:<br />
jmp_buf Bufor[n];<br />
PozycjaListy *nastepna;<br />
}<br />
Proces();<br />
LEKCJA 22. NA ZDROWY CHŁOPSKI ROZUM PROGRAMISTY.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się:<br />
* jak przyspieszać działanie programów w <strong>C++</strong><br />
* jakie dodatkowe narzędzia zyskujesz "przesiadając się" na<br />
nowoczesny kompilator <strong>C++</strong><br />
________________________________________________________________<br />
UNIKAJMY PĘTLI, które nie są NIEZBĘDNE !<br />
Unikanie zbędnych pętli nazywa się fachowo "rozwinięciem pętli"<br />
(ang. loop unrolling). Zwróć uwagę, że zastępując pętlę jej<br />
rozwinięciem (ang. in-line code):<br />
* zmniejszamy ilość obliczeń,<br />
* zmniejszamy ilość zmiennych.<br />
Wyobraźmy sobie pętlę:<br />
for (i = 0; i < max; i++)<br />
T[i] = i;<br />
Jeśli "unowocześnimy" ją tak:<br />
for (i = 0; i < max; )<br />
{<br />
T[i++] = i - 1;<br />
T[i++] = i - 1;<br />
}<br />
ilość powtórzeń pętli zmniejszy się dwukrotnie. Czai się tu<br />
jednak pewne niebezpieczeństwo: tablica może mieć NIEPARZYSTĄ<br />
liczbę elementów. Np. dla 3-elementowej tablicy (max = 3)<br />
nastąpiłyby w pierwszym cyklu operacje:<br />
i = 0;<br />
0 < 3 ? == TRUE --> T[0] = 0 // Tu nastepuje i++; //<br />
T[1] = 1 itd...<br />
To, co następuje w tak "spreparowanej" tablicy możesz<br />
prześledzić uruchamiając program:<br />
[P078.CPP]<br />
# include <br />
# include <br />
# include <br />
# define p(x) printf("%d\t", x)<br />
int T[99+1], i, max;<br />
main()<br />
- 225-
{<br />
cout > max;<br />
cout
zadziała szybciej niż<br />
for(j = 1; j < 1000; j++) (2)<br />
for(i = 1; i < 5; i++)<br />
{ A[i][j] = i + j; }<br />
W przypadku (1) zmienna robocza pętli wewnętrznej będzie<br />
inicjowana pięć razy, a w przypadku (2) - tysiąc (!) razy.<br />
Czasami zdarza się, że w programie można połączyć kilka pętli w<br />
jedną.<br />
for(i = 1; i < 5; i++)<br />
TAB_1[i] = i;<br />
...<br />
for(k = 0; k < 5; k++)<br />
TAB_2[k] = k;<br />
Zmniejsza to i ilość zmiennych, i tekst programu i czas pracy<br />
komputera:<br />
TAB_2[0] = 0;<br />
for(i = 1; i < 5; i++)<br />
TAB_1[i] = i;<br />
TAB_2[i] = i;<br />
Czasami wykonywanie pętli do końca pozbawione jest sensu.<br />
Przerwać pętlę w trakcie wykonywania można przy pomocy<br />
instrukcji break (jeśli pętle są zagnieżcżone, często lepiej<br />
użyć niepopularnego goto przerywającego nie jedną - a wszystkie<br />
pętle). Stosując umiejętnie break, continue i goto możesz<br />
zaoszczędzić swojemu komputerowi wiele pracy i czasu. Rutynowym<br />
"szkolno-strukturalnym" zapętlaniem programu<br />
main() {<br />
char gotowe = 0;<br />
...<br />
while (!gotowe)<br />
{<br />
znak = wybrano_z_menu();<br />
if (znak == 'q' || znak == 'Q') gotowe = 1;<br />
else<br />
.......<br />
gotowe = 1;<br />
}<br />
powodujesz często zupełnie niepotrzebne dziesiątki operacji,<br />
które już niczemu nie służą.<br />
char gotowe;<br />
main() {<br />
...<br />
while (!gotowe)<br />
{<br />
- 227-
znak = wybrano_z_menu();<br />
if (znak == 'q' || znak == 'Q') break; //Quit !<br />
else<br />
.......<br />
gotowe = 1;<br />
}<br />
Tym razem to, co następuje po else zostanie pominięte.<br />
Wskaźniki działają w <strong>C++</strong> szybciej, niż indeksy, stosujmy je w<br />
miarę możliwości w pętlach, przy manipulowaniu tablicami i w<br />
funkcjach.<br />
INSTRUKCJE STERUJĄCE I WYRAŻENIA ARYTMETYCZNE.<br />
Na "chłopski rozum" programisty wiadomo, że na softwarowych<br />
rozstajach, czyli na rozgałęzieniach programów<br />
prawdopodobieństwo wyboru każdwgo z wariantów działania programu<br />
z reguły bywa różne. Kolejność sprawdzania wyrażeń warunkowych<br />
nie jest zatem obojętna. Wyobraźmy sobie lekarza, który<br />
zwiezionego na toboganie narciarza pyta, czy ktoś w rodzinie<br />
chorował na żółtaczkę, koklusz, reumatyzm, podagrę, itp. zamiast<br />
zająć się najpierw wariantem najbardziej prawdopodobnym - czyli<br />
zagipsowaniem nogi nieszczęśnika. Absurdalne, prawda? Ale<br />
przecież (uderzmy się w piersi) nasze programy czasami postępują<br />
w taki właśnie sposób...<br />
NAJPIERW TO, CO NAJBARDZIE PRAWDOPODOBNE I NAJPROSTSZE.<br />
Jeśli zmienna x w naszym programie może przyjmować (równie<br />
prawdopodobne) wartości 1, 2, 3, 4, 5, to "przesiew"<br />
if (x >= 2) { ... }<br />
else if (x == 1) { ... }<br />
else { ... }<br />
okaże się w praktyce skuteczniejszy, niż<br />
if (x == 0) { ... }<br />
else if (x == 1) { ... }<br />
else { ... }<br />
Należy pamiętać, że w drabince if-else-if po spełnieniu<br />
pierwszego warunku - następne nie będą już analizowane.<br />
Zasada ta stosuje się także do wyrażeń logicznych, w których<br />
stosuje się operatory logiczne || (lub) i && (i). W wyrażeniach<br />
tych, których ocenę <strong>C++</strong> prowadzi tylko do uzyskania pewności,<br />
jaka będzie wartość logiczna (a nie koniecznie do końca<br />
wyrażenia) należy zastosować kolejność:<br />
- 228-
MAX || W1 || W2 || W3 ...<br />
MIN && W1 && W2 && W3 ...<br />
gdzie MAX - oznacza opcję najbardziej prawdopodobną, a MIN -<br />
najmniej prawdopodobną.<br />
Podobnie rzecz ma się z pracochłonnością (zatem i<br />
czso-chłonnością) poszczególnych wariantów. Jeśli wariant<br />
najprostszy okaże się prawdziwy, pozostałe możliwości możemy<br />
pominąć.<br />
NIE MNÓŻ I NIE DZIEL BEZ POTRZEBY.<br />
Prawa MATEMATYKI pozostają w mocy dla IBM PC i pozostaną zawsze,<br />
nawet dla zupełnie nieznanych nam komputerów, które skonstruują<br />
nasze dzieci i wnuki. Znajomość praw de Morgana i zasad<br />
arytmetyki jest dla programisty wiedzą niezwykle przydatną. Jako<br />
próbkę zapiszmy kilka trywialnych tożsamości przetłumaczonych na<br />
<strong>C++</strong>:<br />
2 * a == a + a == a 1, ale to nie zawsze<br />
prawda. Przesunięcie w prawo liczb nieparzystych spowoduje<br />
obcięcie części ułamkowej. W przypadku wyrażeń logicznych:<br />
(x && y) || (x && z) == x && (y || z)<br />
(x || y) && (x || z) == x || (y && z)<br />
W arytmetycznej sumie i iloczynie NIE MA takiej symetrii.<br />
!x && !y == !(x || y)<br />
!x || !y == !(x && y)<br />
Jeśli w skomplikowanych wyrażeniach arytmetycznych i logicznych<br />
zastosujemy zasady arytmetyki i logiki, zwykle stają się krótsze<br />
i prostsze. Podobnie jak licząc na kartce, możemy zastosować<br />
zmienne pomocnicze do przechowywania często powtarzających się<br />
wyrażeń składowych. Wyrażenie<br />
wynik = (x * x) + (x * x);<br />
możemy przekształcić do postaci<br />
zm_pomocn = x * x;<br />
wynik = zm_pomocn
zoptymalizować. Jako przykład zastosujmy funkcję biblioteczną<br />
strcmp() (string compare - porównaj łańcuchy znaków). Porównanie<br />
łańcuchów<br />
if (strcmp(string1, string2) == 0) cout
stosując zamiast if-else operator ? : możemy to zapisać tak:<br />
(x == MAXIMUM) ? (x = 0) : (x++);<br />
Mnożenie jest zwykle trochę szybsze niż dzielenie. Zapis<br />
a = b / 10;<br />
można zatem zastąpić szybszym:<br />
a = b * .1;<br />
Jeśli mamy do czynienia ze stałą STALA, to zapis w programie<br />
y = x / STALA;<br />
--> y = x * (1.0 / STALA);<br />
z pozoru bzdurny spowoduje w większości implementacji<br />
wyznaczenie wartości mnożnika 1.0/STALA przez kompilator na<br />
etapie kompilacji programu (compile-time), a w ruchu (run-time)<br />
będzie obliczany iloczyn zamiast ilorazu.<br />
W programach często stosuje się flagi binarne (jest-nie ma). <strong>C++</strong><br />
stosujemy jako flagi zmienne typu int lub char a w Windows BOOL.<br />
Jeśli weźmiemy pod uwagę fakt, że operatory relacji generują<br />
wartości typu TRUE/FALSE, typowy zapis:<br />
if (a > b)<br />
Flaga = 1;<br />
else<br />
Flaga = 0;<br />
zastąpimy krótszym<br />
Flaga = (a > b);<br />
Taki krótszy zapis NIE ZAWSZE powoduje wygenerowanie szybszego<br />
kodu. Jest to zależne od specyfiki konkretnej implementacji.<br />
Jeśli natomiast uprościsz swój program tak:<br />
if (x > 1) a = 3; --> a = 3 * (x > 1);<br />
else a = 0;<br />
spowoduje to wyraźne spowolnienie programu (mnożenie trwa).<br />
Kompilator <strong>C++</strong> rozróżnia dwa rodzaje wyrażeń:<br />
* general expressions - wyrażenia ogólne - zawierające zmienne i<br />
wywołania funkcji, których wartości nie jest w stanie określić<br />
na etapie kompilacji i<br />
* constant expressions - wyrażenia stałe, których wartość można<br />
- 231-
wyznaczyć na etapie kompilacji.<br />
Zapis<br />
wynik = 2 * x * 3.14;<br />
możesz zatem przekształcić do postaci<br />
wynik = 2 * 3.14 * x;<br />
Kompilator przekształci to wyrażenia na etapie kompilacji do<br />
postaci<br />
wynik = 6.28 * x;<br />
co spowoduje zmniejszenie ilości operacji w ruchu programu. Aby<br />
ułatwić takie działanie kompilatora trzeba umieścić stałe obok<br />
siebie.<br />
- 232-
LEKCJA 23. Co nowego w <strong>C++</strong>?<br />
________________________________________________________________<br />
Z tej lekcji dowiesz się, jakie mechanizmy <strong>C++</strong> pozwalają na<br />
stosowanie nowoczesnego obiektowego i zdarzeniowego stylu<br />
programowania i co programy robią z pamięcią.<br />
________________________________________________________________<br />
W porównaniu z klasycznym C - <strong>C++</strong> posiada:<br />
* rozszerzony zestaw słów kluczowych (ang. keywords):<br />
** nowe słowa kluczowe <strong>C++</strong>:<br />
class - klasa,<br />
delete - skasuj (dynamicznie utworzony obiekt),<br />
friend - "zaprzyjaźnione" funkcje z dostępem do danych,<br />
inline - wpleciony (funkcje przeniesione w formie rozwiniętej<br />
do programu wynikowego),<br />
new - utwórz nowy obiekt,<br />
operator - przyporządkuj operatorowi nowe działanie,<br />
private - dane i funkcje prywatne klasy (obiektu), do których<br />
zewnętrzne funkcje nie mają prawa dostępu,<br />
protected - dane i funkcje "chronione", dostępne z<br />
ograniczeniami,<br />
public - dane i funklcje publiczne, dostępne bez ograniczeń,<br />
template - szablon,<br />
this - ten, pointer wskazujący bieżący obiekt,<br />
virtual - funkcja wirtualna, abstrakcyjna, o zmiennym<br />
działaniu.<br />
* nowe operatory (kilka przykładów już widzieliśmy), np.:<br />
> - pobierz ze strumienia wejściowego.<br />
* nowe typy danych:<br />
klasy,<br />
obiekty,<br />
abstrakcyjne typy danych (ang. ADT).<br />
* nowe zasady posługiwania się funkcjami:<br />
funkcje o zmiennej liczbie argumentów,<br />
funkcje "rozwijane" inline,<br />
funkcje wirtualne, itp.;<br />
Przede wszystkim (i od tego właśnie rozpoczniemy) zobaczymy<br />
funkcje o nowych możliwościach.<br />
ROZSZERZENIE C - FUNKCJE.<br />
Funkcje uzyskują w <strong>C++</strong> znacznie więcej możliwości. Przegląd<br />
rozpoczniemy od sytuacji często występującej w praktyce<br />
programowania - wykorzystywania domyślnych (ang. default)<br />
parametrów.<br />
FUNKCJE Z DOMYŚLNYMI ARGUMENTAMI.<br />
Prototyp funkcji w <strong>C++</strong> pozwala na podanie deklaracji domyślnych<br />
wartości argumentów funkcji. Jeśli w momencie wywołania funkcji<br />
- 233-
w programie jeden (lub więcej) argument (ów) zostanie pominięte,<br />
kompilator wstawi w puste miejsce domyślną wartość argumentu.<br />
Aby uzyskać taki efekt, prototyp funkcji powinien zostać<br />
zadeklarowany w programie np. tak:<br />
void Funkcja(int = 7, float = 1.234);<br />
Efekt takiego działania będzie następujący:<br />
Wywołanie w programie: Efekt:<br />
________________________________________________________________<br />
Funkcja(99, 5.127); Normalnie: Funkcja(99, 5.127);<br />
Funkcja(99); Funkcja(99, 1.234);<br />
Funkcja(); Funkcja(7, 1.234);<br />
________________________________________________________________<br />
[!!!] Argumentów może ubywać wyłącznie kolejno. Sytuacja:<br />
Funkcja(5.127);<br />
Funkcja(99);<br />
//ŹLE<br />
//DOBRZE<br />
jest w <strong>C++</strong> niedopuszczalna. Kompilator potraktuje liczbę 5.127<br />
jako pierwszy argument typu int i wystąpi konflikt.<br />
[P079.CPP]<br />
#include <br />
void fun_show(int = 1234, float = 222.00, long = 333L);<br />
main()<br />
{<br />
fun_show();<br />
fun_show(1);<br />
fun_show(11, 2.2);<br />
fun_show(111, 2.22, 3L);<br />
return 0;<br />
}<br />
// Trzy arg. domyslne<br />
// Pierwszy parametr<br />
// Dwa parametry<br />
// Trzy parametry<br />
void fun_show(int X, float Y, long Z)<br />
{<br />
cout
<strong>C++</strong> pozwala deklarować zmienne w dowolnym miejscu, z<br />
zastrzeżeniem, że deklaracja zmiennej musi nastąpić przed jej<br />
użyciem. Umieszczanie deklaracji zmiennych możliwie blisko<br />
miejsca ich użycia znacznie poprawia czytelność (szczególnie<br />
dużych "wieloekranowych") programów. Klasyczny sposób deklaracji<br />
zmiennych:<br />
int x, y, z;<br />
...<br />
main()<br />
{<br />
...<br />
z = x + y + 1;<br />
...<br />
}<br />
może zostać zastąpiony deklaracją w miejscu zastosowania (w tym<br />
np. wewnątrz pętli):<br />
main()<br />
{<br />
...<br />
for ( int i = 1; i
eturn 0;<br />
}<br />
void daj_x(void)<br />
{<br />
cout
switch (n)<br />
{<br />
case niewymowne: cout
suffix) pozwalające na określenie typu parametrów,<br />
* pozwala na tworzenie tzw. funkcji polimorficznych (kilka<br />
różnych funkcji o tej samej nazwie), itp.<br />
Zwykły C tego nie potrafi i nie robi. Dlatego też do<br />
wprowadzenia takiego podziału kompetencji należy czasem<br />
zastosować deklarację extern "C". Funkcja rand() w programie<br />
poniżej generuje liczbę losową.<br />
[P081.CPP]<br />
#include <br />
extern "C"<br />
{<br />
# include //Prototyp rand() w STDLIB.H<br />
}<br />
main()<br />
{<br />
cout
W programie przykładowym funkcje z STDLIB.H zostaną skompilowane<br />
przez kompilator C. Określenie trybu kompilacji deklaracją<br />
extern "C" jest umieszczane zwykle nie wewnątrz programu<br />
głównego a w dołączanych plikach nagłówkowych *.H. Jest to<br />
możliwość szczególnie przydatne, jeśli dysponujesz bibliotekami<br />
funkcji dla C a nie masz chęci, czasu, bądź możliwości<br />
przerabiania ich na wersję przystosowaną do wymagań <strong>C++</strong>. Drugi<br />
przykład poniżej zajmuje się sortowaniem krewnych przy pomocy<br />
funkcji C qsort().<br />
[P082.CPP]<br />
# include <br />
# include <br />
# include <br />
extern "C" int comp(const void*, const void*);<br />
main()<br />
{<br />
int max;<br />
for(;;)<br />
{<br />
cout > max;<br />
if( max > 0 && max < 7) break;<br />
cout
Program wykonuje następujące czynności:<br />
* deklaruje prototyp funkcji typu C,<br />
* deklaruje statyczną tablicę wskaźników do łańcuchów znakowych,<br />
* sortuje wskaźniki,<br />
* wyświetla posortowane łańcuchy znakowe,<br />
* definiuje funkcję comp() - porównaj,<br />
* wykorzystuje funkcję biblioteczną C - strcmp() - String<br />
Compare do porównania łańcuchów znaków.<br />
O PAMIĘCI.<br />
Program w <strong>C++</strong> dzieli dostępną pamięć na kilka obszarów o<br />
określonym z góry przeznaczeniu. Dla zaawansowanego programisty<br />
zrozumienie i efektywne wykorzystanie mechanizmów zarządzania<br />
pamięcią w <strong>C++</strong> może okazać się wiedzą wielce przydatną.<br />
Zaczniemy, jak zwykle od "elementarza".<br />
CO PROGRAM ROBI Z PAMIĘCIĄ.<br />
W klasycznym C najczęściej stosowanymi do zarządzania pamięcią<br />
funkcjami są:<br />
* malloc() - przyporządkuj pamięć,<br />
* farmalloc() - przyporządkuj odległą pamięć,<br />
* realloc() - przyporządkuj powtórnie (zmienioną) ilość pamięci,<br />
* calloc() - przydziel pamięć i wyzeruj,<br />
* free() - zwolnij pamięć.<br />
Pamięć dzielona jest w obszarze programu na następujące bloki:<br />
___________________<br />
niskie adresy --> Ngłówek programu I.<br />
Adres startowy<br />
KOD:<br />
Kod programu<br />
___________________<br />
Zmienne statyczne II.<br />
DANE: 1. Zainicjowane Zmienne globalne<br />
___________________<br />
Zmienne statyczne III.<br />
DANE: 2. Niezainicjowane Zmienne globalne<br />
___________________<br />
STERTA: (heap) W miarę potrzeby IV.<br />
rośnie w dół.<br />
Tu operują funkcje<br />
malloc(), free().<br />
___________________<br />
POLE NICZYJE: V.<br />
___________________<br />
W miarę potrzeby VI.<br />
STOS: (stack) rośnie w górę.<br />
wysokie adresy --> ___________________<br />
- 240-
W obszarze kodu (I.) znajdują się instrukcje. Na stosie<br />
przechowywane są:<br />
* zmienne lokalne,<br />
* argumenty przekazywane funkcji w momencie jej wywołania,<br />
* adresy powrotne dla funkcji (RET == CS:IP).<br />
Na stercie natomiast przy pomocy funkcji (a jak przekonamy się<br />
za chwilę - także operatorów <strong>C++</strong>) możemy przydzielać pamięć dla<br />
różnych obiektów tworzonych w czasie pracy programu (ang.<br />
run-time memory allocation) - np. tworzyć bufory dla łańcuchów,<br />
tablic, struktur itp.. Zwróć uwagę, że obszar V. - POLE NICZYJE<br />
może być w czasie pracy programu stopniowo udostępniany dla<br />
stosu (który rozrasta się "w górę"), albo dla sterty (która<br />
rozrasta się "w dół"). W przykładowym programie poniżej podano,<br />
w którym obszarze pamięci zostanie umieszczony dany element<br />
programu.<br />
# include <br />
int a;<br />
int b = 6;<br />
main()<br />
{<br />
char *Dane;<br />
...<br />
float lokalna;<br />
...<br />
Dane = malloc(16);<br />
...<br />
}<br />
// III.<br />
// II.<br />
// VI.<br />
// IV.<br />
OPERATORY new I delete.<br />
Operatory new i delete działają podobnie do pary funkcji<br />
malloc() - free(). Pierwszy przyporządkowuje - drugi zwalnia<br />
pamięć. Dokładniej rzecz biorąc<br />
- operator new może zostać zastosowany wraz ze wskaźnikiem do<br />
bloku danych określonego typu:<br />
* struktury danych,<br />
* tablicy, itp. (wkrótce zastosujemy go także w stosunku do<br />
klas i obiektów);<br />
- przyporządkowuje pamięć blokowi danych;<br />
- przypisuje począkowy adres bloku pamięci wskaźnikowi.<br />
- operator delete zwalnia pamięć przyporządkowaną poprzednio<br />
blokowi danych,<br />
Operatory new i delete mogą współdziałać z danymi wieloma typami<br />
danych (wcale nie tylko ze strukturami), jednakże rozpoczniemy<br />
do struktury Data zawierającej datę urodzenia mojej córki.<br />
[P083.CPP]<br />
- 241-
# include "iostream.h"<br />
struct Data<br />
{<br />
int dzien;<br />
int miesiac;<br />
int rok;<br />
};<br />
void main()<br />
{<br />
Data *pointer = new Data;<br />
/* Dekl. wskaznik do struct typu Data */<br />
/* Przydziel pamiec dla struktury */<br />
pointer -> miesiac = 11; // pole "miesiac" = 11<br />
pointer -> dzien = 3;<br />
pointer -> rok = 1979;<br />
cout
int *pointer = new int[3]; // Przydziel pamiec<br />
pointer[0] = 3;<br />
pointer[1] = 11;<br />
pointer[2] = 1979;<br />
// Tabl_bez_nazwy[0] - dzien<br />
// Tabl_bez_nazwy[1] - miesiac<br />
cout
Podaj wielkosc tablicy (1...100) --> 20<br />
TABLICA:<br />
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17<br />
18 19<br />
Podaj wielkosc tablicy (1...100) --> 100<br />
TABLICA:<br />
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17<br />
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33<br />
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49<br />
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66<br />
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82<br />
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99<br />
Skoro dynamiczne tablice o zmiennej wielkości "chodzą", możemy<br />
wykorzystać to w bardziej interesujący sposób.<br />
[P086.CPP]<br />
# include <br />
# include <br />
# include <br />
extern "C"<br />
{<br />
int Fporownaj(const void* x, const void* y)<br />
{<br />
return (strcmp(*(char **)x, *(char **)y));<br />
}<br />
}<br />
main()<br />
{<br />
cout > ilosc;<br />
char **pointer = new char *[ilosc];<br />
for (i = 0; i < ilosc; i++)<br />
{<br />
cout imie;<br />
if (strcmp(imie, "stop") == 0) break;<br />
else<br />
pointer[i] = new char[strlen(imie)+1];<br />
strcpy(pointer[i], imie);<br />
delete imie;<br />
}<br />
qsort(pointer, i, sizeof(char *), Fporownaj);<br />
- 244-
for (i = 0; i < ilosc; i++)<br />
cout ilosc;<br />
char **pointer = new char *[ilosc];<br />
for (i = 0; i < ilosc; i++)<br />
{<br />
cout imie;<br />
if (Fporown_string(imie, "stop") == 0) break;<br />
else<br />
- 245-
pointer[i] = new char[strlen(imie)+1];<br />
Fkopiuj_string(pointer[i], imie);<br />
delete imie;<br />
}<br />
/* w tym momencie i == ilosc */<br />
Fsortuj(pointer, i, sizeof(char *), Fporownaj);<br />
for (i = 0; i < ilosc; i++)<br />
cout
cout > Liczba;<br />
cout
# include <br />
# include <br />
static void Funkcja()<br />
{<br />
cout
LEKCJA 24. SKĄD WZIĘŁY SIĘ KLASY I OBIEKTY W <strong>C++</strong>.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, skąd w <strong>C++</strong> biorą się obiekty i<br />
jak z nich korzystać.<br />
________________________________________________________________<br />
Zajmiemy się teraz tym, z czego <strong>C++</strong> jest najbardziej znany -<br />
zdolnością posługiwania się obiektami. Główną zaletą<br />
programowania obiektowego jest wyższy stopień "modularyzacji"<br />
programów. "Mudularyzacja" jest tu rozumiana jako możliwość<br />
podziału programu na niemal niezależne fragmenty, które mogą<br />
opracowywać różne osoby (grupy) i które później bez konfliktów<br />
można łączyć w całość i uruchamiać natychmiast. <strong>C++</strong> powstał, gdy<br />
programy stały się bardzo (zbyt) długie. Możliwość skrócenia<br />
programów nie jest jednakże jedyną zaletą <strong>C++</strong>. W długich,<br />
rozbudowanych programach trudno spamiętać szczegóły dotyczące<br />
wszystkich części programu. Jeśli grupy danych i grupy funkcji<br />
uda się połączyć w moduły, do których można później sięgać, jak<br />
do pewnej odrębnej całości, znacznie ułatwia to życie<br />
programiście. Na tym, w pewnym uproszczeniu, polega idea<br />
programowania obiektowego.<br />
JAK STRUKTURY STAWAŁY SIĘ OBIEKTAMI.<br />
W <strong>C++</strong> struktury uzyskują "trochę więcej praw" niż w klasycznym<br />
C. Przykładowy program poniżej demonstruje kilka sposobów<br />
posługiwania się strukturą w <strong>C++</strong>.<br />
[P90.CPP]<br />
#include <br />
struct Data<br />
{<br />
int dzien;<br />
int miesiac;<br />
int rok;<br />
};<br />
Data NaszaStruktura = {3, 11, 1979}; //Inicjujemy strukture<br />
Data daty[16];<br />
//Tablca struktur<br />
Data *p = daty;<br />
//Wskaznik do tablicy<br />
void Fdrukuj(Data);<br />
//Prototyp funkcji<br />
int i; //Licznik automat. 0<br />
int main()<br />
{<br />
for (; i < 16; i++)<br />
{<br />
*(p + i) = NaszaStruktura;<br />
daty[i].rok += i;<br />
cout
cout
Data::Data(void)<br />
{<br />
dzien = 3;<br />
miesiac = 11;<br />
rok = 1979;<br />
}<br />
int main()<br />
{<br />
Data NStruktura;<br />
//Poczatkowa data - Konstruktor<br />
//Inicjujemy strukture<br />
cout
Data::Data(void)<br />
//Poczatkowa data - Konstruktor<br />
oznacza, że funkcja Data(void) nie pobiera od programu żadnych<br />
parametrów i tworzy (w pamięci komputera) strukturę typu Data.<br />
Takie dziwne funkcje konstruujące (inicjujące) strukturę (o czym<br />
dokładniej w dalszej części książki), nazywane w <strong>C++</strong><br />
konstruktorami nie zwracają do programu żadnej wartości. Zwróć<br />
uwagę, że konstruktory to specjalne funkcje, które:<br />
-- mają nazwę identyczną z nazwą typu własnej struktury,<br />
-- nie posiadają wyspecyfikowanego typu wartości zwracanej do<br />
programu,<br />
-- służą do zainicjowania w pamięci pól struktury,<br />
-- nie są wywoływane w programie w sposób jawny, lecz niejawnie,<br />
automatycznie.<br />
Podstawowym praktycznym efektem dodania do struktur funkcji<br />
stała się możliwość skutecznej ochrony danych zawartych na<br />
polach struktury przed dostępem funkcji z zewnątrz struktury.<br />
Przed dodaniem do struktury jej własnych wewnętrznych funkcji -<br />
wszystkie funkcje pochodziły z zewnątrz, więc "hermetyzacja"<br />
danych wewnątrz była niewykonalna. Zasady dostępu określa się w<br />
<strong>C++</strong> przy pomocy słów:<br />
public - publiczny, dostępny,<br />
protected - chroniony, dostępny z ograniczeniami,<br />
private - niedostępny spoza struktury.<br />
Przykładowy program poniżej demonstruje tzw. "hermetyzację"<br />
struktury (ang. encapsulation). W przykładzie poniżej:<br />
* definiujemy strukturę;<br />
* definiujemy funkcje;<br />
* przekazujemy i pobieramy dane do/od struktury typu Zwierzak.<br />
Zmienna int schowek powinna sugerować ukrytą przez strukturę i<br />
niedostępną dla nieuprawnionych funkcji część danych struktury a<br />
nie cechy anatomiczne zwierzaka.<br />
[STRUCT.CPP]<br />
# include "iostream.h"<br />
//UWAGA: schowek ma status private, jest niedostepny<br />
struct Zwierzak<br />
{<br />
private:<br />
int schowek; //DANE PRYWATNE - niedostepne<br />
public:<br />
void SCHOWAJ(int Xwe); //Funkcje dostepne zzewnatrz<br />
- 252-
int ODDAJ(void);<br />
};<br />
void Zwierzak::SCHOWAJ(int Xwe) //definicja funkcji<br />
{<br />
schowek = Xwe;<br />
}<br />
int Zwierzak::ODDAJ(void)<br />
{<br />
return (schowek);<br />
}<br />
main()<br />
{<br />
Zwierzak Ciapek, Azor, Kotek; // Struktury "Zwierzak"<br />
int Piggy; // zwykla zmienna<br />
Ciapek.SCHOWAJ(1);<br />
Azor.SCHOWAJ(22);<br />
Kotek.SCHOWAJ(-333);<br />
Piggy = -4444;<br />
cout
za chwilę - struktura Ciapek jest już właściwie obiektem, a typ<br />
danych Zwierzak jest już właściwie klasą obiektów. Wystarczy<br />
zamienić słowo "struct" na słowo "class".<br />
[CLASS.CPP]<br />
# include "iostream.h"<br />
//w klasach schowek ma status private AUTOMATYCZNIE<br />
//slowo private stalo sie zbedne<br />
class Zwierzak<br />
{<br />
int schowek;<br />
public:<br />
void SCHOWAJ(int Xwe); //Funkcje dostepne zzewnatrz<br />
int ODDAJ(void);<br />
};<br />
void Zwierzak::SCHOWAJ(int Xwe)<br />
{<br />
schowek = Xwe;<br />
}<br />
int Zwierzak::ODDAJ(void)<br />
{<br />
return (schowek);<br />
}<br />
main()<br />
{<br />
Zwierzak Ciapek, Azor, Kotek; // obiekty klasy "Zwierzak"<br />
int Piggy; // zwykla zmienna<br />
Ciapek.SCHOWAJ(1);<br />
Azor.SCHOWAJ(22);<br />
Kotek.SCHOWAJ(-333);<br />
Piggy = -4444;<br />
cout
Klasy służą do tworzenia formalnego typu danych. W przypadku<br />
klas wiadomo jednak "z definicji", że będzie to bardziej złożony<br />
typ (tzw. agregat) zawierający praktycznie zawsze i dane<br />
"tradycyjnych" typów i funkcje (nazywane "metodami"). Podobnie<br />
jak definiując strukturę tworzysz nowy formalny typ danych, tak<br />
i tu - definiując klasę tworzysz nowy typ danych. Jeśli<br />
zadeklarujesz użycie zmiennych danego typu formalnego, takie<br />
zmienne to właśnnie obiekty. Innymi słowy, klasy stanowią<br />
definicje formalnego typu, natomiast obiekty - to zmienne danego<br />
typu (danej klasy).<br />
Zamiast słowa struct stosujemy przy klasach słowo class.<br />
class Klasa<br />
{<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
void Inicjuj(void);<br />
int Funkcja(int arg);<br />
};<br />
Nasza pierwsza świadomie tworzona klasa nazywa się "Klasa" i<br />
stanowi nowy formalny typ zmiennych. Jeśli zadeklarujesz zmienną<br />
takiej klasy (tego typu formalnego), to taka zmienna będzie<br />
właśnie OBIEKTEM.<br />
Nasza pierwsza prawdziwa Klasa zawiera dane:<br />
prywatna_tab[80] - prywatną tablicę;<br />
dane - publiczną daną prostą typu int;<br />
oraz funkcje:<br />
Inicjuj() - zainicjuj - utwórz obiekt danej klasy w pamięci;<br />
Funkcja() - jakaś funkcja publiczna.<br />
Gdyby była to zwykła struktura, jej definicja w programie<br />
wyglądałaby tak:<br />
struct Klasa<br />
{<br />
private:<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
};<br />
void Inicjuj(void);<br />
int Funkcja(int arg);<br />
Jeżeli w dalszej części programu chcielibyśmy zastosować<br />
- 255-
struktury takiego typu, deklaracja tych struktur musiałaby<br />
wyglądać tak:<br />
struct rodzaj_struktur<br />
{<br />
private:<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
void Inicjuj(void);<br />
int Funkcja(int arg);<br />
} str1, str2, .... , nasza_struktura;<br />
bądź tak:<br />
struct rodzaj_struktur<br />
{<br />
private:<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
void Inicjuj(void);<br />
int Funkcja(int arg);<br />
};<br />
...<br />
(struct) rodzaj_struktur str1, str2, .... , nasza_struktura;<br />
Słowo kluczowe struct jest opcjonalne. Moglibyśmy więc<br />
zadeklarować strukturę w programie, wewnątrz funkcji main():<br />
struct rodzaj_struktur<br />
{<br />
private:<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
void Inicjuj(void);<br />
int Funkcja(int arg);<br />
};<br />
main()<br />
{<br />
...<br />
struct rodzaj_struktur nasza_struktura;<br />
//lub równoważnie:<br />
rodzaj_struktur nasza_struktura;<br />
Do pól struktury możemy odwoływać się przy pomocy operatora<br />
kropki (ang. dot operator). Podobnie dzieje się w przypadku<br />
klas. Jeśli zadeklarujemy zmienną typu Klasa, to ta zmienna<br />
będzie naszym pierwszym obiektem.<br />
class Klasa<br />
{<br />
int prywatna_tab[80]<br />
- 256-
public:<br />
int dane;<br />
void Inicjuj(void)<br />
int Funkcja(int our_param);<br />
} Obiekt;<br />
Podobnie jak wyżej, możemy zadeklarować nasz obiekt wewnątrz<br />
funkcji main():<br />
class Klasa<br />
{<br />
int prywatna_tab[80]<br />
public:<br />
int dane;<br />
void Inicjuj(void)<br />
int Funkcja(int argument);<br />
};<br />
main()<br />
{<br />
...<br />
Klasa Obiekt;<br />
...<br />
Przypiszemy elementom obiektu wartości:<br />
main()<br />
{<br />
...<br />
Klasa Obiekt;<br />
Obiekt.dane = 13;<br />
...<br />
Taką samą metodą, jaką stosowaliśmy do danych - pól struktury,<br />
możemy odwoływać się do danych i funkcji w klasach i obiektach.<br />
main()<br />
{<br />
...<br />
Klasa Obiekt;<br />
Obiekt.dane = 13;<br />
...<br />
Obiekt.Funkcja(44);<br />
Przyporządkowaliśmy obiektowi nie tylko dane, ale także funkcje<br />
poprzez umieszczenie prototypów funkcji wewnątrz deklaracji<br />
klasy:<br />
class Klasa<br />
{<br />
...<br />
public:<br />
...<br />
void Inicjuj(void) /* Prototypy funkcji */<br />
int Funkcja(int argument);<br />
};<br />
- 257-
[!!!] UWAGA!<br />
________________________________________________________________<br />
W <strong>C++</strong> nie możemy zainicjować danych wewnątrz deklaracji klasy:<br />
class Klasa<br />
{<br />
private:<br />
int prywatna_tab[80] = { 1, 2, ... }; //ŹLE !<br />
public:<br />
int dane = 123; //ŹŁE !<br />
...<br />
________________________________________________________________<br />
Inicjowanie danych odbywa się w programie głównym przy pomocy<br />
przypisania (dane publiczne), bądź za pośrednictwem funkcji<br />
należącej do danej klasy i mającej dostęp do wewnętrznych danych<br />
klasy/obiektu (dane prywatne). Inicjowania danych mogą dokonać<br />
także specjalne funkcje - tzw. konstruktory.<br />
Dane znajdujące się wewnątrz deklaracji klasy mogą mieć status<br />
public, private, bądź protected. Dopóki nie zażądasz inaczej -<br />
domyślnie wszystkie elementy klasy mają status private. Jeżeli<br />
część obiektu jest prywatna, to oznacza, że żaden element<br />
programu spoza obiektu nie ma do niej dostępu. W naszej Klasie<br />
prywatną część stanowi tablica złożona z liczb całkowitych:<br />
(default - private:) int prywatna_tab[80];<br />
Do (prywatnych) elementów tablicy dostęp mogą uzyskać tylko<br />
funkcje związane (ang. associated) z obiektem danej klasy.<br />
Funkcje takie muszą zostać zadeklarowane wewnątrz definicji<br />
danej klasy i są nazywane członkami klasy - ang. member<br />
functions. Funkcje mogą mieć status private i stać się dzięki<br />
temu wewnętrznymi funkcjami danej klasy (a w konsekwencji<br />
również prywatnymi funkcjami obiektów danej klasy). Jest to<br />
jedna z najważniejszych cech nowoczesnego stylu programowania w<br />
<strong>C++</strong>. Na tym polega idea hermetyzacji danych i funkcji wewnątrz<br />
klas i obiektów. Gdyby jednak cała zawartość (i dane i funkcje)<br />
znajdujące się w obiekcie zostały dokładnie "zakapsułkowane", to<br />
okazałoby się, że obiekt stał się "ślepy i głuchy", a w<br />
konsekwencji - niedostępny i kompletnie nieużyteczny dla<br />
programu i programisty. Po co nam obiekt, do którego nie możemy<br />
odwołać się z zewnątrz żadną metodą? W naszym obiekcie, w<br />
dostępnej z zewnątrz części publicznej zadeklarowaliśmy zmienną<br />
całkowitą dane oraz dwie funkcje - Inicjuj() oraz Funkcja().<br />
Jeśli dane i funkcje mają status public, to oznacza, że możemy<br />
się do nich odwołać z dowolnego miejsca programu i dowolnym<br />
sposobem. Takie odwołania przypominają sposób odwoływania się do<br />
elementów struktury:<br />
main()<br />
- 258-
{<br />
...<br />
Obiekt.dane = 5;<br />
Obiekt.Inicjuj();<br />
...<br />
Obiekt.Funkcja(3);<br />
//Przypisanie wartości zmiennej.<br />
//Wywołanie funkcji Inicjuj()<br />
//Wywołanie funkcji z argumentem<br />
[!!!] ZAWSZE PUBLIC !<br />
________________________________________________________________<br />
Dane zawarte w obiekcie, podobnie jak zwykłe zmienne wymagają<br />
zainicjowania. Funkcja inicjująca dane - zawartość obiektu musi<br />
zawsze posiadać status public aby mogła być dostępna z zewnątrz<br />
i zostać wywołana w programie głównym - funkcji main(). Funkcje<br />
i dane dostępne z zewnątrz stanowią tzw. INTERFEJS OBIEKTU.<br />
________________________________________________________________<br />
- 259-
LEKCJA 25. PRZYKŁAD OBIEKTU.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak praktycznie projektuje się<br />
klasy i obiekty. Twój pierwszy obiekt zacznie działać.<br />
________________________________________________________________<br />
Nasz pierwszy, doświadczalny obiekt będzie zliczać ile razy<br />
użytkownik nacisnął określony klawisz - np. literę "A". Najpierw<br />
podejdziemy do problemu "klasycznie". Utworzymy strukturę<br />
Licznik, którą można wykorzystać do przechowywania istotnych dla<br />
nas informacji:<br />
char znak - znak do zliczania<br />
int ile - ile razy wystąpił dany znak.<br />
Zwróć uwagę, że Licznik oznacza tu typ struktur (nowy formalny<br />
typ danych) a licznik oznacza naszą roboczą zmienną danego typu.<br />
struct Licznik //Licznik - nowy typ struktur<br />
{<br />
public: //Status public jest domyślny dla struktur<br />
char znak;<br />
int ile;<br />
...<br />
} licznik; //Zmienna typu "Licznik"<br />
Do pól struktury licznik.znak i licznik.ile możemy odwoływać się<br />
w programie w następujący sposób:<br />
//Przypisanie (zainicjowanie pola struktury)<br />
licznik.znak = 'A';<br />
cin >> licznik.znak;<br />
//Odczyt (wyprowadzenie) bież. zawartości pola struktury.<br />
cout
Aby funkcja inicjująca pole struktury zadziałała prawidłowo, jej<br />
definicja powinna wyglądać tak:<br />
void Inicjuj(char x)<br />
{<br />
licznik.znak = x;<br />
licznik.ile = 0;<br />
}<br />
//Deklaracja zmiennej znak.<br />
//x - wewnętrzna zmienna funkcji<br />
Inicjując strukturę licznik funkcja zeruje pole "ile" struktury.<br />
Przyda nam się jeszcze jedna funkcja PlusJeden(). Ta funkcja<br />
powinna zwiększyć zmienną służącą do zliczania ile razy wystąpił<br />
interesujący nas znak po każdym pojawieniu się odpowiedniego<br />
znaku (w tym przypadku "A").<br />
void PlusJeden(void) //Definicja funkcji<br />
{ //incrementującej licznik<br />
licznik.ile++;<br />
}<br />
Zbudowaliśmy licznik, który składa się z danych rozmieszczonych<br />
na polach struktury oraz dwu stowarzyszonych ze strukturą<br />
funkcji. Jeśli spróbujemy zastosować to w programie, gdzie:<br />
char znak_we - znak wczytany z klawiatury;<br />
program będzie wyglądać tak:<br />
void main()<br />
{<br />
char znak_we;<br />
Inicjuj('A');<br />
cout znak_we;<br />
if (znak_we == 'k' || znak_we == 'K') break;<br />
if(znak_we == licznik.znak) PlusJeden();<br />
}<br />
....<br />
W tekście mogą wystąpić zarówno duże jak i małe litery. Jeśli<br />
zechcemy zliczać i jedne i drugie, możemy posłużyć się funkcją<br />
biblioteczną C zamieniającą małe litery na duże - toupper().<br />
Najpierw poddamy wczytany zank konwersji a następnie porównamy z<br />
"zadanym" na polu licznik.znak:<br />
if(licznik.znak == toupper(znak_we)) PlusJeden();<br />
- 261-
Po przerwaniu pętli przez użytkownika wystarczy sprawdzić jaka<br />
wartość jest wpisana w polu licznik.ile i możemy wydrukować<br />
wynik zliczania wystąpień litery 'A' we wprowadzonym tekście.<br />
cout
obiekt. Zawartość naszego obiektu powinna wyglądać tak:<br />
Dane:<br />
char znak;<br />
int ile;<br />
Funkcje:<br />
void Inicjuj(char);<br />
void PlusJeden(void);<br />
Łączymy w całość funkcje operujące pewnymi danymi i te właśnnie<br />
dane. Co więcej, jeśli zaistnieją takie funkcje, które nie będą<br />
wykorzystywane przez nikogo więcej poza własnym obiektem i poza<br />
jego składnikami: funkcją Inicjuj() i funkcją PlusJeden(),<br />
funkcje te nie muszą być widoczne, ani dostępne dla reszty<br />
programu. Takie funkcje mogą wraz z danymi zostać uznane za<br />
prywatną część obiektu. Takie praktyki, szczególnie w programach<br />
przeznaczonych dla środowiska Windows są uzasadnione i<br />
pożyteczne. Rozważmy obiekt, modularyzację i hermetyzację<br />
obiektu na konkretnych przykładach.<br />
Zacznijmy od zdefiniowania klasy.<br />
class Licznik<br />
{<br />
char znak;<br />
int ile;<br />
public:<br />
void Inicjuj(char);<br />
void PlusJeden(void);<br />
};<br />
Następny krok, to zdefiniowanie obu funkcji. Zwróć uwagę, że<br />
funkcje nie są już definiowane "niezależnie", lecz w stosunku do<br />
własnej klasy:<br />
void Licznik::Inicjuj(char x)<br />
{<br />
znak = x;<br />
ile = 0;<br />
}<br />
void Licznik::PlusJeden(void)<br />
{<br />
ile++;<br />
}<br />
Skoro funkcje widzą już wyłącznie własną klasę, zapis<br />
licznik.znak może zostać uproszczony do --> znak<br />
i<br />
licznik.ile do --> ile<br />
Aby wskazać, że funkcje są członkami klasy Licznik stosujemy<br />
- 263-
operator :: (oper. widoczności/przesłaniania - ang. scope<br />
resolution operator). Taki sposób zapisu definicji funkcji<br />
oznacza dla <strong>C++</strong>, że funkcja jest członkiem klasy (ang. member<br />
function). Logika <strong>C++</strong> w tym przypadku wygląda tak:<br />
* Prototypy funkcji należy umieścić w definicji klasy.<br />
* Definicje funkcji mogą znajdować się w dowolnym miejscu<br />
programu, ponieważ operator przesłaniania :: pozwala rozpatrywać<br />
klasę podobnie jak zmienne globalne.<br />
* Wstawiając operator :: pomiędzy nazwę klasy i prototyp funkcji<br />
informujemy <strong>C++</strong> że dana funkcja jest członkiem określonej klasy.<br />
Funkcje - członkowie klas nazywane są często METODAMI.<br />
Definicje klas i definicje funkcji - METOD są często umieszczane<br />
razem - w plikach nagłówkowych. Jeśli posługujemy się taką<br />
metodą, wystarczy dołączyć odpowiedni plik dyrektywą # include.<br />
Kompilator <strong>C++</strong> skompiluje wtedy automatycznie wszystkie funkcje,<br />
które znajdzie w dołączonych plikach nagłówkowych.<br />
Możemy przystąpić do utworzenia programu.<br />
main()<br />
{<br />
char znak_we;<br />
Licznik licznik;<br />
licznik.Inicjuj('A');<br />
...<br />
//Dekl. zwyklej zmiennej<br />
//Deklarujemy obiekt klasy Licznik<br />
//Inicjujemy licznik<br />
Możemy teraz określić ilość wprowadzonych z klawiatury liter 'A'<br />
oraz 'a' i wyprowadzić ją na ekran monitora. Pojawia się jednak<br />
pewien problem. Nie uda się sięgnąć z zewnątrz do prywatnych<br />
danych obiektu tak, jak poprzednio:<br />
if(licznik.znak == toupper(znak_we)) ....<br />
Potrzebna nam będzuie jeszcze jedna metoda autoryzowana do<br />
dostępu do danych obiektu:<br />
char Licznik::Pokaz(void);<br />
która nie będzie w momencie wywołania pobierać od programu<br />
żadnych argumentów (void), natomiast pobierze znak z pola char<br />
Licznik.znak i przekaże tę informację w postaci zmiennej typu<br />
char do programu. Definicja takiej metody powinna być<br />
następująca:<br />
char Licznik::Pokaz(void)<br />
- 264-
{<br />
return znak;<br />
}<br />
Ten sam problem wystąpi przy próbie pobrania od obiektu efektów<br />
jego pracy - stanu pola licznik.ile. Do tego też niezbędna jest<br />
autoryzowana do dostępu metoda. Nazwiemy ją Efekt():<br />
int Licznik::Efekt(void)<br />
{<br />
return ile;<br />
}<br />
Program w wersji obiektowej będzie wyglądać tak:<br />
[P093.CPP]<br />
# include <br />
# include <br />
class Licznik<br />
{<br />
char znak;<br />
int ile;<br />
public:<br />
void Inicjuj(char);<br />
void PlusJeden(void);<br />
char Pokaz(void);<br />
int Efekt(void);<br />
};<br />
void main()<br />
{<br />
char znak_we;<br />
Licznik licznik;<br />
licznik.Inicjuj('A');<br />
cout
void Licznik::Inicjuj(char x)<br />
{<br />
znak = x;<br />
ile = 0;<br />
}<br />
void Licznik::PlusJeden(void)<br />
{<br />
ile++;<br />
}<br />
char Licznik::Pokaz(void)<br />
{<br />
return znak;<br />
}<br />
int Licznik::Efekt(void)<br />
{<br />
return ile;<br />
}<br />
Przejdziemy teraz do bardziej szczegółowego omówienia<br />
zasygnalizowanego wcześniej problemu inicjowania struktur w<br />
pamięci przy pomocy funkcji o specjalnym przeznaczeniu - tzw.<br />
KONSTRUKTORÓW.<br />
- 266-
LEKCJA 26. CO TO JEST KONSTRUKTOR.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, w jaki sposób w pamięci<br />
komputera są tworzone obiekty.<br />
________________________________________________________________<br />
<strong>C++</strong> zawiera specjalną kategorię funkcji - konstruktory w celu<br />
automatyzacji inicjowania struktur (i obiektów). Konstruktory to<br />
specjalne funkcje będące członkami struktur (kategorii member<br />
functions) które są automatycznie wywoływane i dokonują<br />
zainicjowania struktury zgodnie z naszymi życzeniami, po<br />
napotkaniu w programie pierwszej deklaracji struktury/obiektu<br />
danego typu.<br />
PRZYKŁADOWY KONSTRUKTOR.<br />
Struktura Licznik zawiera funkcję inicjującą obiekt (niech<br />
obiekt będzie na razie zmienną typu struktura):<br />
struct Licznik //Typ formalny struktur<br />
{<br />
char znak;<br />
int ile;<br />
} licznik; //Przykladowa struktura<br />
void Inicjuj(char x)<br />
{<br />
licznik.znak = x;<br />
licznik.ile = 0;<br />
}<br />
//Funkcja inicjująca<br />
Zdefiniujmy naszą strukturę w sposób bardziej<br />
"klasowo-obiektowy":<br />
struct Licznik<br />
{<br />
private:<br />
char znak;<br />
int ile;<br />
public:<br />
void Inicjuj(char);<br />
void PlusJeden(void);<br />
};<br />
Funkcja Inicjuj() wykonuje takie działanie jakie może wykonać<br />
konstruktor struktury (obiektu), z tą jednak różnicą, że<br />
konstruktor jest wywoływany automatycznie. Jeśli wyposażymy<br />
strukturę Licznik w konstruktor, to funkcja Inicjuj() okaże się<br />
zbędna. Aby funkcja Inicjuj() stała się konstruktorem, musimy<br />
zmienić jej nazwę na nazwę typu struktury, do której konstruktor<br />
ma należeć. Zwróć uwagę, że konstruktor, w przeciwieństwie do<br />
innych, "zwykłych" funkcji nie ma podanego typu wartości<br />
zwracanej:<br />
- 267-
struct Licznik<br />
{<br />
private:<br />
char znak;<br />
int ile;<br />
public:<br />
Licznik(void); //Konstruktor nie pobiera argumentu<br />
void PlusJeden(void);<br />
};<br />
Teraz powinniśmy zdefiniować konstruktor. Zrobimy to tak, jak<br />
wcześniej definiowaliśmy funkcję Inicjuj().<br />
Licznik::Licznik(void) //Konstruktor nie pobiera argumentu<br />
{<br />
ile = 0;<br />
}<br />
Jeśli formalny typ struktur (klasa) posiada kostruktor, to po<br />
rozpoczęciu programu i napotkaniu deklaracji struktur danego<br />
typu konstruktor zostanie wywołany automatycznie. Dzięki temu<br />
nie musimy "ręcznie" inicjować struktur na początku programu.<br />
Jednakże nasz przykładowy konstruktor nie załatwia wszystkich<br />
problemów - nie ustawia w strukturze zmiennej (pola) int znak -<br />
określającego, który znak powinien być zliczany w liczniku. W<br />
tak zainicjowanej strukturze zmienna ile jest zerowana, ale<br />
zawartość pola znak pozostaje przypadkowa. Niby wszystko w<br />
porządku, ale wygląda to niesolidnie. Czy nie możnaby przekazać<br />
parametru przy pomocy konstruktora? Można! Konstruktor<br />
"bezparametrowy"<br />
Licznik::Licznik(void)<br />
taki, jak powyżej to tylko szczególny przypadek - tzw.<br />
konstruktor domyślny (ang. default constructor).<br />
PRZEKAZYWANIE ARGUMENTÓW DO KOSTRUKTORA.<br />
Czasem chcemy zainicjować nową strukturę już z pewnymi<br />
ustawionymi parametrami. Te początkowe parametry struktury<br />
możemy przekazać jako argumenty konstruktora.<br />
struct Licznik<br />
{<br />
private:<br />
char znak;<br />
int ile;<br />
public:<br />
Licznik(char); //Konstruktor z argumentem typu char<br />
void PlusJeden(void);<br />
};<br />
Licznik::Licznik(char x) //Konstruktor z jednym argumentem<br />
- 268-
{<br />
...<br />
}<br />
main()<br />
{<br />
Licznik licznik('A'); //Deklaracja struktury typu Licznik<br />
// oznacza to automatyczne wywołanie konstruktora z argumentem<br />
....<br />
Poniewż nowy konstruktor pobiera od programu argument typu<br />
znakowego char, więc i definicję konstruktora należy zmienić:<br />
Licznik::Licznik(char x) //Konstruktor z jednym argumentem<br />
{<br />
ile = 0;<br />
znak = x;<br />
}<br />
Jeśli parametrów jest więcej niż jeden, możemy je przekazać do<br />
konstruktora, a konstruktor wykorzysta je do zainicjowania<br />
struktury w następujący sposób:<br />
struct Sasiedzi //sąsiedzi<br />
{<br />
private:<br />
char Tab_imion[4];<br />
...<br />
public:<br />
Sasiedzi(char *s1, char *s2, char *s3, char s4);<br />
...<br />
};<br />
main()<br />
{<br />
Sasiedzi chopy("Helmut", "Ulrich", "Adolf", "Walter");<br />
....<br />
Przekazanie konstruktorowi argumentów i w efekcie automatyczne<br />
ustawiamie przez konstruktor paramatrów struktury już w momencie<br />
zadeklarowania struktury w programie rozwiązuje wiele problemów.<br />
W <strong>C++</strong> istnieje jednakże pewne dość istotne ograniczenie - nie<br />
możemy zadeklarować tablicy złożonej z obiektów posiadających<br />
konstruktory, chyba że wszystkie konstruktory są bezparametrowe<br />
(typu default constructors).<br />
Udoskonalmy teraz nasz program zliczający wystąpienia w tekście<br />
litery a posługując się konstruktorem struktury.<br />
[P094.CPP] /* Wersja ze strukturą */<br />
# include <br />
# include <br />
- 269-
struct Licznik<br />
{<br />
private:<br />
char znak;<br />
int ile;<br />
public:<br />
Licznik(char);<br />
void PlusJeden(void);<br />
char Pokaz(void);<br />
int Efekt(void);<br />
};<br />
Licznik::Licznik(char x)<br />
{<br />
znak = x;<br />
ile = 0;<br />
}<br />
void main()<br />
{<br />
Licznik licznik('A');<br />
//Konstruktor<br />
//Def. konstruktora<br />
//Zainicjowanie przez konstruktor<br />
cout
[P095.CPP] /* Wersja z klasą i obiektem */<br />
# include <br />
# include <br />
class Licznik<br />
{<br />
char znak;<br />
int ile;<br />
public:<br />
Licznik(char);<br />
void PlusJeden(void);<br />
char Pokaz(void);<br />
int Efekt(void);<br />
};<br />
//Konstruktor<br />
Licznik::Licznik(char x) //Def. konstruktora<br />
{<br />
znak = x;<br />
ile = 0;<br />
}<br />
void main()<br />
{<br />
Licznik licznik('A');<br />
//Zainicjowanie obiektu licznik<br />
cout
do skasowania obiektu możemy zastosować tzw. desruktor (ang.<br />
destructor). Nazwy konstruktora i destruktora są identyczne z<br />
nazwą macieżystego typu struktur (macieżystej klasy), z tym, że<br />
nazwa destruktora poprzedzona jest znakiem "~" (tylda).<br />
CO TO JEST DESTRUKTOR.<br />
Specjalna funkcja - destruktor (jeśli zadeklarujemy zastosowanie<br />
takiej funkcji) jest wywoływana automatycznie, gdy program<br />
zakończy korzystanie z obiektu. Konstruktor towrzy, a destruktor<br />
(jak sama nazwa wskazuje) niszczy strukturę (obiekt) i zwalnia<br />
przyporządkowaną pamięć. Przykład poniżej to program<br />
manipulujący stosem, rozbudowany tak, by zawierał i konstruktor<br />
i destruktor struktury (obiektu). Zorganizujmy zarządzanie<br />
pamięcią przeznaczoną dla stosu w taki sposób:<br />
struct Stos<br />
{<br />
private:<br />
int *bufor_danych;<br />
int licznik;<br />
public:<br />
Stos(int ile_RAM);<br />
int Pop(int *ze_stosu);<br />
int Push(int na_stos);<br />
};<br />
/* Konstruktor<br />
gdzie:<br />
*bufor_danych - wskaźnik do bufora (wypełniającego rolę stosu),<br />
licznik - wierzchołek stosu, jeśli == -1, stos jest pusty.<br />
Stos::Stos(...) - konstruktor inicjujący strukturę typu Stos<br />
(lub obiekt klasy Stos),<br />
ile_RAM - ilość pamięci potrzebna do poprawnego działanie stosu,<br />
*ze_stosu - wskaźnik do zmiennej, której należy przypisać<br />
wartość zdjętą właśnie ze stosu,<br />
na_stos - liczba przeznaczona do zapisu na stos.<br />
Zajmijmy się teraz definicją konstruktora. Wywołując konstruktor<br />
w programie (deklarując użycie w programie struktury typu Stos)<br />
przekażemy mu jako argument ilość potrzebnej nam pamięci RAM w<br />
bajtach. Do przyporządkowznia pamięci na stercie dla naszego<br />
stosu wykorzystamy funkcję malloc().<br />
Stos::Stos(int n_RAM)<br />
//Konstruktor - def.<br />
{<br />
licznik = -1;<br />
bufor_danych = (int *) malloc(n_RAM);<br />
}<br />
Posługując się funkcją malloc() przyporządkowujemy buforowi<br />
danych, w oparciu o który organizujemy nasz obiekt (na razie w<br />
formie struktury) - stos 100 bajtów pamięci, co pozwala na<br />
- 272-
ozmieszczenie 50 liczb typu int (po 2 bajty każda). Liczbę<br />
potrzebnych bajtów pamięci - 100 przekazujemy jako argument<br />
konstruktorowi w momencie deklaracji struktury typu Stos. Nasza<br />
struktura w programie będzie się nazywać nasz_stos.<br />
main()<br />
{<br />
...<br />
Stos nasz_stos(100);<br />
...<br />
Kiedy wykorzystamy naszą strukturę w programie, możemy zwolnić<br />
pamięć przeznaczoną dla struktury posługując się funkcją<br />
biblioteczną C free(). Przykład przydziału pamięci przy pomocy<br />
pary operatorów new - delete już był, przedstawimy tu zatem<br />
tradycyjną (coraz rzadziej stosowaną metodę) opartą na<br />
"klasycznych" funkcjach z biblioteki C. Funkcją free() posłużymy<br />
się w destruktorze struktury nasz_stos - ~Stos(). Destruktory są<br />
wywoływane automatycznie, gdy kończy się działanie programu, lub<br />
też, gdy struktura (obiekt) przestaje być widoczna / dostępna w<br />
programie. Obiekt (struktura) przestaje być widoczny (podobnie<br />
ja zwykła zmienna lokalna/globalna), jeśli opuszczamy tę<br />
funkcję, wewnątrz której obiekt został zadeklarowany. Jest to<br />
właściwość bardzo ważna dla naszego przykładowego stosu. W<br />
naszym programie przykładowym pamięć przydzielona strukturze<br />
stack pozostaje zarezerwowana "na zawsze", nawet wtedy, gdy nasz<br />
stos przestaje być "widoczny" (ang. out of scope). Obiekt może<br />
przestać być widoczny np. wtedy, gdy działa funkcja "nie<br />
widząca" obiektu. Idąc dalej tym torem rozumowania, jeśli<br />
destruktor zostanie wywołany automatycznie zawsze wtedy, gdy<br />
obiekt przestanie być widoczny, istnienie destruktora w<br />
definicji typu struktur Stos pozwala na automatyczne wyzerowanie<br />
stosu. Deklarujemy destruktor podobnie do konstruktora, dodając<br />
przed nazwą destruktora znak ~ (tylda):<br />
struct Stos<br />
{<br />
...<br />
public:<br />
...<br />
~Stos(void);<br />
...<br />
}<br />
Jeśli program zakończy się lub struktura przestanie być<br />
widoczna, zostanie wywołany destruktor struktury nasz_stos i<br />
pamięć zostanie zwolniona. Praktycznie oznacza to, że możemy<br />
zwolnić pamięc przyporządkowaną strukturze w taki sposób:<br />
Stos::~Stos(void)<br />
//Definicja destruktora<br />
- 273-
{<br />
free(bufor_danych);<br />
cout
cout
void main()<br />
{<br />
...<br />
}<br />
Wykażemy, że zamiana struktury na klasę odbędzie się całkiem<br />
bezboleśnie. Mało tego, jeśli dokonamy zmian w implementacji w<br />
pliku nagłówkowym (struct --> class i usuniemy słowo private)<br />
nasz program główny nie zmieni się WCALE !<br />
Oto plik nagłówkowy A:\INCLUDE\STOSCL.HPP:<br />
[P097.CPP]<br />
# include <br />
# include <br />
/* ---------------------poczatek pliku STOSCL.HPP------------ */<br />
# define OK 1<br />
class Stos<br />
{<br />
int *bufor_danych;<br />
int licznik;<br />
public:<br />
Stos(int); /* Konstruktor */<br />
~Stos(void); /* Destruktor */<br />
int Pop(int*);<br />
int Push(int);<br />
};<br />
Stos::Stos(int n_RAM)<br />
//Konstruktor - def.<br />
{<br />
licznik = -1;<br />
bufor_danych = (int *) malloc(n_RAM);<br />
cout
{<br />
if(licznik >= 49) return 0;<br />
else bufor_danych[++licznik] = na_stos;<br />
return OK;<br />
}<br />
/* ------------------------koniec pliku STOSCL.HPP----------- */<br />
void main()<br />
{<br />
Stos nasz_stos(100);<br />
int i, Liczba;<br />
//OBIEKT Klasy Stos<br />
cout
#include <br />
#include <br />
#include <br />
struct Data<br />
{<br />
int miesiac, dzien, rok;<br />
void Display(void);<br />
};<br />
//Metoda "wyswietl"<br />
void Data::Display(void)<br />
{<br />
char *mon[] =<br />
{<br />
"Stycznia","Lutego","Marca","Kwietnia","Maja","Czerwca",<br />
"Lipca","Sierpnia","Wrzesnia","Pazdziernika","Listopada",<br />
"Grudnia"<br />
};<br />
cout
dzis.rok = 1900 + tim.tm_year;<br />
cout
LEKCJA 27. O DZIEDZICZENIU.<br />
________________________________________________________________<br />
W trakcie tej lakcji dowiesz się na czym polega dziedziczenie.<br />
________________________________________________________________<br />
Dziedziczenie (ang inheritance) jest próbą naśladowania w<br />
technice programowania najcenniejszego bodaj wynalazku Matki<br />
Natury - zdolności przekazywania cech. Jeśli wyobrazimy sobie<br />
typy struktur konik, lew, słoń, czy krokodyl, to jest oczywiste,<br />
że struktury te będą posiadać wiele wspólnych cech. Wspólnymi<br />
cechami mogą być zarówno wspólne dane (parametry) - np. nogi =<br />
4; jak i wspólne wykonywane przez nie funkcje - np. jedz(),<br />
śpij(), oddychaj() itd.. Mogą występować oczywiście i różnice,<br />
ale wiele danych i funkcji okaże się wspólnych.<br />
LOGIKA DZIEDZICZENIA.<br />
Rozwijając dalej myśl naszkicowaną we wstępie, w kategoriach<br />
obiegowego języka naturalnego można rzec, że słoń Trombalski<br />
byłby tu strukturą typu formalnego Słoń. Funkcjami wewnętrznymi<br />
słonia Trombalskiego i np. krokodyla Eugeniusza mogłyby być<br />
wspólne czynności tych struktur (lub obiektów):<br />
jedz()<br />
śpij()<br />
oddychaj()<br />
Projektanci <strong>C++</strong> wpadli na pomysł naśladowania mechanizmu<br />
dziedziczenia. Zamiast tworzyć te wszystkie struktury<br />
oddzielnie, możemy zdefiniować w <strong>C++</strong> jeden ogólny typ struktur<br />
(ang. generic structure), nazywany inaczej STRUKTURĄ BAZOWĄ<br />
(ang. base structure). Wszystkie wymienione wyżej struktury<br />
(słoń, krokodyl, itp.) stałyby się wtedy strukturami pochodnymi<br />
(ang. derived structures). Nasza struktura bazowa mogłaby<br />
nazywać się znów np. Zwierzak.<br />
Ponieważ niektóre funkcje są wspólne dla wszystkich struktur<br />
(wszystkie Zwierzaki muszą jeść, spać, itp.), moglibyśmy<br />
przyjąć, że każda struktura pochodna od bazowego typu Zwierzak<br />
musi zawierać funkcje jedz(), spij() i oddychaj(). Jeśli<br />
zdefiniujemy strukturę bazową Zwierzak i zadeklarujemy w tej<br />
klasie funkcje jedz(), spij() i oddychaj(), możemy spodziewać<br />
się, że struktura pochodna słoń powinna odziedziczyć funkcje -<br />
cechy po strukturze bazowej Zwierzak. . Słoń może oczywiście<br />
mieć i swoje odrębne cechy - dane i funkcje - np.:<br />
Slon.flaga_ssak<br />
Slon.trabie()<br />
Slon.tupie()<br />
"Gramatyka" <strong>C++</strong> przy opisywaniu wzajemnego pokrewieństwa<br />
struktur (i klas) wygląda następująco:<br />
struct NazwaStrukturyPochodnej : NazwaStrukturyBazowej<br />
- 280-
{<br />
private:<br />
Lista danych i funkcji prywatnych<br />
public:<br />
Lista danych i funkcji publicznych<br />
} Lista struktur danego typu;<br />
a dla klas i obiektów:<br />
class NazwaKlasyPochodnej : dostęp NazwaKlasyBazowej<br />
{<br />
Lista danych i funkcji prywatnych<br />
public:<br />
Lista danych i funkcji publicznych<br />
} Lista obiektow danej klasy;<br />
Bazowy typ struktur w <strong>C++</strong> wyglądałaby tak:<br />
struct Zwierzak<br />
{<br />
void jedz();<br />
void spij();<br />
void oddychaj();<br />
};<br />
Jeśli chcemy zasygnalizować, że pochodny typ struktur Slon ma<br />
odziedziczyć coś po typie bazowym Zwierzak, musimy w definicji<br />
klasy pochodnej podać nazwę klasy bazowej (jeśli mamy<br />
dziedziczyć - należy wskazać po kim):<br />
struct Slon : Zwierzak<br />
{<br />
int trabie();<br />
int tupie();<br />
};<br />
Przed nazwą typu struktury (klasy) bazowej (tu: Zwierzak) może<br />
pojawić się słowo określające zasady dostępu do danych i funkcji<br />
(tu: public).<br />
[!!!] RÓŻNIE MOŻNA DZIEDZICZYĆ...<br />
________________________________________________________________<br />
* Jeśli użyjemy w tym miejscu słowa public (przy strukturach<br />
domyślne), to atrybuty dostępu zostaną odziedziczone wprost.<br />
Oznacza to, że to, co było prywatne w strukturze bazowej<br />
zostanie przeniesione jako prywatne do struktury pochodnej, a<br />
to, co było publiczne w strukturze bazowej zostanie przeniesione<br />
jako publiczne do struktury pochodnej.<br />
* Jeśli natomiast użyjemy w tym miejscu słowa private, to<br />
wszystko, co struktura pochodna odziedziczy po strukturze<br />
bazowej stanie się w strukturze pochodnej prywatne.<br />
________________________________________________________________<br />
- 281-
Opracowanie przykładowego programu ilustrującego mechanizm<br />
dziedziczenia rozpoczniemy od zdefiniowania bazowego typu<br />
struktur i struktury pochodnej.<br />
struct Zwierzak<br />
{<br />
int nogi;<br />
void jedz();<br />
void spij();<br />
void oddychaj();<br />
};<br />
należącymi do obu typów struktur - bazowego: Zwierzak i<br />
pochodnego: Slon.<br />
[???] A CO Z UNIAMI ?<br />
_______________________________________________________________<br />
Unie nie mogą brać udziału w dziedziczeniu. Unia nie może być<br />
ani typem bazowym ani typem pochodnym.<br />
_______________________________________________________________<br />
Program w całości będzie wyglądał tak:<br />
[P099.CPP]<br />
# include <br />
struct Zwierzak<br />
{<br />
int nogi;<br />
void jedz();<br />
void spij();<br />
void oddychaj();<br />
};<br />
void Zwierzak::jedz(void) { cout
LEKCJA 28. DZIEDZICZENIE ZŁOŻONE.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak można odziedziczyć wiele<br />
cech po wielu różnych przodkach.<br />
________________________________________________________________<br />
Jeśli zechcemy dziedziczyć dalej według schematu<br />
dziadek-ojciec-syn-wnuk...? Nic nie stoi na przeszkodzie. Przy<br />
okazji zwróć uwagę, że następne pokolenia są coraz bardziej<br />
złożone (tak być nie musi, ale może). W przykładzie poniżej<br />
dziedziczymy według schematu Punkt-Okrąg-Elipsa.<br />
[P100.CPP]<br />
//Przyklad dziedziczenia "wielopokoleniowego"<br />
#include "stdio.h"<br />
#include "conio.h"<br />
struct punkt //BAZOWY typ struktur - punkt(x, y)<br />
{<br />
int x; //wspolrzedne punktu na ekranie<br />
int y;<br />
};<br />
struct kolo: punkt //Str. pochodna - kolo(x, y, R)<br />
{<br />
int promien; //wspolrzedne srodka x,y dziedziczymy<br />
};<br />
struct elipsa: kolo //dziedziczymy x,y i promien<br />
{<br />
int mniejszy_promien; //Str. pochodna elipsa(x, y, R, r)<br />
};<br />
punkt P;<br />
kolo C;<br />
elipsa E;<br />
//deklarujemy trzy struktury<br />
main()<br />
{<br />
clrscr();<br />
P.x = C.x = E.x = 1;<br />
P.y = C.y = E.y = 2;<br />
//Nadajemy wartosci polom struktur<br />
C.promien = E.promien = 4;<br />
E.mniejszy_promien = 3;<br />
//Sprawdzamy zawartosc pol struktur<br />
printf("%d %d %d %d %d %d \n",<br />
P.x, C.x, E.x, P.y, C.y, E.y);<br />
printf("%d %d %d",<br />
C.promien, E.promien, E.mniejszy_promien );<br />
getch();<br />
return 0;<br />
- 284-
}<br />
Można dziedziczyć po więcej niż jednym przodku także w inny<br />
sposób. Kwadrat, dla przykładu, dziedziczy cechy po prostokątach<br />
i po rombach jednocześnie (jest jednocześnie szczególnym<br />
przypadkiem prostokąta i szczególnym przypadkiem rombu). Typ<br />
pochodny w tym wypadku, zamiast "dziadka" i "ojca" powinien mieć<br />
DWU RÓŻNYCH OJCÓW (!). W <strong>C++</strong> takie dziedziczenie po dwu różnych<br />
typach bazowych jednocześnie nazywa się DZIEDZICZENIEM<br />
WIELOBAZOWYM (ang. multi-base inheritance). A oto przykład<br />
takiego dziedziczenia.<br />
[P101.CPP]<br />
#include <br />
struct BAZOWA1<br />
{ //Struktura bazowa pierwsza<br />
public:<br />
void Funkcja_a(void);<br />
};<br />
struct BAZOWA2<br />
{ //Struktura bazowa druga<br />
public:<br />
void Funkcja_b(void);<br />
};<br />
struct POCHODNA : BAZOWA1, BAZOWA2<br />
{<br />
public:<br />
void Funkcja_c(void);<br />
};<br />
//Lista "przodkow"<br />
void BAZOWA1::Funkcja_a(void){cout
(klas) bazowych w schemacie baza_1-baza_2-....-baza_n może być<br />
więcej niż 2.<br />
DZIEDZICZENIE KLAS.<br />
Oto "klasowo-obiektowa" wersja poprzedniego programu<br />
przykładowego ze słonikiem Cholerykiem. Typy struktur Zwierzak i<br />
Slon nazwiemy klasami, (odpowiednio - klasą bazową i klasą<br />
pochodną) a strukturę Slon Choleryk nazwiemy obiektem.<br />
[P102.CPP]<br />
#include <br />
class Zwierzak<br />
{<br />
public:<br />
int nogi;<br />
//Klasa bazowa (base class)<br />
void jedz();<br />
void spij();<br />
void oddychaj();<br />
};<br />
void Zwierzak::jedz(void) { cout
Pamiętając o problemie domyślnego statusu członków<br />
struktur/public i klas/private) możemy przejść do klas i<br />
obiektów.<br />
O KLASACH SZCZEGÓŁOWO.<br />
Aby wykazać możliwość modularyzacji programu zaprojektujemy<br />
moduł w postaci pliku nagłówkowego. Moduł będzie zawierać<br />
definicję naszej prywatnej klasy obiektów ZNAK.<br />
Zaczynamy od danych, które będą nam potrzebne do tworzenia w<br />
programach (różnych !) obiektów typu Znak.<br />
class ZNAK<br />
{<br />
char znak_dany;<br />
...<br />
//Kod ASCII znaku<br />
Aby obiekt został zainicjowany (tzn. wiedział jakim znakiem ma<br />
być w danym programie) dodamy do definicji klasy<br />
jednoparametrowy konstruktor<br />
class ZNAK<br />
{<br />
char znak_dany;<br />
public:<br />
ZNAK(...);<br />
...<br />
Dane mogą być prywatne, natomiast konstruktor i funkcje-metody<br />
powinny być publiczne, by można było wywoływać je w programach.<br />
Konstruktor będziemy wywoływać w programach tak:<br />
ZNAK Obiekt('a');<br />
Znaczy to: Utwórz w RAM obiekt klasy ZNAK pod nazwą "Obiekt" i<br />
wytłumacz mu, że jest znakiem 'a'.<br />
Konstruktor powinien pobierać od programu jeden argument typu<br />
char i przekazywać go obiektowi klasy ZNAK na jego pole danych<br />
znak_dany. Definicja konstruktora będzie zatem wyglądać tak:<br />
ZNAK::ZNAK(char x)<br />
{<br />
znak_dany = x;<br />
}<br />
Zakres dopuszczalnych znaków zawęzimy np. do kodów ASCII 65...90<br />
(od A do Z). Jeśli użytkownik "nie trafi", ustawimy zawsze "*"<br />
(asterisk). Dodatkowo, dla "elegancji" zamienimy ewentualne małe<br />
litery na duże.<br />
- 287-
ZNAK::ZNAK(char x)<br />
{<br />
znak_dany = x;<br />
if(znak_dany < 65 || znak_dany >122) znak_dany = '*';<br />
if(znak_dany > 97) znak_dany -= 32;<br />
}<br />
A jeśli użytkownik nie zechce podać żadnego znaku i zda się na<br />
domyślność obiektu? Żaden problem, wystarczy do klasy ZNAK dodać<br />
bezparametrowy konstruktor domyślny. Konstruktory domyślne<br />
spełniają w <strong>C++</strong> taką właśnie rolę:<br />
class ZNAK<br />
{<br />
char znak_dany;<br />
public:<br />
ZNAK(char);<br />
ZNAK(void);<br />
...<br />
//Konstruktor zwykly ("jednoznakowy")<br />
//Konstruktor domyślny (bezparametrowy)<br />
Słowo void (tu opcjonalne) może nie wystąpić. Aby "kłuło w<br />
oczy", który konstruktor jest konstruktorem domyślnym (ang.<br />
default konstructor), większość programistów zapisuje to tak:<br />
class ZNAK<br />
{<br />
char znak_dany;<br />
public:<br />
ZNAK(char);<br />
ZNAK(); //Z daleka widać, że nic nie ma !<br />
...<br />
Definicja konstruktora bezparametrowego będzie wyglądać tak:<br />
ZNAK::ZNAK() { znak_dany = 'X'; }<br />
W zależności od sposobu zadeklarowania obiektu w programie <strong>C++</strong><br />
wywoła automatycznie albo konstruktor ZNAK(char), albo<br />
konstruktor domyślny ZNAK():<br />
ZNAK obiekt; //Nie sprecyzowano jaki, konstruktor domyślny<br />
ZNAK obiekt('m'); //Wiadomo jaki, konstruktor jednoparametrowy<br />
Dzięki temu, że <strong>C++</strong> "pedantycznie" sprawdza przed wywołaniem<br />
funkcji zgodność typów argumentów przekazywanych do funkcji<br />
(konstruktor to też funkcja) i porównuje typ argumentów z<br />
życzeniem programisty wyrażonym w prototypie - bezbłędnie<br />
rozpozna (mimo identycznej nazwy), którą funkcję należy<br />
zastosować.<br />
Dodajmy do klasy ZNAK deklaracje (prototypy) funkcji-metod:<br />
- 288-
class ZNAK<br />
{<br />
char znak_dany;<br />
public:<br />
ZNAK(char);<br />
ZNAK();<br />
void Pokaz_sie();<br />
void Znikaj();<br />
void Skacz();<br />
};<br />
i zdefiniujmy te metody.<br />
void ZNAK::Pokaz_sie(void)<br />
{<br />
cout
ZNAK::ZNAK()<br />
{<br />
znak_dany = 'X';<br />
}<br />
ZNAK::ZNAK(char x)<br />
{<br />
znak_dany = x;<br />
if(znak_dany < 65 && znak_dany >122) znak_dany = '*';<br />
if(znak_dany > 97) znak_dany -= 32;<br />
}<br />
void ZNAK::Pokaz_sie(void)<br />
{<br />
cout
}<br />
Obiekt.Znikaj();<br />
getch();<br />
Obiekt.Skacz();<br />
ZNAK Obiekt2;<br />
Obiekt2.Skacz();<br />
//To bedzie domyslny 'X'<br />
I tu już widać pewne cechy nowoczesnego obiektowego stylu<br />
programowania. Tym razem sprwdzenie, czy słowo class można<br />
spokojnie zamienić na słowo struct pozostawim dociekliwym<br />
Czytelnikom.<br />
- 291-
LEKCJA 29. FUNKCJE I OVERLOADING.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak jeszcze w <strong>C++</strong> można<br />
wykorzystywać funkcje.<br />
________________________________________________________________<br />
w <strong>C++</strong> jedna funkcja może być definiowana wielokrotnie a każda z<br />
wersji funkcji może być przystosowana do obsługi innego typu<br />
argumentów. <strong>C++</strong> wybiera tę właściwą wersję funkcji<br />
automatycznie.<br />
JEDNA NAZWA FUNKCJI - WIELE ZASTOSOWAŃ.<br />
Overloading funkcji bywa czasem w podręcznikach dzielony na<br />
odrębne zagadnienia:<br />
* funkcja może tolerować różną liczbę argumentów (co dało się<br />
spokojnie realizować również w klasycznym C - vide printf());<br />
* funkcja może tolerować różne typy argumentów;<br />
* funkcja może realizować różne operacje dla różnych<br />
Wyobraźmy sobie, że mamy funkcję wydrukuj(), która potrafi<br />
wysłać na ekran otrzymany znak:<br />
void wydrukuj(char znak)<br />
{<br />
cout
}<br />
Łańcuch znaków jest widziany jako jednowymiarowa tablica<br />
zawierająca dane typu znakowego, czyli w taki sposób:<br />
char TABLICA[9] ={ "123456789" };<br />
Definice powinny mieć następującą postać:<br />
void KLASA::wydrukuj(char znak) {cout
main()<br />
{<br />
kopiuj_string(Piggie, "Panna Piggie");<br />
kopiuj_string(Kermit, "Kermit - to protokul transmisji", 6);<br />
cout
kluczowego "inline" w definicjach funkcji. Zwróć uwgę, że w<br />
samej definicji klasy słowo inline NIE POJAWIA SIĘ:<br />
[P105.CPP]<br />
# include <br />
class Klasa<br />
{<br />
public:<br />
void wydrukuj(char* tekst);<br />
void wydrukuj(char Znak);<br />
void wydrukuj(int KodASCII);<br />
};<br />
inline void Klasa::wydrukuj(char* tekst)<br />
{<br />
cout
typu inline.<br />
________________________________________________________________<br />
Status inline możemy nadać wszystkim trzem wersjom funkcji<br />
wydrukuj() umieszczając definicje funkcji bezpośrednio wewnątrz<br />
definicji klasy:<br />
class Klasa<br />
{<br />
public:<br />
inline void wydrukuj(char* a) { cout
for(int i = 1; i < ile; i++)<br />
cout
void main()<br />
{<br />
Klasa Obiekt1; //Konstr. domyślny<br />
Klasa Obiekt2('A'); // ile - domyslnie == 4<br />
Klasa Obiekt3('B', 3);<br />
Klasa Obiekt4(p);<br />
}<br />
- 298-
LEKCJA 30. WYMIANA DANYCH MIĘDZY OBIEKTAMI.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak można wymieniać dane i<br />
informacje pomiędzy różnymi obiektami.<br />
________________________________________________________________<br />
Hermetyzacja danych jest cenną zdobyczą, ale od czasu do czasu<br />
obiekty powinny dokonywać pomiędzy sobą wymiany informacji,<br />
także tych wewnętrznych - prywatnych. Ten problem może sprawiać<br />
programiście trochę kłopotów - należy zatem poświęcić mu trochę<br />
uwagi.<br />
DOSTĘP DO DANYCH PRZY POMOCY FUNKCJI KATEGORII friend.<br />
Aby wyjaśnić mechanizmy dostępu do danych obiektów będziemy<br />
potrzebować:<br />
* wielu obiektów;<br />
* danych prywatnych obiektów (dostęp do publicznych,<br />
"niezakapsułkowanych" danych jest prosty i oczywisty);<br />
* funkcji o specjalnych uprawnieniach.<br />
Takie funkcje o specjalnych uprawnieniach - z możliwością<br />
odwoływania się do prywatnych danych wielu obiektów (a nie tylko<br />
swojego) muszą w <strong>C++</strong> posiadać status "friend" (ang. friend -<br />
przyjaciel).<br />
Nasz przykładowy program będzie operował tablicą złożoną z<br />
obiektów klasy Licznik.<br />
class Licznik<br />
{<br />
char moja_litera;<br />
int ile;<br />
public:<br />
void Inicjuj_licznik(char);<br />
void Skok_licznika(void);<br />
void Pokazuj();<br />
};<br />
...<br />
Licznik TAB[MAX];<br />
Obiekty - liczniki będą zliczać wystąpienie (każdy swojego)<br />
określonego znaku w strumieniu znaków wejściowych (wczytywanym z<br />
klawiatury). Tablica będzie się składać z MAX == 26 elementów -<br />
obiektów - liczników, po jednym dla każdej dużej litery<br />
alfabetu. Tablica będzie nazywać się TAB[26]. Po zadeklarowaniu:<br />
- 299-
nazwa_klasy TAB[MAX];<br />
kolejne obiekty będą się nazywać:<br />
nazwa_klasy Obiekt1 == TAB[0]; //Licznik 1 - 'A'<br />
nazwa_klasy Obiekt2 == TAB[1]; //Licznik 2 - 'B'<br />
... ...<br />
nazwa_klasy ObiektN == TAB[N-1];<br />
Po wprowadzeniu znaku z klawiatury wywołamy wbudowaną do każdego<br />
obiektu funkcję Skok_licznika(), która doda jedynkę do<br />
wewnętrznego licznika obiektu. Wywołując funkcję zastosujemy<br />
zamiast typowej składni<br />
ObiektK.Skok_licznika();<br />
odpowiadającą jej w tym wypadku notację<br />
TAB[i].Skok_licznika();<br />
Powinniśmy jeszcze przed wywołaniem funkcji sprawdzić, czy znak<br />
jest dużą literą alfabetu. W przykładowym programie zrobimy to<br />
tak:<br />
...<br />
cin >> znak;<br />
//Pobranie znaku z klawiatury<br />
for(int i = 0; i < 26; i++)<br />
{<br />
if(i == (znak - 'A')) TAB[i].Skok_licznika();<br />
}<br />
...<br />
Dzięki temu wewnętrzny licznik obiektu TAB[2] zostanie<br />
powiększony tylko wtedy, gdy znak - 'A' == 2 (znak jest literą<br />
C, bo 'C' - 'A' == 2).<br />
Można to zapisać skuteczniej.<br />
...<br />
cin >> znak;<br />
TAB[znak - 'A'].Skok_licznika(); //Inkrementacja licznika<br />
...<br />
bądź jeszcze krócej:<br />
...<br />
TAB[getch() - 'A'].Skok_licznika();<br />
...<br />
Istnieje tu wszakże niebezpieczeństwo próby odwołania się do<br />
nieistniejącego elementu tablicy, przed czym powinniśmy się<br />
wystrzegać.<br />
W wyniku działania programu otrzymamy zliczoną ilość<br />
występowania danej litery w strumieniu znaków wejściowych.<br />
- 300-
[P107.CPP]<br />
# include //prototyp toupper()<br />
# include <br />
class Licznik<br />
{<br />
char moja_litera;<br />
int ile;<br />
public:<br />
void Inicjuj(char);<br />
void Skok_licznika();<br />
void Pokazuj();<br />
};<br />
void Licznik::Inicjuj(char z)<br />
{<br />
moja_litera = z;<br />
ile = 0;<br />
}<br />
void Licznik::Skok_licznika(void)<br />
{<br />
ile++;<br />
}<br />
void Licznik::Pokazuj(void)<br />
{<br />
cout
}<br />
/* sprawdzamy: ----------------------------------------*/<br />
char sprawdzamy;<br />
cout
kluczowego friend. A oto definicja:<br />
int Suma(int ilosc_obiektow)<br />
{<br />
int i, suma = 0;<br />
for(i = 0; i < ilosc_obiektow; i++)<br />
suma += TAB[i].ile;<br />
}<br />
return (suma);<br />
Dzięki zastosowaniu słowa "friend", funkcja Suma() jest<br />
zaprzyjaźniona ze wszystkimi 26 obiektami, ponieważ wszystkie<br />
obiekty należą do tej klasy, w której zadeklarowaliśmy funkcję:<br />
class ...<br />
{<br />
...<br />
friend int Suma(...);<br />
...<br />
} ... ;<br />
Tablica TAB[MAX] złożona z obiektów klasy Licznik została<br />
zadeklarowana nazewnątrz funkcji main() ma więc status tablicy<br />
GLOBALNEJ. Funkcja Suma() ma dostęp do prywatnych danych<br />
wszystkich obiektów, możemy więc zastosować ją w programie w<br />
następujący sposób:<br />
[P108.CPP]<br />
# include <br />
# include <br />
class Licznik<br />
{<br />
char moja_litera;<br />
int ile;<br />
public:<br />
void Inicjuj(char);<br />
void Skok_licznika();<br />
void Pokazuj();<br />
friend int Suma(int);<br />
}<br />
const MAX = 26;<br />
Licznik TAB[MAX];<br />
register int i;<br />
main()<br />
{<br />
/* inicjujemy liczniki: -------------------------------*/<br />
for(i = 0; i < MAX; i++)<br />
{<br />
TAB[i].Inicjuj('A' + i);<br />
}<br />
- 303-
* pracujemy - zliczamy: -------------------------------*/<br />
cout znak;<br />
if(znak == '.') break;<br />
for(i = 0; i < MAX; i++)<br />
{<br />
if(i == (znak - 'A')) TAB[i].Skok_licznika();<br />
}<br />
}<br />
/* sprawdzamy: ----------------------------------------*/<br />
char sprawdzamy;<br />
cout
{<br />
}<br />
ile++;<br />
//Wiadomo o ktory obiekt chodzi<br />
Funkcja klasy friend odwołuje się do pól obiektów tak:<br />
int Suma(int liczba)<br />
{<br />
...<br />
suma += TAB[i].ile;<br />
/* - wymaga dodatkowo wskazania, o który obiekt chodzi - */<br />
}<br />
Należy pamiętać, że dla funkcji kategorii friend wszystkie<br />
obiekty należące do danej klasy mają status public - są<br />
dostępne.<br />
O ZAPRZYJAŹNIONYCH KLASACH.<br />
W <strong>C++</strong> mogą być zaprzyjaźnione ze sobą wzajemnie także klasy.<br />
Pozwala to metodom zdefiniowanym wewnątrz jednej z klas na<br />
dostęp do prywatnych danych obiektów innych klas. W przypadku<br />
zaprzyjaźnionych klas słowem kluczowym friend poprzedzamy nazwę<br />
klasy (a nie każdej zaprzyjaźnionej metody z osobna, choć<br />
zamierzony skutek właśnie na tym polega). Oto praktyczny<br />
przykład zaprzyjaźnionych klas.<br />
[P109.CPP]<br />
# include <br />
class Data1;<br />
//Deklaracja (a nie definicja!) klasy<br />
class TEZ_DATA<br />
{<br />
int dz, rok;<br />
public:<br />
TEZ_DATA() {}<br />
TEZ_DATA(int d, int y) { dz = d; rok = y;}<br />
void Pokazuj() {cout
Data1::operator TEZ_DATA(void)<br />
{<br />
TEZ_DATA DT_Obiekt(0, rok);<br />
for (int i = 0; i < mc-1; i++)<br />
DT_Obiekt.dz += TAB[i];<br />
DT_Obiekt.dz += dz;<br />
return DT_Obiekt;<br />
}<br />
main()<br />
{<br />
Data1 dt_Obiekt(11,17,89);<br />
TEZ_DATA DT_Obiekt;<br />
DT_Obiekt = dt_Obiekt;<br />
DT_Obiekt.Pokazuj();<br />
return 0;<br />
}<br />
Zaprzyjaźnione są klasy Data1 i TEZ_DATA. Dzięki temu metody<br />
zadeklarowane wewnątrz zaprzyjaźnionej klasy Data1 mają dostęp<br />
do prywatnych danych obiektów klasy TEZ_DATA. Ponieważ klasa to<br />
nowy formalny typ danych, a obiekt to dane takiego nowego typu,<br />
nic nie stoi na przeszkodzie, by obiekty przekazywać do funkcji<br />
jako argumenty (tak jak wcześniej obiekty typów typowych - int,<br />
float itp.).<br />
W <strong>C++</strong> mamy jeszcze jedną metodę wymiany danych. Możemy nadać<br />
elementom klas i obiektów status static (statyczny).<br />
WYMIANA INFORMACJI PRZY POMOCY DANYCH STATYCZNYCH.<br />
Jeśli element klasy został zadeklarowany jako element statyczny<br />
(przy pomocy słowa kluczowego static), to bez względu na to jak<br />
wiele obiektów danej klasy utworzymy, w pamięci będzie istnieć<br />
TYLKO JEDEN EGZEMPLARZ (kopia) tego elementu. W przykładowym<br />
programie z obiektami-licznikami możemy osiągnąc taki efekt<br />
nadając zmiennej ile (stan licznika) status static int ile:<br />
class Licznik<br />
{<br />
char moja_litera;<br />
static int ile;<br />
...<br />
};<br />
Jeśli utworzymy wiele obiektów takiej klasy, to wszystkie te<br />
obiekty będą posługiwać się tą samą (wspólną!) zmienną ile. Dla<br />
przykładu, jeśli zechcemy zliczać ile razy w strumieniu danych<br />
wejściowych pojawiły się np. znaki 'a' , 'b' i 'c', możemy<br />
utworzyć trzy obiekty - liczniki: licznik_a, licznik_b i<br />
licznik_c. wszystkie te liczniki będą posługiwać się wspólną<br />
zmienną statyczną ile:<br />
class Licznik<br />
- 306-
{<br />
public:<br />
char moja_litera;<br />
static int ile;<br />
Licznik(char);<br />
...<br />
};<br />
//Konstruktor<br />
Do zainicjownia obiektów posłużymy się konstruktorem. Deklaracja<br />
obiektu spowoduje automatyczne wywołanie kostruktora i<br />
zainicjowanie obiektu w pamięci. Przy okazji przekazujemy<br />
obiektom znaki do zliczania.<br />
Licznik licznik_a('a'), licznik_b('b'), licznik_c('c');<br />
Jeśli teraz w strumieniu wejściowym pojawi się któraś z<br />
interesujących nas liter (a, b, bądź c), zostanie wywołana<br />
właściwa wersja metody Skok_licznika():<br />
int main(void)<br />
{<br />
char litera;<br />
...<br />
cin >> litera;<br />
...<br />
if(litera == licznik_a.moja_litera) licznik_a.Skok_licznika();<br />
...<br />
}<br />
if(litera == licznik_b.moja_litera) licznik_b.Skok_licznika();<br />
Zmienna ile jest zmienną statyczną, więc wsztstkie trzy funkcje<br />
dokonają inkrementacji zmiennej znajdującej się pod tym samym<br />
fizycznym adresem pamięci. Jeśli dla wszystkich obiektów danej<br />
klasy jakaś zmienna oznacza zawartość tego samego adresu<br />
pamięci, możemy się odwołać do tej zmiennej również tak:<br />
nazwa_klasy::nazwa_zmiennej<br />
Ten sposób można jednakże stosować wyłącznie wobec statycznych<br />
elementów klasy o statusie danych publicznych. Jeśli są to dane<br />
prywatne nie można jeszcze dodatkowo zapominać o hermetyzacji i<br />
zasadach dostępu. Jeżeli pole danej klasy jest polem statycznym,<br />
możemy do niego odwoływać się na dwa sposoby. Za pośrednictwem<br />
obiektów w taki sposób:<br />
identyfikator_obiektu.identyfikator_pola<br />
A za pośrednictwem nazwy klasy (podobnie jak do zmiennych<br />
globalnych), taką metodą:<br />
identyfikator_klasy::identyfikator_pola<br />
- 307-
Możemy zmodyfikować program przykładowy posługując się<br />
(globalną) zmienną statyczną. Zamiast wszystkich liter będziemy<br />
zliczać tylko wystąpienia 'a', 'b' i 'c'.<br />
[P110.CPP]<br />
# include "ctype.h"<br />
# include "iostream.h"<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
static int ile;<br />
Licznik(char);<br />
void Skok_licznika();<br />
void Pokazuj();<br />
};<br />
//Konstruktor<br />
void main()<br />
{<br />
/* inicjujemy liczniki: -------------------------------*/<br />
Licznik licznik_a('a'), licznik_b('b'), licznik_c('c');<br />
/* pracujemy - zliczamy: -------------------------------*/<br />
cout znak;<br />
if(znak == '.') break;<br />
if (znak == licznik_a.moja_litera) licznik_a.Skok_licznika();<br />
if (znak == licznik_b.moja_litera) licznik_b.Skok_licznika();<br />
if (znak == licznik_c.moja_litera) licznik_c.Skok_licznika();<br />
}<br />
/* sprawdzamy: ----------------------------------------*/<br />
cout
ile++;<br />
}<br />
void Licznik::Pokazuj(void)<br />
{<br />
cout
LEKCJA 31. PRZEKAZANIE OBIEKTÓW JAKO ARGUMENTÓW DO FUNKCJI.<br />
________________________________________________________________<br />
W trakcie tej lekcji poznasz sposoby manipulowania obiektami<br />
przy pomocy funkcji. Poznasz także trochę dokładniej referencje.<br />
________________________________________________________________<br />
Typowy sposób przekazywania argumentów do funkcji w <strong>C++</strong> to<br />
przekazanie przez wartość (ang. by value). W przypadku obiektów<br />
oznacza to w praktyce przekazanie do funkcji kopii obiektu. Jako<br />
przykład zastosujemy program zliczający wystąpienia znaków w<br />
strumieniu wejściowym. Zmienimy w tym programie sposób<br />
wyprowadzenia wyników. Funkcji Pokazuj() przekażemy jako<br />
argument obiekt. Obiekt-licznik zawiera w środku tę informację,<br />
której potrzebuje funkcja - ilość zliczonych znaków. Zacznijmy<br />
od zdefiniowania klasy.<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik(char litera);<br />
void Skok_licznika();<br />
};<br />
W programie głównym możemy zastosować konstruktor do<br />
zainicjowania obiektu np. tak:<br />
main()<br />
{<br />
Licznik licznik_a('a');<br />
...<br />
Zdefiniujmy funkcję. Obiekt licznik_a będzie argumentem funkcji<br />
Pokazuj(). Funkcja powinna wyprowadzić na ekran zawartość pola<br />
licznik_a.ile. Deklaracja - prototyp takiej pobierającej obiekt<br />
funkcji będzie wyglądać tak:<br />
wart_zwracana Nazwa_funkcji(nazwa_klasy nazwa_obiektu);<br />
Nazwa klasy spełnia dokładnie taką samą rolę jak każdy inny typ<br />
danych. W naszym przypadku będzie to wyglądać tak:<br />
void Pokazuj(Licznik obiekt);<br />
Ponieważ "obiekt" jest parametrem formalnym i jego nazwa nie<br />
jest tu istotna, możemy pominąć ją w prototypie funkcji (w<br />
definicji już nie!) i skrócić zapis do postaci:<br />
void Pokazuj(Licznik);<br />
Funkcja Pokazuj() otrzyma w momencie wywołania jako swój<br />
argument kopię obiektu, którą jako argument formalny funkcji<br />
nazwaliśmy "obiekt". W naszym programie wywołanie tej funkcji<br />
- 310-
ędzie wyglądać tak:<br />
Pokazuj(licznik_a);<br />
Obiekt "licznik_a" jest tu BIEŻĄCYM ARGUMENTEM FAKTYCZNYM. Typ<br />
(tzn. tu: klasa) argumentu faktycznego musi być oczywiście<br />
zgodny z zadeklarowanym wcześniej typem argumentu formalnego<br />
funkcji.<br />
Jeśli funkcja dostała własną kopię obiektu, może odwołać się do<br />
elementów tego obiektu w taki sposób:<br />
void Pokazuj(Licznik obiekt)<br />
{<br />
cout
int Pokazuj2(Licznik);<br />
void main()<br />
{<br />
/* inicjujemy licznik: -------------------------------*/<br />
Licznik licznik_a('a');<br />
/* pracujemy - zliczamy: -------------------------------*/<br />
cout znak;<br />
if(znak == '.') break;<br />
if (znak == licznik_a.moja_litera) licznik_a.Skok_licznika();<br />
}<br />
/* sprawdzamy: ----------------------------------------*/<br />
cout
Programy manipulujące obiektami w taki sposób mogą wymagać<br />
modelu pamięci większego niż przyjmowany domyślnie model SMALL.<br />
Typowy komunikat pojawiający się przy zbyt małym modelu pamięci<br />
to:<br />
Error 43: Type mismatch in parameter to call to<br />
Pokazuj1(Licznik)...<br />
(Źły typ argumentu przy wywołaniu funkcji Pokazuj(...)...)<br />
Programy obiektowe są z reguły szybke, ale niestety dość<br />
"pamięciochłonne". W IDE BORLAND <strong>C++</strong> masz do dyspozycji opcję:<br />
Options | Compiler | Code generation | Model<br />
Dokładniejsze informacje o modelach pamięci znajdziesz w dalszej<br />
części książki.<br />
________________________________________________________________<br />
O PROBLEMIE REFERENCJI.<br />
Typowy (domyślny) sposób przekazywania argumentów do funkcji w<br />
<strong>C++</strong> polega na tzw. "przekazaniu przez wartość" i jest inny niż<br />
Pascalu, czy Basicu. Ponieważ w polskich warunkach do C/<strong>C++</strong><br />
większość adeptów "dojrzewa" po przebrnięciu przez Basic i/lub<br />
Pascal, programiści ci obciążeni są już pewnymi nawykami i<br />
pewnym schematyzmem myślenia, który do <strong>C++</strong> niestety nie da się<br />
zastosować i jest powodem wielu pomyłek. To, co w Basicu wygląda<br />
zrozumiale (uwaga, tu właśnie pojawia się automatyzm myślenia):<br />
PRINT X<br />
INPUT X<br />
REM Wyprowadź bieżącą wartość zmiennej X<br />
REM Pobierz wartość zmiennej X<br />
a w Pascalu:<br />
writeln(X); { Wyprowadź bieżacą wartość zmiennej X }<br />
readln(X); { Pobierz wartość zmiennej X }<br />
przyjmuje w C/<strong>C++</strong> formę zapisu wyraźnie dualnego:<br />
printf("%d", X); //Wyprowadź wartość zmiennej X<br />
scanf("%d", &X); //Pobierz wartość zmiennej X<br />
Na czym polega różnica? Jeśli odrzucimy na chwilę automatyzm i<br />
zastanowimy się nad tą sytuacją, zauważymy, że w pierwszym<br />
przypadku (wyprowadzanie istniejących już danych - PRINT,<br />
wrilteln, printf()) w celu poprawnego działania funkcji<br />
powinniśmy przekazać jej BIEŻĄCĄ WARTOŚĆ ARGUMENTU X (adres<br />
zmiennej w pamięci nie jest funkcji potrzebny). Dla Basica,<br />
Pascala i <strong>C++</strong> bieżąca wartość zmiennej kojarzoana jest z jej<br />
identyfikatorem - tu: "X". W drugim jednakże przypadku (pobranie<br />
danych i umieszczenie ich pod właściwym adresem pamięci) jest<br />
- 313-
inaczej. Funkcji zupełnie nie interesuje bieżąca wartść zmiennej<br />
X, jest jej natomiast do poprawnego działania potrzebny adres<br />
zarezerwowany dla zmiennej X w pamięci. Ale tu okazuje się, że<br />
Basic i Pascal postępują dokładnie tak samo, jak poprzednio:<br />
INPUT X i read(X);<br />
Oznacza to, że X nie oznacza dla Pascala i Basica bieżącej<br />
wartości zmiennej, lecz oznacza (DOMYŚLNIE) przekazanie do<br />
funkcji adresu zmiennej X w pamięci. Funkcje oczywiście<br />
"wiedzą", co dostały i dalej już one same manipulują danymi we<br />
właściwy sposób.<br />
W <strong>C++</strong> jest inaczej. Zapis:<br />
Funkcja(X);<br />
oznacza w praktyce, że zostaną wykonane następujące operacje:<br />
* spod adresu pamięci przeznaczonego dla zmiennej X zostanie<br />
(zgodnie z zadeklarowanym formatem) odczytana bieżąca wartość<br />
zmiennej X;<br />
* wartość X zostanie zapisana na stos (PUSH X);<br />
* zostanie wywołana funkcja Funkcja();<br />
* Funkcja() pobierze sobie wartość argumentu ze stosu (zgodnie z<br />
formatem zadeklarowanym w prototypie Funkcji()).<br />
* Funkcja() zadziała zgodnie ze swoją definicją i jeśli ma coś<br />
do pozostawienia (np. return (wynik); ) pozostawi wynik.<br />
Jak widać:<br />
* funkcja "nie wie", gdzie w pamięci umieszczony był przekazany<br />
jej argument;<br />
* funkcja komunikuje się "ze światem zewnętrznym" (czyli własnym<br />
programem, bądź funkcją wyższego rzędu - wywołującą) tylko za<br />
pośrednictwem stosu;<br />
* funkcja dostaje swoją "kopię" argumentu z którym działa;<br />
* funkcja nie ma wpływu na "oryginał" argumentu, który pozostaje<br />
bez zmian.<br />
REFERENCJA - CO TO TAKIEGO ?<br />
Zastanówmy się, czym właściwie jest referencja zmiennej w <strong>C++</strong>.<br />
Pewne jest, że jest to alternatywny sposób odwołania się do<br />
zmiennej. Zacznijmy od trywialnego przykładu odwołania się do<br />
tej samej zmiennej mającej swoją właściwą nazwę "zmienna" i<br />
referencję "ksywa".<br />
# include "iostream.h"<br />
main()<br />
- 314-
{<br />
int zmienna;<br />
int& ksywa;<br />
...<br />
Aby "ksywa" oznaczała tę samą zmienną, referencję należy<br />
zainicjować:<br />
int& ksywa = zmienna;<br />
Zainicjujemy naszą zmienną "zmienna" i będziemy robić z nią<br />
cokolwiek (np. inkrementować). Równocześnie będziemy sprawdzać,<br />
czy odwołania do zmiennej przy pomocy nazwy i referencji będą<br />
pozostawać równoważne.<br />
[P111.CPP]<br />
/* UWAGA: Program moze potrzebowac modelu wiekszego niz<br />
domyslnie ustawiany MODEL SMALL */<br />
# include "iostream.h"<br />
main()<br />
{<br />
int zmienna = 6666;<br />
int& ksywa = zmienna;<br />
cout
zastosowania: określenie adresu w pamęci oraz tworzenie<br />
wskazania. Aby rozróżnić te dwie sytuacje zwróć uwagę na<br />
"gramatykę" zapisu. Jeśli identyfikator zminnej jest poprzedzony<br />
określeniem typu zminnej:<br />
int &zmienna; /* lub */ int &zmienna = ... ;<br />
to zmienną nazywamy "zmienną referencyjną". Jeśli natomiast<br />
identyfikator nie został poprzedzony określeniem typu:<br />
p = &zmienna;<br />
to mówimy wtedy o adresie zmiennej.<br />
Przekazanie argumentu do funkcji poprzez referencję jest w<br />
istocie zbliżone do przekazania wskaźnika do argumentu. Zwróć<br />
uwagę, że przekazanie wskaźnika do obiektu może zwykle odbyć się<br />
szybciej niż sporządzenie kopii obiektu i przekazanie tej kopii<br />
do funkcji. Zastosowanie w deklaracji funkcji operatora<br />
adresowego & pozwala nam stosować syntaktykę zapisu taką "jak<br />
zwykle" - przy przekazaniu przez wartość. Jeśli nie chcemy<br />
ryzykować zmian wprowadzonych do oryginalnego parametru<br />
przekazanego funkcji poprzez wskazanie, możemy zadeklarować<br />
oryginalny parametr jako stałą (kompilator "dopilnuje" i<br />
uniemożliwi zmianę wartości):<br />
nazwa_funkcji(const &nazwa_obiektu);<br />
________________________________________________________________<br />
Poprosimy <strong>C++</strong> by pokazał nam konkretne fizyczne adresy<br />
skojarzone z identyfikatorami "zmienna" i "ksywa". Operator &<br />
oznacza dla <strong>C++</strong><br />
&X --> adres w pamięci zmiennej X<br />
[P112.CPP]<br />
/* UWAGA: Program moze potrzebowac modelu wiekszego niz<br />
domyslnie ustawiany MODEL SMALL */<br />
# include "iostream.h"<br />
main()<br />
{<br />
int zmienna = 6666;<br />
int& ksywa = zmienna;<br />
cout
Zmienna (ADR-hex) Ksywa (ADR-hex):<br />
0x287efff4<br />
0x287efff4<br />
Fizyczny adres pamięci, który "kojarzy się" <strong>C++</strong> ze zmienną i<br />
ksywą jest identyczny. Referencja nie oznacza zatem ani<br />
sporządzania dodatkowej kopii zmiennej, ani wskazania do<br />
zmiennej w rozumieniu wskaźnika (pointer). Jest to inna metoda<br />
odwołania się do tej samej pojedynczej zmiennej.<br />
- 317-
LEKCJA 32. WSKAŹNIKI DO OBIEKTÓW.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak posługiwać się obiektami<br />
za pośrednictwem wskaźników.<br />
________________________________________________________________<br />
Wskaźniki do obiektów funkcjonują podobnie jak wskaźniki do<br />
struktur. Operator -> pozwala na dostęp zarówno do danych jak i<br />
do funkcji. Dla przykładu wykorzystamy obiekt naszej prywatnej<br />
klasy Licznik.<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik(char znak) { moja_litera = z; ile = 0; }<br />
void Skok_licznika(void) { ile++; }<br />
};<br />
Aby w programie można było odwołać się do obiektu nie poprzez<br />
nazwę a przy pomocy wskaźnika, zadeklarujemy wskaźnik do<br />
obiektów klasy Licznik:<br />
Licznik *p;<br />
Wskaźnik w programie możemy zastosować np. tak:<br />
p->Skok_licznika();<br />
(czytaj: Wywołaj metodę "Skok_licznika()" w stosunku do obiektu<br />
wskazywanego w danym momencie przez wskaźnik p)<br />
Trzeba pamiętać, że sama deklaracja w przypadku referencji i<br />
wskaźników nie wystarcza. Przed użyciem należy jeszcze<br />
zainicjować wskaźnik w taki sposób, by wskazywał na nasz<br />
obiekt-licznik. Wskaźnik do obiektu inicjujemy w taki sam sposób<br />
jak każdy inny pointer:<br />
p = &Obiekt;<br />
Możemy przystąpić do utworzenia programu przykładowego.<br />
[P119.CPP]<br />
# include "ctype.h"<br />
# include "iostream.h"<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik(char z) { moja_litera = z; ile = 0; }<br />
- 318-
void Skok_licznika(void) { ile++; }<br />
};<br />
void main()<br />
{<br />
char znak;<br />
cout > znak;<br />
Licznik Obiekt1(znak), Obiekt2('a'), *p1, *p2;<br />
p1 = &Obiekt1;<br />
p2 = &Obiekt2;<br />
cout znak;<br />
if(znak == '.') break;<br />
if(znak == p1->moja_litera) p1->Skok_licznika();<br />
if(znak == p2->moja_litera) p2->Skok_licznika();<br />
}<br />
cout
wątpliwości zidentyfikować właśnie ten obiekt, z którym pracuje<br />
a nie obiekt przypadkowy.<br />
[!!!] FUNKCJE KATEGORII static NIE OTRZYMUJĄ POINTERA this.<br />
Należy pamiętać, że wskaźnik this istnieje wyłącznie podczas<br />
wykonywania metod (ang. class member function execution), za<br />
wyjątkiem funkcji statycznych.<br />
Jeśli w programie zadeklarujemy klasę Klasa:<br />
class Klasa<br />
{<br />
int dane;<br />
...<br />
}<br />
a wewnątrz tej klasy metodę Pokazuj():<br />
class Klasa<br />
{<br />
int dane;<br />
public:<br />
void Pokazuj();<br />
...<br />
}<br />
void Klasa::Pokazuj(void)<br />
{<br />
cout ), jak poniżej, będzie równoważne:<br />
void Klasa::Pokazuj(void)<br />
{<br />
cout dane;<br />
}<br />
Przypomnijmy, że taka notacja wskaźnikowa oznacza:<br />
"Wyprowadź zawartość pola "dane" obiektu, na który wskazuje<br />
wskaźnik" (ponieważ jest to wskaźnik this, więc chodzi o własny<br />
obiekt).<br />
- 320-
LEKCJA 33. OVERLOADING OPERATORÓW.<br />
________________________________________________________________<br />
Podczas tej lekcji poznasz możliwości dostosowania operatorów<br />
<strong>C++</strong> do własnego "widzimisię" i do potrzeb własnych obiektów.<br />
________________________________________________________________<br />
Niemal od początku niniejszej książki korzystamy z operatorów<br />
poddanych overloadingowi. Są to operatory > , które<br />
pierwotnie wykonywały bitowe przesunięcie w lewo i w prawo.<br />
Owerloading tych operatorów "załatwił" za nas producent<br />
(<strong>Borland</strong>, Microsoft, czy inny). Jak widzisz, nie powoduje to w<br />
dalszym użytkowaniu tych operatorów żadnych zauważalnych<br />
komplikacji, a często ułatwia tworzenie programów. Zwróć uwagę,<br />
że overloading operatorów (jak i definicje klas) może znajdować<br />
się w dołączonych plikach nagłówkowych i po jednorazowym<br />
wykonaniu może być "niewidoczny" dla programistów tworzących<br />
programy aplikacyjne.<br />
Jeśli projektujemy (definiujemy) nową klasę, dodajemy do <strong>C++</strong><br />
nowy, lecz pełnoprawny typ danych. Autorzy <strong>C++</strong> nie byli w stanie<br />
przewidzieć jakie klasy i jakie obiekty mogą wymyślić kolejne<br />
pokolenia programistów w ramach swojej radosnej twórczości.<br />
Wprowadzili zatem do <strong>C++</strong> jasne i jednoznaczne algorytmy<br />
postępowania z typami "typowymi". <strong>C++</strong> doskonale wie jak dodawać,<br />
mnożyć, czy odejmować np. liczby int, long, float itp., nie wie<br />
jednak jak dodać do siebie obiekty klas CString (CString = Class<br />
String = klasa "łańcuch znaków"), TOdcinek (to taki kawałek<br />
prostej) itp.. A przecież miło byłoby, gdyby rozbudować<br />
działanie operatorów tak, by było możliwe ich typowe<br />
zastosowanie w stosunku do naszych własnych, "nietypowych"<br />
obiektów:<br />
int x, y; int z = x + y; //To operator + załatwia sam<br />
float x, y; float z = x + y;<br />
Zanim jednak stanie się możliwe postępowanie takie:<br />
class CString x, y, z; z = x + y;<br />
class Nasza_Klasa obiekt1, obiekt2, obiekt3;<br />
obiekt3 = obiekt1 + obiekt2;<br />
itp., itd. ...<br />
musimy "uzupełnić" <strong>C++</strong> i "wyjaśnić" operatorom, co właściwie ma<br />
w praktyce oznaczać operacja obiekt1 = obiekt2 + obiekt3; .<br />
Jest wyczuwalne intuicyjnie, że działanie operatorów w stosunku<br />
do różnych obiektów może być różne. Dla przykładu - wiesz<br />
zapewne, że inaczej wygląda algorytm mnożenia liczb zespolonych,<br />
a inaczej liczb całkowitych rzeczywistych. Dlatego też wykonanie<br />
- 321-
operacji mnożenia wymaga od operatora * podjęcia różnych<br />
działań:<br />
class Liczba_zespolona x, y, z; z = x * y;<br />
int x, y, z; z = x * y;<br />
Czasem może się zdarzyć, że dla dwu różnych klas działanie<br />
jakiegoś operatora jest identyczne, częściej jednak (i tak<br />
należy się spodziewać) działanie operatora dla każdej klasy<br />
będzie odrębne i unikalne.<br />
Pójdźmy w tym rozumowaniu o krok dalej. Skoro rozszerzenie<br />
obszaru zastosowań jakiegoś operatora na obiekty nowej<br />
(nieznanej wcześniej klasy) wymaga zdefiniowania nowego<br />
algorytmu działania operatora, <strong>C++</strong> będzie potrzebował do tego<br />
celu specjalnych środków, które powinny być łatwo rozpoznawalne.<br />
Do opisu algorytmów służą generalnie w <strong>C++</strong> funkcje i tu Autorzy<br />
nie wprowadzili wyjątku. Zastrzegli jednak dla tych specjalnych<br />
funkcji specjalną nazwę: operator ...();<br />
I tak funkcja precyzująca nowy algorytm dodawania (nowy sposób<br />
działania operatora + ) będzie się nazywać:<br />
operator+();<br />
a np. funkcja określająca nowy algorytm mnożenia (nowy sposób<br />
działania operatora * ) będzie się nazywać:<br />
operator*();<br />
Spróbujmy zastosować taką filozofię w praktyce programowania.<br />
[!!!] NIESTETY NIE WSZYSTKIE OPERATORY MOŻNA ROZBUDOWAĆ.<br />
________________________________________________________________<br />
Są w <strong>C++</strong> operatory, których nie możemy poddać overloadingowi. Są<br />
to:<br />
. :: .* ?:<br />
. operator kropki umożliwia dostęp do pól struktur i obiektów;<br />
:: operator "widoczności-przesłaniania" (ang. scope);<br />
.* wskazanie członka klasy (ang. pointer-to-member);<br />
?: operator warunkowy.<br />
________________________________________________________________<br />
Wszystkie pozostałe operatory możemy poddać overloadingowi i<br />
przypisywać im potrzebne nam działanie.<br />
OVERLOADING OPERATORA [+] (DWUARGUMENTOWEGO).<br />
- 322-
Zaczniemy od operatora + należącego do grupy "dwuargumentowych<br />
operatorów arytmetycznych" (ang. binary arithmetic operator).<br />
Zwracamy tu już na początku rozważań uwagę na przynależność<br />
operatora do określonej grupy, ponieważ overloading różnych<br />
opertorów należących do tej samej grupy przebiega podobnie.<br />
Ponieważ znak + może być także operatorem jednoargumentowym<br />
(ang. unary plus, o czym za chwilę), podkreślamy, że tym razem<br />
chodzi o plus jako operator dodawania. Overloading operatora<br />
przeprowadzimy w stosunku do obiektów prostej, znanej Ci już z<br />
poprzednich przykładów klasy Data, którą (w celu upodobnienia<br />
się do maniery stosowanej w Windows i bibliotekach klas)<br />
nazwiemy tym razem CData. "Namówimy" operator + do<br />
przeprowadzenia operacji na obiektach (dokładniej na polach<br />
obiektów):<br />
CData nowadata = staradata + 7;<br />
// W tydzien pozniej<br />
Operator + musi oczywiście "wiedzieć", na którym polu obiekty<br />
klasy CData przechowują liczbę dni i jak związane są (logicznie)<br />
pola obiektu dz, mc, rok. Jest rzeczą zrozumiałą, że samo<br />
dodanie dni do pola dz może nie wystarczyć, ponieważ data<br />
37.11.93 jest niedopuszczalna.<br />
Jeśli staradata jest obiektem klasy CData z zawartymi wewnątrz<br />
danymi, to w wyniku działania "nowego" operatora + powinien<br />
powstać obiekt nowadata klasy CData, którego pola zostaną w<br />
sensowny sposób powiększone o dodaną liczbę dni. Rozważ<br />
działanie programu (najlepiej skompiluj i uruchom).<br />
[P120.CPP]<br />
/* Overloading operatora dwuargumentowego + */<br />
# include <br />
class CData<br />
{<br />
int dz, mc, rok;<br />
public:<br />
CData() {} //Konstruktor domyslny (pusty)<br />
CData(int d, int m, int y) { mc = m; dz = d; rok = y; }<br />
void Pokazuj() { cout
{<br />
n -= TAB[kopia_obiektu.mc-1];<br />
if (++kopia_obiektu.mc == 13)<br />
{ kopia_obiektu.mc = 1; kopia_obiektu.rok++; }<br />
}<br />
kopia_obiektu.dz = n;<br />
return (kopia_obiektu);<br />
}<br />
main()<br />
{<br />
CData staradata(31, 1, 94); //Kostruktor z argumentami<br />
CData nowadata; //Pusty konstruktor<br />
cout > n;<br />
nowadata = staradata + n;<br />
cout
nowadata = staradata + 14;<br />
zostanie wykonana poprawnie.<br />
Ale to nie wszystko. Jeśli wystąpi układ odwrotny - np.:<br />
nowadata = 14 + staradata;<br />
nasz operator "zgłupieje". Doszedłszy do operatora + <strong>C++</strong> "nie<br />
będzie jeszcze wiedział" (analizuje wyrażenia arytmetyczne od<br />
lewej do prawej), KTÓRY obiekt wystąpi za chwilę. Jedno jest<br />
pewne, nie zawsze musi być to "własny" obiekt funkcji, do<br />
którego mamy pointer this. Aby uzyskać jednoznaczność sytuacji,<br />
funkcja operatorowa powinna tu w jawny sposób pobierać przed<br />
zadziałaniem dwa argumenty:<br />
CData operator+(int n, CData obiekt);<br />
aby działanie:<br />
CData obiekt_wynik;<br />
obiekt_wynik = n + obiekt;<br />
stało się wykonalne. Pojawia się tu wszakże pewien problem.<br />
Wskaźnik this wskazuje własny obiekt funkcji-metody, a tym razem<br />
funkcja potrzebuje dostępu nie do pola własnego obiektu, lecz do<br />
pola "obcego" obiektu przekazanego jej jako argument. Ale w <strong>C++</strong><br />
możemy:<br />
* zdefiniować dwie (i więcej) funkcji o tej samej nazwie (każda<br />
na inną ewentualność);<br />
* możemy nadać funkcji status friend (wtedy nie będąc metodą też<br />
uzyska dostęp do danych obiektu).<br />
Definicja naszej klasy CData zawierająca deklaracje dwu funkcji<br />
operatorowych operator+() różniących się zastosowaniem i (po<br />
czym rozpozna je <strong>C++</strong>) liczbą argumentów, będzie wyglądać tak:<br />
class CData<br />
{<br />
int dz, mc, rok;<br />
public:<br />
CData() {}<br />
CData(int d, int m, int y) { mc = m; dz = d; rok = y; }<br />
void Pokazuj() { cout
zawiera:<br />
* prywatne dane;<br />
* dwa konstruktory;<br />
* własną metodę - funkcję operatorową operator+();<br />
* deklarację zaprzyjaźnionej z klasą funkcji kategorii friend<br />
(choć jest to funkcja o tej samej nazwie, jej status i<br />
uprawnienia są nieco inne).<br />
[!!!] NIE WSZYSTKO, CO WEWNĄTRZ JEST METODĄ.<br />
________________________________________________________________<br />
Nawet, jeśli wewnątrz definicji klasy zdefiniujemy w pełni<br />
funkcję (nadając jej status inline), nie stanie się ona metodą!<br />
Słowo kluczowe friend określa status funkcji jednoznacznie, bez<br />
względu na to, w którym miejscu w tekście programu umieścimy<br />
definicję ciała funkcji.<br />
________________________________________________________________<br />
W zasadzie ciało funkcji jest na tyle proste (wymagamy od niej<br />
tylko zwrotu obiektu ze zmodyfikowanym polem danych), że możemy<br />
skorzystać z rozbudowanego wcześniej operatora + i całe ciało<br />
zdefiniować tak:<br />
class CData<br />
{<br />
int dz, mc, rok;<br />
public:<br />
...<br />
CData operator+(int);<br />
friend CData operator+(int n, CData& x) { return (x + n); }<br />
};<br />
Jeśli w operacji dodawania argumenty zastosujemy we<br />
wcześniejszej kolejności:<br />
return (obiekt + liczba);<br />
to zostanie tu wykorzystany operator + rozbudowany poprzednio<br />
przez metodę CData::operator+(int). Program w całości może<br />
zatem wyglądać tak:<br />
[P121.CPP]<br />
# include "iostream.h"<br />
class CData<br />
{<br />
int dz, mc, rok;<br />
public:<br />
CData() {}<br />
CData(int d, int m, int y) { mc = m; dz = d; rok = y; }<br />
void Pokazuj() { cout
static int TAB[] = {31,28,31,30,31,30,31,31,30,31,30,31};<br />
CData CData::operator+(int n)<br />
{<br />
CData kopia_obiektu = *this;<br />
n += kopia_obiektu.dz;<br />
while (n > TAB[kopia_obiektu.mc-1])<br />
{<br />
n -= TAB[kopia_obiektu.mc-1];<br />
if (++kopia_obiektu.mc == 13)<br />
{ kopia_obiektu.mc = 1; kopia_obiektu.rok++; }<br />
}<br />
kopia_obiektu.dz = n;<br />
return (kopia_obiektu);<br />
}<br />
main()<br />
{<br />
CData staradata(31, 1, 94); //Kostruktor z argumentami<br />
CData nowadata, jeszczejednadata;<br />
cout > n;<br />
nowadata = staradata + n;<br />
cout -10<br />
Jest zatem --> 21.1.94<br />
Testuje nowy operator: 22.1.94<br />
lub tak:<br />
C:\>program<br />
Stara data: 31.1.94<br />
Podaj ile minelo dni --> -150<br />
- 327-
Jest zatem --> -119.1.94<br />
Testuje nowy operator: -118.1.94<br />
Funkcja operatorowa została napisana w taki sposób, że po<br />
przekroczeniu wartości -31 program będzie wypisywał bzdury. Jako<br />
zadanie domowe - spróbuj zmodyfikować algorytm w taki sposób, by<br />
rozszerzyć zakres poprawnych wartości.<br />
[!!!] Możesz dodawać obiekty minusem.<br />
________________________________________________________________<br />
* Należy tu zwrócić uwagę, że dodawanie obiektów może wykonywać<br />
nie tylko i nie koniecznie operator + . Jeśli zechcesz, możesz<br />
do tego celu zastosować dowolnie wybrany operator (np. -, *<br />
itp.). W celu ułatwienia zrozumienia zapisu (i tylko dlatego)<br />
większość programistów rozbudowuje działanie operatorów zgodnie<br />
z ich pierwotnym zastosowaniem.<br />
* DOWOLNOŚĆ, ALE NIE PEŁNA!<br />
O tyle, o ile działanie operatora może być zmienione, to ilość<br />
argumentów potrzebnych operatorowi pozostaje w <strong>C++</strong> "sztywna"<br />
(patrz przykład z n!).<br />
________________________________________________________________<br />
W bardzo podobny sposób możesz rozbudowywać inne arytmetyczne<br />
operatory dwuargumentowe (*, /, -, itp.) w stosunku także do<br />
innych klas.<br />
OVERLOADING OPERATORÓW JEDNOARGUMENTOWYCH ++ I -- .<br />
Typowe operatory jednoargumentowe to ++ i --. Jako przykładem<br />
posłużymy się problemem zlicznia znaków pobieranych ze<br />
strumienia wejściowego.<br />
Zaczniemy od redefinicji postinkrementacji licznika. Musimy<br />
zastosować funkcję operatorową. Funkcja, chcąc operować na<br />
obiektach musi w stosunku do tych obiektów posiadać status<br />
friend, lub być metodą. Prototyp funkcji operatorowej potrzebnej<br />
do wykonania overloadingu operatora jednoargumentowego ++<br />
wygląda w postaci ogólnej tak:<br />
typ_zwracany nazwa_klasy::operator++(lista argumentów);<br />
Funkcje operatorowe zwracają zwykle wartość zgodną co do typu z<br />
typem obiektów z którymi współpracują. Jeśli identyfikatory b, c<br />
i d reprezentują obiekty, nic nie stoi na przeszkodzie, by stał<br />
się możliwy zapis:<br />
class Klasa<br />
{<br />
...<br />
} x, y, z;<br />
...<br />
- 328-
z = x + y;<br />
Dodajemy dwa obiekty x i y tego samego typu (tej samej klasy), a<br />
wynik przypisujemy obiektowi z, który także jest obiektem tego<br />
samego typu. Jeśli możnaby jeszcze zastosować operator<br />
przypisania tak:<br />
z = q = x + y;<br />
operator przypisania = zwracałby nam w efekcie obiekt tego<br />
samego typu. Funkcje operatorowe muszą przestrzegać tych samych<br />
zasad, które obowiązują wyrażenia: typ argumentów x, y, z, q,<br />
... powinien być zgodny, rezultat operacji (x + y) powinien być<br />
obiektem tego samego typu, co obiekty x, y, z, q. Dokonując<br />
overloadingu operatorów powinniśmy precyzyjnie określić typ<br />
wartości zwracanej w wyniku działania operatora.<br />
Stosowaną poprzednio do inkrementacji liczników metodę<br />
Skok_licznika() zastąpimy w definicji klasy funkcją operatorową:<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik(char);<br />
Licznik operator++();<br />
};<br />
Powinniśmy teraz zdefiniować funkcję operatorową. Ponieważ pole<br />
obiektu, które zamierzamy inkrementować nazywa się:<br />
obiekt.ile<br />
// Licznik::ile;<br />
funkcja powinna zadziałać tak:<br />
Licznik Licznik::operator++(void)<br />
{<br />
this->ile++;<br />
return (*this);<br />
}<br />
Przetłumaczmy tę notację na "ludzki język". Funkcja operatorowa:<br />
* nie pobiera żadnych jawnych argumentów (void);<br />
* jest metodą, zatem w momencie wywołania otrzymuje w niejawny<br />
sposób wskaźnik *this do "własnego" obiektu;<br />
* posługując się wsakźnikiem this inkrementuje zawartość pola<br />
int ile własnego obiektu;<br />
* zwraca obiekt (zmodyfikowany) klasy Licznik (tj. dokładniej -<br />
zwraca wskaźnik this do własnego-zmodyfikowanego obiektu.<br />
- 329-
Ponieważ funkcja operatorowa jest metodą zadeklarowaną wewnątrz<br />
klasy, bez problemu uzyska dostęp do wewnętrznych pól obiektów<br />
tej klasy i wykona inkrementację licznika. Możemy zatem<br />
zastosować wyrażenie typu:<br />
Licznik licznik_a;<br />
licznik_a++;<br />
Funkcja jest metodą wraz ze wszystkimi właściwymi metodom<br />
przywilejami. Zapis możemy zatem uprościć do postaci:<br />
Licznik Licznik::operator++(void)<br />
{<br />
ile++;<br />
return (*this);<br />
}<br />
a tak skrócone ciało funkcji umieścić w definicji klasy obok<br />
definicji konstruktora:<br />
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik(char z) { ile = 0; moja_litera = z; }<br />
Licznik operator++() { ile++; return (this); }<br />
};<br />
Aby nie zaciemniać obrazu, przy pomocy licznika będziemy tym<br />
razem zliczać wszystkie znaki za wyjątkiem kropki. Ponieważ<br />
licznik nie będzie miał swojej ulubionej litery, możemy<br />
zastosować pusty konstruktor.<br />
[P121.CPP]<br />
/* --------------------- POST - inkrementacja ----------- */<br />
# include "iostream.h"<br />
class Licznik<br />
{<br />
public:<br />
int ile;<br />
Licznik() { ile = 0;}<br />
Licznik operator++() { ile++; return (*this); }<br />
} obiekt;<br />
void main()<br />
{<br />
cout > znak;<br />
if(znak == '.') break;<br />
obiekt++;<br />
}<br />
- 330-
cout
class Licznik<br />
{<br />
public:<br />
char moja_litera;<br />
int ile;<br />
Licznik() { ile = 0; } //Pusty konstruktor<br />
Licznik(char);<br />
Licznik operator++(); //Funkcja pre/post-inkrementacji<br />
Licznik operator--(); //Funkcja pre/post-dekrementacji<br />
};<br />
Licznik::Licznik(char z) { moja_litera = z; ile = 10; }<br />
Licznik Licznik::operator++(void) { ile++; return *this; }<br />
Licznik Licznik::operator--(void) { ile--; return *this; }<br />
void main()<br />
{<br />
Licznik obiekt1('A'), obiekt2;<br />
//obiekt2 - "pusty"<br />
cout
{<br />
public:<br />
long wartosc;<br />
Liczba(int x) { wartosc = (long) x; }<br />
friend void operator!(Liczba&);<br />
};<br />
void operator!(Liczba& obiekt)<br />
{<br />
long wynik = 1;<br />
for(int i = 1; i
cout
overloadingowi operator
LEKCJA 34. O ZASTOSOWANIU DZIEDZICZENIA.<br />
________________________________________________________________<br />
Z tej lekcji dowiesz się, do czego w praktyce programowania<br />
szczególnie przydaje się dziedziczenie.<br />
________________________________________________________________<br />
Dzięki dziedziczeniu programista może w pełni wykorzystać gotowe<br />
biblioteki klas, tworząc własne klasy i obiekty, jako klasy<br />
pochodne wazględem "fabrycznych" klas bazowych. Jeśli bazowy<br />
zestw danych i funkcji nie jest adekwatny do potrzeb, można np.<br />
przesłonić, rozbudować, bądź przebudować bazową metodę dzięki<br />
elastyczności <strong>C++</strong>. Zdecydowana większość standardowych klas<br />
bazowych wyposażana jest w konstruktory. Tworząc klasę pochodną<br />
powinniśmy pamiętać o istnieniu konstruktorów i rozumieć sposoby<br />
przekazywania argumentów obowiązujące konstruktory w przypadku<br />
bardziej złożonej struktury klas bazowych-pochodnych.<br />
PRZEKAZANIE PARAMETRÓW DO WIELU KONSTRUKTORÓW.<br />
Klasy bazowe mogą być wyposażone w kilka wersji konstruktora.<br />
Dopóki nie przekażemy konstruktorowi klasy bazowej żadnych<br />
argumentów - zostanie wywołany (domyślny) pusty konstruktor i<br />
klasa bazowa będzie utworzona z parametrami domyślnymi. Nie<br />
zawsze jest to dla nas najwygodniejsza sytuacja.<br />
Jeżeli wszystkie, bądź choćby niektóre z parametrów, które<br />
przekazujemy konstruktorowi obiektu klasy pochodnej powinny<br />
zostać przekazane także konstruktorowi (konstruktorom) klas<br />
bazowych, powinniśmy wytłumaczyć to <strong>C++</strong>. Z tego też powodu,<br />
jeśli konstruktor jakiejś klasy ma jeden, bądź więcej<br />
parametrów, to wszystkie klasy pochodne względem tej klasy<br />
bazowej muszą posiadać konstruktory. Dla przykładu dodajmy<br />
konstruktor do naszej klasy pochodnej Cpochodna:<br />
class CBazowa1<br />
{<br />
public:<br />
CBazowa1(...);<br />
};<br />
class CBazowa2<br />
{<br />
public:<br />
CBazowa2(...);<br />
};<br />
//Konstruktor<br />
//Konstruktor<br />
class Cpochodna : public CBazowa1, CBazowa2<br />
{<br />
public:<br />
Cpochodna(...); //Konstruktor<br />
};<br />
//Lista klas<br />
- 336-
main()<br />
{<br />
Cpochodna Obiekt(...);<br />
...<br />
//Wywolanie konstruktora<br />
W momencie wywołania kostruktora obiektu klasy pochodnej<br />
Cpochodna() przekazujemy kostruktorowi argumenty. Możemy (jeśli<br />
chcemy, nie koniecznie) przekazać te argumenty konstruktorom<br />
"wcześniejszym" - konstruktorom klas bazowych. Ta możliwość<br />
okazuje się bardzo przydatna (niezbędna) w środowisku obiektowym<br />
- np. OWL i TVL. Oto prosty przykład definiowania konstruktora w<br />
przypadku dziedziczenia. Rola konstruktorów będzie polegać na<br />
trywialnej operacji przekazania pojedynczego znaku.<br />
class CBazowa1<br />
{<br />
public:<br />
CBazowa1(char znak) { cout
gdzie:<br />
lista - oznacza listę parametrów odpowiedniego konstruktora.<br />
W takiej sytuacji na liście argumentów konstruktorów klas<br />
bazowych mogą znajdować się także wyrażenia, przy założeniu, że<br />
elementy tych wyrażeń są widoczne i dostępne (np. globalne<br />
stałe, globalne zmienne, dynamicznie inicjowane zmienne globalne<br />
itp.). Konstruktory będą wykonywane w kolejności:<br />
CBazowa1 --> CBazowa2 --> Cpochodna<br />
Dzięki tym mechanizmom możemy łatwo przekazywać argumenty<br />
"wstecz" od konstruktorów klas pochodnych do konstruktorów klas<br />
bazowych.<br />
FUNKCJE WIRTUALNE.<br />
Działanie funkcji wirtualnych przypomina rozbudowę funkcji<br />
dzięki mechanizmowi overloadingu. Jeśli, zdefiniowaliśmy w<br />
klasie bazowej funkcję wirtualną, to w klasie pochodnej możemy<br />
definicję tej funkcji zastąpić nową definicją. Przekonajmy się o<br />
tym na przykładzie. Zacznijmy od zadeklarowania funkcji<br />
wirtualnej (przy pomocy słowa kluczowego virtual) w klasie<br />
bazowej. Zadeklarujemy jako funkcję wirtualną funkcję oddychaj()<br />
w klasie CZwierzak:<br />
class CZwierzak<br />
{<br />
public:<br />
void Jedz();<br />
virtual void Oddychaj();<br />
};<br />
Wyobraźmy sobie, że chcemy zdefiniować klasę pochodną CRybka<br />
Rybki nie oddychają w taki sam sposób, jak inne obiekty klasy<br />
CZwierzak. Funkcję Oddychaj() trzeba zatem będzie napisać w dwu<br />
różnych wariantach. Obiekt Ciapek może tę funkcję odziedziczyć<br />
bez zmian i sapać spokojnie, z Sardynką gorzej:<br />
class CZwierzak<br />
{<br />
public:<br />
void Jedz();<br />
virtual void Oddychaj() { cout
char imie[30];<br />
public:<br />
void Oddychaj() { cout
wirtualnej w taki sposób, funkcja we wszystkich "pokoleniach"<br />
musi mieć taki sam prototyp, tj. pobierać taką samą liczbę<br />
parametrów tych samych typów oraz zwracać wartość tego samego<br />
typu. Jeśli tak się nie stanie, <strong>C++</strong> potraktuje różne prototypy<br />
tej samej funkcji w kolejnych pokoleniach zgodnie z zasadami<br />
overloadingu funkcji. Zwróćmy tu uwagę, że w przypadku funkcji<br />
wirtualnych o wyborze wersji funkcji decyduje to, wobec którego<br />
obiektu (której klasy) funkcja została wywołana. Jeśli wywołamy<br />
funkcję dla obiektu Ciapek, <strong>C++</strong> wybierze wersję<br />
CZwierzak::Oddychaj(), natomiast wobec obiektu Sardynka zostanie<br />
zastosowana wersja CRybka::Oddychaj().<br />
W <strong>C++</strong> wskaźnik do klasy bazowej może także wskazywać na klasy<br />
pochodne, więc zastosowanie funkcji wirtualnych może dać pewne<br />
ciekawe efekty "uboczne". Jeśli zadeklarujemy wskaźnik *p do<br />
obiektów klasy bazowej CZwierzak *p; a następnie zastosujemy ten<br />
sam wskaźnik do wskazania na obiekt klasy pochodnej:<br />
p = &Ciapek; p->Oddychaj();<br />
...<br />
p = &Sardynka; p->Oddychaj();<br />
zarządamy w taki sposób od <strong>C++</strong> rozpoznania właściwej wersji<br />
wirtualnej metody Oddychaj() i jej wywołania we właściwym<br />
momencie. <strong>C++</strong> może rozpoznać, którą wersję funkcji należałoby<br />
zastosować tylko na podstawie typu obiektu, wobec którego<br />
funkcja została wywołana. I tu pojawia się pewien problem.<br />
Kompilator wykonując kompilcję programu nie wie, co będzie<br />
wskazywał pointer. Ustawienie pointera na konkretny adres<br />
nastąpi dopiero w czasie wykonania programu (run-time).<br />
Kompilator "wie" zatem tylko tyle:<br />
p->Oddychaj()(); //która wersja Oddychaj() ???<br />
Aby mieć pewność, co w tym momencie będzie wskazywał pointer,<br />
kompilator musiałby wiedzieć w jaki sposób będzie przebiegać<br />
wykonanie programu. Takie wyrażenie może zostać wykonane "w<br />
ruchu programu" dwojako: raz, gdy pointer będzie wskazywał<br />
Ciapka (inaczej), a drugi raz - Sardynkę (inaczej):<br />
CZwierzak *p;<br />
...<br />
for(p = &Ciapek, int i = 0; i < 2; i++)<br />
{<br />
p->Oddychaj();<br />
p = &Sardynka;<br />
}<br />
lub inaczej:<br />
if(p == &Ciapek) CZwierzak::Oddychaj();<br />
else CRybka::Oddychaj();<br />
- 340-
Taki efekt nazywa się polimorfizmem uruchomieniowym (ang.<br />
run-time polymorphism).<br />
Overloading funkcji i operatorów daje efekt tzw. polimorfizmu<br />
kompilacji (ang. compile-time), to funkcje wirtualne dają efekt<br />
polimorfizmu uruchomieniowego (run-time). Ponieważ wszystkie<br />
wersje funkcji wirtualnej mają taki sam prototyp, nie ma innej<br />
metody stwierdzenia, którą wersję funkcji należy zastosować.<br />
Wybór właściwej wersji funkcji może być dokonany tylko na<br />
podstawie typu obiektu, do którego należy wersja funkcji-metody.<br />
Różnica pomiędzy polimorfizmem przejawiającym się na etapie<br />
kompilacji i poliformizmem przejawiającym się na etapie<br />
uruchomienia programu jest nazywana również wszesnym albo póżnym<br />
polimorfizmem (ang. early/late binding). W przypadku wystąpienia<br />
wczesnego polimorfizmu (compile-time, early binding) <strong>C++</strong> wybiera<br />
wersję funkcji (poddanej overloadingowi) do zastosowania już<br />
tworząc plik .OBJ. W przypadku późnego polimorfizmu (run-time,<br />
late binding) <strong>C++</strong> wybiera wersję funkcji (poddanej przesłanianiu<br />
- overriding) do zastosowania po sprawdzeniu bieżącego kontekstu<br />
i zgodnie z bieżącym wskazaniem pointera.<br />
Przyjrzyjmy się dokładniej zastosowaniu wskaźników do obiektów w<br />
przykładowym programie. Utworzymy hierarchię złożoną z klasy<br />
bazowej i pochodnej w taki sposób, by klasa pochodna zawierała<br />
jakiś unikalny element - np. nie występującą w klasie bazowej<br />
funkcję.<br />
class CZwierzak<br />
{<br />
public:<br />
void Jedz();<br />
virtual void Oddychaj() {cout
taki element klasy pochodnej, który nie został odziedziczony i<br />
którego nie ma w klasie bazowej? Rozwiązanie jest proste -<br />
wystarczy zarządać od <strong>C++</strong>, by chwilowo zmienił typ wskaźnika z<br />
obiektów klasy bazowej na obiekty klasy pochodnej. W przypadku<br />
funkcji Szczekaj() w naszym programie wyglądałoby to tak:<br />
CZwierzak *p;<br />
...<br />
p->Oddychaj();<br />
p->Szczekaj(); //ŹLE !<br />
(CPiesek*)p->Szczekaj(); //Poprawnie<br />
...<br />
Dzięki funkcjom wirtualnym tworząc klasy bazowe pozwalamy<br />
późniejszym użytkownikom na rozbudowę funkcji-metod w<br />
najwłaściwszy ich zdaniem sposób. Dzięki tej "nieokreśloności"<br />
dziedzicząc możemy przejmować z klasy bazowej tylko to, co nam<br />
odpowiada. Funkcje w <strong>C++</strong> mogą być jeszcze bardziej<br />
"nieokreślone" i rozbudowywalne. Nazywają się wtedy funkcjami w<br />
pełni wirtualnymi.<br />
- 342-
LEKCJA 35. FUNKCJE WIRTUALNE i KLASY ABSTRAKCYJNE.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, co mawia żona programisty, gdy<br />
nie chce być obiektem klasy abstrakcyjnej.<br />
________________________________________________________________<br />
FUNKCJE W PEŁNI WIRTUALNE (PURE VIRTUAL).<br />
W skrajnych przypadkach wolno nam umieścić funkcję wirtualną w<br />
klasie bazowej nie definiując jej wcale. W klasie bazowej<br />
umieszczamy wtedy tylko deklarację-prototyp funkcji. W<br />
następnych pokoleniach klas pochodnych mamy wtedy pełną swobodę<br />
i możemy zdefiniować funkcję wirtualną w dowolny sposób -<br />
adekwatny dla potrzeb danej klasy pochodnej. Możemy np. do klasy<br />
bazowej (ang. generic class) dodać prototyp funkcji wirtualnej<br />
funkcja_eksperymentalna() nie definiując jej w (ani wobec)<br />
klasie bazowej. Sens umieszczenia takiej funkcji w klasie<br />
bazowej polege na uzyskaniu pewności, iż wszystkie klasy<br />
pochodne odziedziczą funkcję funkcja_eksperymentalna(), ale<br />
każda z klas pochodnych wyposaży tę funkcję we własną definicję.<br />
Takie postępowanie może okazać się szczególnie uzasadnione przy<br />
tworzeniu biblioteki klas (class library) przeznaczonej dla<br />
innych użytkowników. <strong>C++</strong> w wersji instalacyjnej posiada już<br />
kilka gotowych bibliotek klas. Funkcje wirtuale, które nie<br />
zostają zdefiniowane - nie posiadają zatem ciała funkcji -<br />
nazywane są funkcjami w pełni wirtualnymi (ang. pure virtual<br />
function).<br />
O KLASACH ABSTRAKCYJNYCH.<br />
Jeśli zadeklarujemy funkcję CZwierzak::Oddychaj() jako funkcję w<br />
pełni wirtualną, oprócz słowa kluczowego virtual, trzeba tę<br />
informację w jakiś sposób przekazać kompilatorowi <strong>C++</strong>. Aby <strong>C++</strong><br />
wiedział, że naszą intencją jest funkcja w pełni wirtalna, nie<br />
możemy zadeklarować jej tak:<br />
class CZwierzak<br />
{<br />
...<br />
public:<br />
virtual void Oddychaj();<br />
...<br />
};<br />
a następnie pominąć definicję (ciało) funkcji. Takie<br />
postępowanie <strong>C++</strong> uznałby za błąd, a funkcję - za zwykłą funkcję<br />
wirtualną, tyle, że "niedorobioną" przez programistę. Naszą<br />
intencję musimy zaznaczyć już w definicji klasy w taki sposób:<br />
class CZwierzak<br />
{<br />
- 343-
...<br />
public:<br />
virtual void Oddychaj() = 0;<br />
...<br />
};<br />
Informacją dla kompilatora, że chodzi nam o funkcję w pełni<br />
wirtualną, jest dodanie po prototypie funkcji "= 0". Definiując<br />
klasę pochodną możemy rozbudować funkcję wirtualną np.:<br />
class CZwierzak<br />
{<br />
...<br />
public:<br />
virtual void Oddychaj() = 0;<br />
...<br />
};<br />
class CPiesek : public CZwierzak<br />
{<br />
...<br />
public:<br />
void Oddychaj() { cout
}<br />
W tym pokoleniu definicja wirtualnej metody Mow() mogłaby<br />
wyglądać np. tak:<br />
void Zona::Mow(void)<br />
{<br />
cout
eturn 0;<br />
}<br />
Przykładowa klasa CZŁOWIEK jest klasą ABSTRAKCYJNĄ. Jeśli<br />
spróbujesz dodać do powyższego programu np.:<br />
CZLOWIEK Facet;<br />
Facet.Jedz();<br />
uzyskasz komunikat o błędzie:<br />
Cannot create a variable for abstract class "CZLOWIEK"<br />
(Nie mogę utworzyć zmiennych dla klasy abstrakcyjnej "CZLOWIEK"<br />
[???] KLASY ABSTRAKCYJNE.<br />
________________________________________________________________<br />
* Po klasach abstrakcyjnych MOŻNA dziedziczyć!<br />
* Obiektów klas abstrakcyjnych NIE MOŻNA stosować bezpośrednio!<br />
________________________________________________________________<br />
Ponieważ wyjaśniliśmy, dlaczego klasy są nowymi typami danych,<br />
więc logika (i sens) innej rozpowszechnionej nazwy klas<br />
abstrakcyjnych - ADT - Abstract Data Type (Abstrakcyjne Typy<br />
Danych) jest chyba zrozumiała i oczywista.<br />
ZAGNIEŻDŻANIE KLAS I OBIEKTÓW.<br />
Może się np. zdarzyć, że klasa stanie się wewnętrznym elementem<br />
(ang. member) innej klasy i odpowiednio - obiekt - elementem<br />
innego obiektu. Nazywa się to fachowo "zagnieżdżaniem" (ang.<br />
nesting). Jeśli, dla przykładu klasa CB będzie zawierać obiekt<br />
klasy CA:<br />
class CA<br />
{<br />
int liczba;<br />
public:<br />
CA() { liczba = 0; }<br />
CA(int x) { liczba = x; }<br />
//Konstruktor domyslny<br />
void operator=(int n) { liczba = n }<br />
};<br />
class CB<br />
{<br />
CA obiekt;<br />
public:<br />
CB() { obiekt = 1; }<br />
};<br />
Nasze klasy wyposażyliśmy w konstruktory i od razu poddaliśmy<br />
overloadingowi operator przypisania = . Aby prześledzić<br />
kolejność wywoływania funkcji i sposób przekazywania parametrów<br />
pomiędzy tak powiązanymi obiektami rozbudujemy każdą funkcję o<br />
- 346-
zgłoszenie na ekranie.<br />
class CA<br />
{<br />
int liczba;<br />
public:<br />
CA() { liczba = 0; cout program<br />
-> CA(), CA_O::liczba = 0 ->operator ->Konstruktor CB()<br />
Skoro oprócz zainicjowania obiektu klasy pochodnej nie robimy w<br />
programie dokładnie nic, nie dziwmy się ostrzeżeniu<br />
- 347-
Warning: Obiekt is never used...<br />
Jest to sytuacja trochę podobna do komunikacji pomiędzy<br />
konstruktorami klas bazowych i pochodnych. Jeśli zaprojektujemy<br />
prostą strukturę klas:<br />
class CBazowa<br />
{<br />
private:<br />
int liczba;<br />
public:<br />
CBazowa() { liczba = 0}<br />
CBazowa(int n) { liczba = n; }<br />
};<br />
class CPochodna : public CBazowa<br />
{<br />
public:<br />
CPochodna() { liczba = 0; }<br />
CPochodna(int x) { liczba = x; }<br />
};<br />
problem przekazywania parametrów między konstruktorami klas<br />
możemy w <strong>C++</strong> rozstrzygnąć i tak:<br />
class CPochodna : public CBazowa<br />
{<br />
public:<br />
CPochodna() : CBazowa(0) { liczba = 0; }<br />
CPochodna(int x) { liczba = x; }<br />
};<br />
Będzie to w praktyce oznaczać wywołanie konstruktora klasy<br />
bazowej z przekazanym mu argumentem 0. Podobnie możemy postąpić<br />
w stosunku do klas zagnieżdżonych:<br />
[P130.CPP]<br />
#include "iostream.h"<br />
class CA<br />
{<br />
int liczba;<br />
public:<br />
CA() { liczba = 0; cout
};<br />
main()<br />
{<br />
CB Obiekt;<br />
return 0;<br />
}<br />
Eksperymentując z dwoma powyższymi programami możesz przekonać<br />
się, jak przebiega przekazywanie parametrów pomiędzy<br />
konstruktorami i obiektami klas bazowych i pochodnych.<br />
JESZCZE RAZ O WSKAŹNIKU *this.<br />
Szczególnie ważnym wskaźnikiem przy tworzeniu klas pochodnych i<br />
funkcji operatorowych może okazać się pointer *this. Oto<br />
przykład listy.<br />
[P131.CPP]<br />
# include "string.h"<br />
# include "iostream.h"<br />
class CLista<br />
{<br />
private:<br />
char *poz_listy;<br />
CLista *poprzednia;<br />
public:<br />
CLista(char*);<br />
CLista* Poprzednia() { return (poprzednia); };<br />
void Pokazuj() { cout
char TAB[70];<br />
cin >> TAB;<br />
if (strncmp(TAB, ".", 1) == 0) break;<br />
CLista *lista = new CLista(TAB);<br />
if (ostatni != NULL)<br />
ostatni->Dodaj(*lista);<br />
ostatni = lista;<br />
}<br />
for(; ostatni != NULL;)<br />
{<br />
ostatni->Pokazuj();<br />
CLista *temp = ostatni;<br />
ostatni = ostatni->Poprzednia();<br />
delete (temp);<br />
}<br />
return 0;<br />
}<br />
Z reguły to kompilator nadaje wartość wskaźnikowi this i to on<br />
automatycznie dba o przyporządkowanie pamięci obiektom. Pointer<br />
this jest zwykle inicjowany w trakcie działania konstruktora<br />
obiektu.<br />
- 350-
LEKCJA 36. KAŹDY DYSK JEST ZA MAŁY, A KAŹDY PROCESOR ZBYT<br />
WOLNY...<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak komputer dysponuje swoimi<br />
zasobami w środowisku tekstowym (DOS).<br />
________________________________________________________________<br />
Truizmy użyte w tytule mają znaczyć, że "zasoby najlepszego<br />
nawet komputera są ograniczone" i zwykle okazują się<br />
wystarczające tylko do pewnego momentu. Najbardziej newralgiczne<br />
zasoby to:<br />
* czas mikroprocesora i<br />
* miejsce w pamięci operacyjnej.<br />
Tworzone przez nas programy powinny wystrzegać się zatem<br />
najcięższych grzechów:<br />
* nie pozwalać mikroprocesorowi na słodkie nieróbstwo;<br />
Rzadko uzmysławiamy sobie, że oczekiwanie na naciśnięcie<br />
klawisza przez użytkownika (czasem po przeczytaniu napisu na<br />
ekranie) trwa sekundy (1, 2, .... czasem 20), a każda sekunda<br />
lenistwa PC to stracone miliony cykli mikroprocesora.<br />
* oszczędnie korzystać z pamięci dyskowej, a szczególnie<br />
oszczędnie z pamięci operacyjnej RAM.<br />
MODELE PAMIĘCI IBM PC.<br />
Jak zapewne wiesz, Twój PC może mieć:<br />
* pamięć ROM (tylko do odczytu),<br />
* konwencjonalną pamięć RAM (640 KB),<br />
* pamięć rozszerzoną EMS i XMS,<br />
* pamięć karty sterownika graficznego ekranu (np. SVGA-RAM),<br />
* pamięć Cache dla buforowania operacji dyskowych.<br />
Najczęściej stosowane modele pamięci to:<br />
* Small - mały,<br />
* Medium - średni,<br />
* Compact - niewielki (tu mam wątpliwość, może "taki sobie" ?),<br />
* Large - duży,<br />
* Huge - jeszcze większy, odległy.<br />
Dodatkowo może wystąpić<br />
* Tiny - najmniejszy.<br />
Taki podział został spowodowany segmentacją pamięci komputera<br />
przez procesory Intel 8086 i podziałem pamięci na bloki o<br />
wielkości 64 KB. Model Small (Tiny, jeśli jest) jest najszybszy,<br />
- 351-
ale najmniej pojemny. Model Huge - odwrotnie - najpojemniejszy,<br />
za to najwolniejszy. Model Tiny powoduje ustawienia wszystkich<br />
rejestrów segmentowych mikroprocesora na tę samą wartość<br />
(początek tej samej stronicy pamięci) i umieszczenie wszystkich<br />
zasobów programu wewnątrz wspólnego obszaru pamięci o wielkości<br />
nie przekraczającej 64 KB. Wszystkie skoki są wtedy "krótkie", a<br />
wszystkie pointery (adresy) 16-bitowe. Kompilacja z<br />
zastosowaniem modelu Tiny pozwala uzyskać program wykonywalny w<br />
wersji *.COM (a nie *.EXE). Ale niestety nie wszystkie programy<br />
mieszczą się w 64 KB. W modelu Small segment kodu jest jeden<br />
(kod max. 64 K) i segment danych też tylko jeden (dane max. 64<br />
K), ale są to już dwa różne segmenty. Zestawienia<br />
najważniejszych parametrów poszczególnych modeli pamięci<br />
przedstawia tabelka poniżej:<br />
Modele pamięci komputera IBM PC.<br />
________________________________________________________________<br />
Model Segment kodu Segment danych *dp *cp<br />
________________________________________________________________<br />
Tiny 1 1 (CS = DS) 16 bit 16 bit<br />
Small 1 1 16 bit 16 bit<br />
Medium wiele 1 16 bit 32 bit<br />
Compact 1 wiele 32 bit 16 bit<br />
Large wiele wiele 32 bit 32 bit<br />
Huge wiele wiele 32 bit 32 bit<br />
________________________________________________________________<br />
*dp - data pointer - wskaźnik do danych (near/far)<br />
*cp - code pointer - wskaźnik do kodu.<br />
Large - kod + dane = max. 1 MB.<br />
Huge - kod = max. 1 MB, wiele segmentów danych po 64 K każdy.<br />
Wynikające z takich modeli pamięci kwalifikatory near, far, huge<br />
dotyczące pointerów w <strong>C++</strong> nie są akceptowane przez standard ANSI<br />
C (ponieważ odnoszą się tylko do IBM PC i nie mają charakteru<br />
uniwersalnego). Trzeba tu zaznaczyć, że typ wskaźnika jest przez<br />
kompilator przyjmowany domyślnie (automatycznie) zgodnie z<br />
wybranym do kompilacji modelem pamięci. Jeśli poruszamy się<br />
wewnątrz niewielkiego obszaru pamięci, możesz "forsować" bliższy<br />
typ pointera, przyspieszając tym samym działanie programów:<br />
huge *p;<br />
...<br />
near *ptr;<br />
...<br />
near int Funkcja(...)<br />
{<br />
...<br />
//Bliski pointer<br />
//Bliska funkcja<br />
- 352-
}<br />
#define ILE (1024*640)<br />
near unsigned int Funkcja(void)<br />
{<br />
huge char *ptr; // tu długi pointer jest niezbędny<br />
long suma = 0;<br />
for (p = 0; p < ILE; p++) suma += *p;<br />
return (suma);<br />
}<br />
Zarówno zadeklarowanie funkcji jako bliskiej (near), jak i jako<br />
statycznej (static) powoduje wygenerowanie uproszczonej<br />
sekwencji wywołania funkcji przez kompilator. Daje to w efekcie<br />
mniejszy i szybszy kod wynikowy.<br />
IDENTYFIKACJA KLAWISZY.<br />
Znane Ci z pliku i "klasyczne" funkcje<br />
obsługi konsoli mają pewne zalety. Korzystanie z klasycznych,<br />
nieobiektowych mechanizmów powoduje z reguły wygenerowanie<br />
znacznie krótszego kodu wynikowego. Funkcje scanf() i gets()<br />
wymagają wciśnięcia klawisza [Enter]. Dla szybkiego dialogu z<br />
komputerem znacznie bardziej nadają się szybsze getch() i<br />
kbhit(). Ponieważ klawiatura zawiera także klawisze specjalne<br />
(F1 ... F10, [Shift], [Del], itp.), pełną informację o stanie<br />
klawiatury można uzyskać za pośrednictwem funkcji bioskey(),<br />
korzystającej z przerywania BIOS Nr 16. Oto krótki przykład<br />
zastosowania funkcji bioskey():<br />
#include "bios.h"<br />
#include "ctype.h"<br />
#include "stdio.h"<br />
#include "conio.h"<br />
# define CTRL 0x04<br />
# define ALT 0x08<br />
# define RIGHT 0x01<br />
# define LEFT 0x02<br />
int klawisz, modyfikatory;<br />
void main()<br />
{<br />
clrscr();<br />
printf("Funkcja zwraca : %d", bioskey(1));<br />
printf("\n Nacisnij klawisz ! \n");<br />
while (!bioskey(1));<br />
printf("Funkcja zwrocila: %c", bioskey(1));<br />
printf("\nKod: %d", (char)bioskey(1));<br />
...<br />
A to jeszcze inny sposób korzystania z tej bardzo przydatnej<br />
funkcji, tym razem z innymi parametrami:<br />
/* Funkcja z parametrem (0) zwraca kod klawisza: ------ */<br />
- 353-
klawisz = bioskey(0);<br />
/* Funkcja sprawdza stan klawiszy specjalnych --------- */<br />
modyfikatory = bioskey(2);<br />
if (modyfikatory)<br />
{<br />
printf("\n");<br />
if (modyfikatory & RIGHT) printf("RIGHT");<br />
if (modyfikatory & LEFT) printf("LEFT");<br />
if (modyfikatory & CTRL) printf("CTRL");<br />
if (modyfikatory & ALT) printf("ALT");<br />
printf("\n");<br />
}<br />
/* drukujemy pobrany klawisz */<br />
if (isalnum(klawisz & 0xFF))<br />
printf("'%c'\n", klawisz);<br />
else<br />
printf("%#02x\n", klawisz);<br />
}<br />
Należy tu zwrócić uwagę, że funkcje kbhit() i bioskey() nie<br />
dokonują czyszczenia bufora klawiatury. Identyfikują znak<br />
(znaki) w buforze, ale pozostawiają bufor w stanie niezmienionym<br />
do dalszej obróbki. Zwróć uwagę, że funkcja getch() może<br />
oczekiwać na klawisz w nieskończoność. Sprawdzić szybciej, czy<br />
użytkownik nacisnął już cokolwiek możesz np. tak:<br />
if (kbhit()) ...; if (!kbhit()) ...;<br />
while (!bioskey(1)) ... if (bioskey(1)) ...;<br />
Inną wielce przydatną "szybką" funkcją jest getch(). Oto<br />
praktyczny przykład pobierania i testowania naciśniętych<br />
klawiszy klawiatury.<br />
[P131.CPP]<br />
# include "stdio.h"<br />
# include "conio.h"<br />
char z1, z2;<br />
void Odczyt(void)<br />
{<br />
z2 = '\0';<br />
z1 = getch();<br />
if (z1 == '\0') z2 = getch();<br />
}<br />
main()<br />
{<br />
clrscr();<br />
- 354-
printf("\nKropka [.] = Quit");<br />
printf("\nRozpoznaje klawisze [F1] ... [F3] \n\n");<br />
for (;;)<br />
{<br />
while(!kbhit());<br />
Odczyt();<br />
if (z1 == '.') break;<br />
if (z1 != '\0') printf("\nZnak: %c", z1);<br />
else<br />
switch (z2)<br />
{<br />
case ';' : printf("\n F1"); break;<br />
case '
RightArrow M 77<br />
UpArrow H 72<br />
DownArrow P 80<br />
Ctrl + PgDn v 118<br />
Ctrl + PgUp Ń (?) 132<br />
Ctrl + Home w 119<br />
Ctrl + End u 117<br />
________________________________________________________________<br />
Wyprowadzanie znaków na ekran można przeprowadzić szybciej<br />
posługując się przerywaniem DOS INT 29H. Drukowanie na ekranie w<br />
trybie tekstowym przebiega wtedy szybciej niż robią to<br />
standardowe funkcje , , czy .<br />
Poniżej prosty przykład praktyczny wykorzystania przerywania<br />
29H:<br />
[P132.CPP]<br />
# include <br />
# include <br />
# pragma inline<br />
void SpeedBox(int, int, int, int, char);<br />
main()<br />
{<br />
clrscr();<br />
for (; !kbhit(); )<br />
{<br />
int x = rand() % 40;<br />
int y = rand() % 12;<br />
SpeedBox(x, y, (80 - x), (24 - y), ('€' + x % 50));<br />
}<br />
return 0;<br />
}<br />
void SpeedBox(int x1, int y1, int x2, int y2, char znak)<br />
{<br />
int k;<br />
for (; y1 < y2; y1++) { gotoxy(x1, y1);<br />
for (k = x1; k < x2; k++)<br />
{<br />
asm MOV AL, znak<br />
asm INT 29H<br />
}<br />
}<br />
}<br />
[Z]<br />
________________________________________________________________<br />
1. Opracuj program pozwalający porównać szybkość wyprowadzania<br />
- 356-
danych na ekran monitora różnymi technikami (cout, puts(),<br />
printf(), asm).<br />
2. Porównaj wielkość plików wynikowych .EXE powstających w<br />
różnych wariantach z poprzedniego zadania.<br />
________________________________________________________________<br />
- 357-
LEKCJA 37. O <strong>C++</strong>, Windows i małym Chińczyku.<br />
czyli:<br />
KTO POWIEDZIAŁ, ŻE PROGRAMOWANIE DLA WINDOWS JEST TRUDNE?!!!<br />
Jak świat światem ludzie przekazują sobie sądy, opinie,<br />
poglądy... W ciągu naszej nowożytnej ery wymyślono już wiele<br />
opinii, które krążyły przez dziesięcio- i stulecia gwarantując<br />
jednym komfort psychiczny (- Ja przecież mam swoje zdanie na ten<br />
temat!), innym dając pozory wiedzy (- Tak, ja coś o tym wiem,<br />
słyszałem, że...). Żywotność takich ćwierćprawd, uproszczeń,<br />
uogólnień, czy wręcz kompletnie bzdurnych mitów była i jest<br />
zadziwiająca.<br />
Podejmę tu próbę obalenia funkcjonującego powszechnie przesądu,<br />
że<br />
- <strong>Programowanie</strong> dla Windows jest trudne. (BZDURA!!!)<br />
Aby nie zostać całkowicie posądzonym o herezję, przyznaję na<br />
wstępie dwa bezsporne fakty.<br />
Po pierwsze, wielu powszechnie szanowanych ludzi zrobiło wiele,<br />
by już pierwszymi przykładami (zwykle na co najmniej dwie<br />
strony) skutecznie odstraszyć adeptów programowania dla Windows.<br />
No bo jak tu nie stracić zapału, gdy program piszący tradycyjne<br />
"Hello World." w okienku ma 2 - 3 stronice i jeszcze zawiera<br />
kilkadziesiąt zupełnie nieznanych i niezrozumiałych słów<br />
(skrótów? szyfrów?).<br />
Po drugie, wszystko jest trudne, gdy brak odpowiednich narzędzi.<br />
Nawet odkręcenie małej śrubki bywa niezwykle trudne, gdy do<br />
dyspozycji mamy tylko młotek... Napisanie aplikacji okienkowej<br />
przy pomocy Turbo Pascal 6, Turbo C, Quick C, czy QBASIC<br />
rzeczywiście BYŁO nadwyraz trudne.<br />
I tu właśnie dochodzimy do sedna sprawy:<br />
(!!!) <strong>Programowanie</strong> dla Windows BYŁO trudne (!!!)<br />
UWAGA!<br />
Pierwsza typowa aplikacja dla Windows napisana w BORLAND <strong>C++</strong> 3/4<br />
może wyglądać np. tak:<br />
#include <br />
void main()<br />
{<br />
cout
TAK!.<br />
W BORLAND <strong>C++</strong> 3+ ... 4+ wystarczy dobrać parametry pracy<br />
kompilatora i zamiast aplikacji DOS-owskiej otrzymamy program<br />
wyposażony we własne okienko, paski przewijania w okienku,<br />
klawisze, menu, ikonkę, itp., itd.!<br />
O MAŁYM CHIŃCZYKU, czyli - NAJLEPIEJ ZACZĄĆ OD POCZĄTKU...<br />
Istnieje jedyny sprawdzony sposób rozwiązywania zagadnień<br />
takiego typu - tzw. METODA MAŁEGO CHIŃCZYKA.<br />
WSZYSCY DOSKONALE WIEDZĄ, że język chiński jest szalenie trudny.<br />
Dlatego też mimo ogromnego wysiłku prawie NIKOMU nie udaje się<br />
biegle nauczyć chińskiego - z jednym wyjątkiem - wyjątkiem<br />
małego Chińczyka. Dlaczego? To proste. Mały Chińczyk po prostu o<br />
tym nie wie! I dlatego już po kilku latach doskonale swobodnie<br />
włada tym bodaj najtrudniejszym językiem świata!<br />
Jeśli zatem komuś udało się przekonać Cię, szanowny Czytelniku,<br />
że programowanie dla Windows jest trudne, namawiam Cię na<br />
dokonanie pewnego eksperymentu intelektualnego. Spróbuj<br />
zapomnieć, że masz już na ten temat jakieś zdanie i wczuj się w<br />
rolę małego Chińczyka. Co roku udaje się to wielu milionom<br />
przyszłych ekspertów od wszystkich możliwych języków świata (<strong>C++</strong><br />
jest chyba znacznie łatwiejszy do chińskiego).<br />
BORLAND <strong>C++</strong> aby dopomóc programiście w jego ciężkiej pracy<br />
tworzy (często automatycznie) wiele plików pomocniczych. Krótkie<br />
zestawienie plików pomocniczych zawiera tabela poniżej.<br />
Najważniejsze pliki pomocnicze w kompilatorach <strong>Borland</strong>/Turbo<br />
<strong>C++</strong>.<br />
________________________________________________________________<br />
Rozszerzenie Przeznaczenie Gdzie/Uwagi<br />
________________________________________________________________<br />
.C .CPP Teksty żródłowe \EXAMPLES \SOURCE<br />
(ASCII) (przykłady) (kod żródł.)<br />
.H .HPP .CAS Pliki nagłówkowe \INCLUDE<br />
(ASCII)<br />
.PRJ .DPR .IDE Projekty \EXAMPLES \SOURCE<br />
.TAH .TCH .TDH<br />
.TFH .HLP .HPJ<br />
.RTF<br />
.DSK .TC .CFG<br />
.DSW .BCW<br />
.DEF .RC .RES<br />
Help<br />
Konfiguracyjne<br />
Zasoby i definicje<br />
- 359-
.RH .ICO .BMP<br />
.BGI .CHR .RTF<br />
Grafika DOS, fonty<br />
.MAK .NMK .GEN Pliki instruktażowe dla<br />
MAKEFILE MAKE.EXE<br />
.ASM .INC .ASI<br />
Do asemblacji (ASCII)<br />
.RSP<br />
.LIB .DLL<br />
.TOK<br />
.DRV<br />
.OVL<br />
Instruktażowy dla TLINK<br />
Biblioteki<br />
Lista słów zastrzeżonych (reserved words)<br />
(ASCII)<br />
Sterowniki (drivery)<br />
Nakładki (overlay)<br />
.SYM Plik ze skompilowanymi (Pre - compiled)<br />
plikami nagłówkowymi.<br />
________________________________________________________________<br />
Świadome i umiejętne wykorzystanie tych plików może znacznie<br />
ułatwić i przyspieszyć pracę.<br />
Po wprowadzeniu na rynek polskiej wersji Windows 3.1 okienka<br />
zaczęły coraz częściej pojawiać się w biurach i domach, i<br />
stanowią coraz częściej naturalne (właśnie tak, jak chiński dla<br />
Chińczyków) środowisko pracy dla polskich użytkowników PC. Nie<br />
pozostaje nam nic innego, jak po prostu zauważyć i uznać ten<br />
fakt.<br />
Po uruchomieniu <strong>Borland</strong> <strong>C++</strong> (2 * klik myszką, lub rozkaz Uruchom<br />
z menu Plik) zobaczymy tradycyjny pulpit (desktop)<br />
zintegrowanego środowiska IDE - podobny do Turbo Pascala, z<br />
tradycyjnym układem głównego menu i okien roboczych.<br />
Skoro mamy zająć się tworzeniem aplikacji dla Windows- zaczynamy<br />
od rozwinięcia menu Options i wybieramy z menu rozkaz<br />
Application... . Rozwinie się okienko dialogowe. Przy pomocy<br />
klawiszy możemy wybrać sposób generowania aplikacji - dla DOS,<br />
dla Windows lub tworzenie bibliotek statycznych .LIB, czy też<br />
dynamicznych .DLL. Wybieramy oczywiście wariant [Windows EXE].<br />
[!!!]UWAGA!<br />
________________________________________________________________<br />
Struktura podkatalogów i wewnętrzna organizacja pakietów 3.0,<br />
3.1, 4 i 4.5 ZNACZNIE SIĘ RÓŻNI.<br />
________________________________________________________________<br />
- 360-
Skoro ustawiliśmy już poprawnie najważniejsze dla nas parametry<br />
konfiguracyjne - możemy przystąpić do uruchomienia pierwszej<br />
aplikacji dla Windows.<br />
PIERWSZA APLIKACJA "specjalnie dla Windows".<br />
Tryb postępowania z kompilatorem BORLAND <strong>C++</strong> 3.0/3.1 będzie w<br />
tym przypadku dokładnie taki sam, jak np. z Turbo Pascalem.<br />
Wszystkich niezbędnych zmian w konfiguracji kompilatora już<br />
dokonaliśmy. Kompilator "wie" już, że chcemy uzyskać w efekcie<br />
aplikację dla Windows w postaci programu .EXE. Możemy zatem<br />
* Wydać rozkaz File | New<br />
Pojawi się nowe okienko robocze. Zwróć uwagę, że domyślne<br />
rozszerzenie jest .CPP, co powoduje domyślne zastosowanie<br />
kompilatora <strong>C++</strong> (a nie kompilatora C - jak w przypadku plików z<br />
rozszerzeniem .C). Możesz to oczywiście zmienić, jeśli zechcesz,<br />
posługując się menu Options | Compiler | <strong>C++</strong> options... (Opcje |<br />
Kompilator | Kompilator C albo <strong>C++</strong>). W tym okienku dialogowym<br />
masz sekcję:<br />
Use <strong>C++</strong> Compiler: Zastosuj Kompilator <strong>C++</strong><br />
(zamiast kompilatora C)<br />
(.) CPP extention - tylko dla rozszerzenia .CPP<br />
( ) <strong>C++</strong> always - zawsze<br />
* Wybierz rozkaz Save as... z menu File<br />
Pojawi się okienko dialogowe "Save File As" (zapis pliku pod<br />
wybraną nazwą i w wybranym miejscu).<br />
* Do okienka edycyjnego wpisz nazwę pliku i pełną ścieżkę<br />
dostępu - np. A:\WIN1.CPP lub C:\C-BELFER\WIN1.CPP<br />
Zmieni się tytuł roboczego okna z NONAME00 na wybraną nazwę<br />
Możemy wpisać tekst pierwszego programu:<br />
[P133.CPP]<br />
#include <br />
void main()<br />
{<br />
cout
W okienku komunikatów (Messages) powinien pojawić się w trakcie<br />
konsolidacji komunikat ostrzegawczy:<br />
*Linker Warning: No module definition file specified:<br />
using defaults<br />
Oznacza to: Konsolidator ostrzega, że brak specjalnego<br />
stowarzyszonego z plikiem .CPP tzw. pliku definicji sposobu<br />
wykorzystania zasobów Windows - .DEF. Program linkujący<br />
zastosuje wartości domyślne.<br />
Jeśli w IDE wersji kompilatora przeznaczonej dla środowiska DOS<br />
spróbujesz uruchomić program WIN1.EXE w tradycyjny sposób -<br />
rozkazem Run z menu Run - na ekranie pojawi się okienko z<br />
komunikatem o błędzie (Error message box):<br />
Can't run a Windows EXE file<br />
D:\WIN1.EXE<br />
[ OK ]<br />
czyli: "Nie mogę uruchomić pliku EXE dla Windows".<br />
Jak już napisałem wcześniej, kompilatory <strong>C++</strong> w pakietach 3.0/3.1<br />
mają swoje ulubione specjalności:<br />
<strong>Borland</strong> <strong>C++</strong><br />
Turbo <strong>C++</strong><br />
- jest zorientowany na współpracę z DOS<br />
- jest zorientowany na współpracę z Windows<br />
w wersji 3.1:<br />
BCW -<br />
BC -<br />
dla Windows<br />
dla DOS<br />
nie oznacza to jednak, że będą kłopoty z pracą naszego programu!<br />
Wyjdź z IDE BC/BCW.<br />
Z poziomu Menedżera Programów możesz uruchomić swój program<br />
rozkazem Plik | Uruchom. Do okienka musisz oczywiście wpisać<br />
poprawną ścieżkę do pliku WIN1.EXE (czyli katalog wyjściowy<br />
kompilatora <strong>Borland</strong> <strong>C++</strong>).<br />
*** Wybierz z menu głównego Menedżera Programów (pasek w górnej<br />
części ekranu) rozkaz Plik. Rozwinie się menu Plik.<br />
*** Wybierz z menu Plik rozkaz Uruchom. Pojawi się okienko<br />
dialogowe uruchamiania programów. Wpisz pełną ścieżkę<br />
dostępu do programu - np.:<br />
D:\KATALOG\WIN1.EXE<br />
i "kliknij" myszką na klawiszu [OK] w okienku.<br />
Na ekranie pojawi się okno naszej aplikacji. Okno jest<br />
- 362-
wyposażone w:<br />
- Pasek z tytułem (Caption) - np.: A:\WIN1.EXE ;<br />
- Klawisz zamykania okna i rozwinięcia standardowego menu (tzw.<br />
menu systemowego Windows) - [-] ;<br />
- Paski przewijania poziomego i pionowego;<br />
- Klawisze MINIMIZE i MAXIMIZE (zmniejsz do ikonki | powiększ na<br />
cały ekran) w prawym górnym narożniku okna;<br />
Program znajduje się w wersji .EXE na dyskietce dołączonej do<br />
książki. Możesz uruchomić go z poziomu Menedżera Plików (Windows<br />
File Manager), Menedżera Programów (Windows Program Manager) lub<br />
z DOS-owskiego wiersza rozkazów (DOS Command Line):<br />
C\>WIN A:\WIN1.EXE[Enter]<br />
Co może nasza pierwsza aplikacja?<br />
- Typową dla Windows techniką drag and drop - pociągnij i upuść<br />
możesz przy pomocy myszki przesuwać okno naszej pierwszej<br />
aplikacji po ekranie ("ciągnąc" okno za pasek tytułowy).<br />
- Ciągnąc ramki bądź narożniki możesz zmieniać wymiary okna w<br />
sposób dowolny.<br />
- Posługując się paskami przewijania możesz przewijać tekst w<br />
oknie w pionie i w poziomie.<br />
- Miżesz zredukować okno do ikonki.<br />
- Możesz uruchomić naszą aplikację wielokrotnie i mieć na<br />
ekranie kilka okien programu WIN1.EXE.<br />
- Nasza aplikacja wyposażona jest w menu systemowe. Możesz<br />
rozwinąć menu i wybrać z menu jeden z kilku rozkazów.<br />
Jeśli nie pisałeś jeszcze programów dla Windows - możesz być<br />
trochę zaskoczony. Gdzie w naszym programie jest napisane np. -<br />
co powinno znaleść się w menu??? Odpowiedź jest prosta -<br />
nigdzie. Podobnie jak programy tworzone dla DOS korzystają w<br />
niejawny sposób z zasobów systemu - standardowych funkcji DOS,<br />
standardowych funkcji BIOS, przerywań, itp - tak programy<br />
tworzone dla Windows mogą w niejawny sposób korzystać z zasobów<br />
środowiska Windows - standardowego menu, standardowych okien,<br />
standardowych klawiszy, itp.. Takie zasoby udostępniane przez<br />
środowisko programom aplikacyjnym nazywają się interfejsem API<br />
(Application Program Interface). Poznałeś już API DOS'a - jego<br />
przerywania i funkcje. Interfejs Windows nazywa się "Windows<br />
API" i to z jego gotowych funkcji właśnie korzystamy.<br />
Uruchom program wielokrotnie (min. 4 razy). Wykonaj 4 - 6 razy<br />
czynności oznaczone powyżej trzema gwiazdkami *** . Ponieważ nie<br />
- 363-
zażądaliśmy, by okno programu było zawsze "na wierzchu" (on top)<br />
- po każdym kolejnym uruchomieniu (nie musisz nic robić -<br />
nastąpi to automatycznie - zadba o to Menedżer Windows)<br />
poprzednie okno programu zniknie. Jeśli po czwartym (piątym)<br />
uruchomieniu programu zredukujesz okno Menedżera Programów do<br />
ikony (np. [-] i "do ikony" z menu systemowego) - okaże się, że<br />
"pod spodem" stale widać kaskadę okien naszej aplikacji WIN1.EXE<br />
(patrz rys. poniżej). Na rysunkach poniżej przedstawiono kolejne<br />
stadia pracy z naszą PIERWSZĄ APLIKACJĄ.<br />
Aplikacja WIN1.EXE została wyposażona w ikonkę systemową (znane<br />
Ci okienko). Ikonka jest transparentna (półprzezroczysta) i<br />
możemy ją także metodą drag and drop przenieść w dowolne miejsce<br />
- np. do roboczego okna naszej aplikacji. Zwróć uwagę także na<br />
towarzyszący nazwie programu napis "inactive" (nieaktywna).<br />
Chodzi o to, że program zrobił już wszystko, co miał do<br />
zrobienia i zakończył się. DOS dołożyłby standardowo funkcję<br />
zwolnienia pamięci i zakończył program. W Windows niestety<br />
okienko nie zamknie się samo w sposób standardowy. W Windows,<br />
jak wiesz, możemy mieć otwarte jednocześnie wiele okien<br />
programów a aktywne jest (tylko jedno) zawsze to okno, do<br />
którego przekażemy tzw. focus. Okno to można rozpoznać po<br />
ciemnym pasku tytułowym. Właśnie z przyjęcia takiego sposobu<br />
podziału zasobów Windows pomiędzy aplikacje wynika skutek<br />
praktyczny - okno nie zamknie się automatycznie po zakończeniu<br />
programu - lecz wyłącznie na wyrażne życzenie użytkownika. API<br />
Windows zawiera wiele gotowych funkcji (np. CloseWindow() -<br />
zamknij okno, DestroyWindow() - skasuj okno i in.), z których<br />
może skorzystać programista pisząc aplikację. Nie jesteśmy więc<br />
całkiem bezradni.<br />
Spróbuj skompilować w opisany wyżej sposób i uruchomić pierwszą<br />
aplikację w takiej wersji:<br />
#include <br />
void main()<br />
{<br />
printf(" Pierwsza Aplikacja \n Dla MS Windows ");<br />
}<br />
Jak łatwo się przekonać, całkowicie klasyczny, w pełni<br />
nieobiektowy program WIN1.C będzie w Windows działać dokładnie<br />
tak samo. Nasze aplikacje nie muszą bynajmniej być całkowicie<br />
obiektowe, chociaż zastosowanie obiektowej techniki<br />
programowania pozwala zmusić nasz komputer do zdecydowanie<br />
wydajniejszego działania.<br />
PODSUMUJMY:<br />
- 364-
* Jeśli korzystamy wyłącznie ze standardowych zasobów środowiska<br />
Windows, tworzenie aplikacji dla Windows nie musi być wcale<br />
trudniejsze od tworzenia aplikacji DOS'owskich.<br />
* Czy aplikacja ma być przeznaczona dla DOS, czy dla Windows<br />
możemy zdecydować "w ostatniej chwili" ustawiając odpowiednio<br />
robocze parametry kompilatora <strong>C++</strong>:<br />
Options | Applications... | DOS standard<br />
albo<br />
Options | Applications... | Windows EXE<br />
* Aplikacje skompilowane do wersji DOS'owskiej możemy uruchamiać<br />
wewnątrz kompilatora DOS'owskiego rozkazem Run | Run.<br />
* Aplikacje skompilowane (ściślej - skonsolidowane) do wersji<br />
okienkowej możemy uruchamiać wewnątrz Windows z poziomu<br />
Menedżera Plików bądź Menedżera Programów rozkazem Uruchom z<br />
menu Plik.<br />
* Dodatkowe pliki nagłówkowe .H i biblioteki .LIB .DLL znajdują<br />
się w katalogach<br />
\BORLANDC\INCLUDE<br />
\BORLANDC\OWL\INCLUDE<br />
\BORLANDC\LIB<br />
\BORLANDC\OWL\LIB<br />
Ścieżki dostępu do tych katalogów należy dodać do roboczych<br />
katalogów kompilatora w okienku Options | Directories...<br />
* Aplikacje nie korzystające z funkcji Windows API nie muszą<br />
dołączać okienkowych plików nagłówkowych. Jeśli jednak<br />
zechcemy zastosować funkcje i dane (stałe, struktury,<br />
obiekty, itp.) wchodzące w skład:<br />
- Windows API<br />
- Windows Stock Objects - obiekty "ze składu Windows"<br />
- biblioteki klas Object Windows Library<br />
należy dołączyć odpowiedni plik nagłówkowy:<br />
#include <br />
#include <br />
#include <br />
TYPOWE BŁĘDY I KŁOPOTLIWE SYTUACJE:<br />
* Należy pamiętać o ustawieniu właściwych katalogów roboczych<br />
kompilatora Options | Directories...<br />
* Przy bardziej skomplikowanych aplikacjach może wystąpić<br />
potrzeba dobrania innego (zwykle większego) modelu pamięci.<br />
Modelem domyślnym jest model Small. Inne parametry pracy<br />
kompilatora ustawia się podobnie za pomocą menu Options.<br />
________________________________________________________________<br />
- 365-
LEKCJA 38. KORZYSTAMY ZE STANDARDOWYCH ZASOBÓW Windows.<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak korzystać z zasobów<br />
Windows bez potrzeby wnikania w wiele szczególów technicznych<br />
interfejsu aplikacji - Windows API.<br />
________________________________________________________________<br />
Ponieważ nasze programy mogą korzystać ze standardowych zasobów<br />
Windows, na początku będziemy posługiwać się okienkami<br />
standardowymi. Począwszy od aplikacji WIN3.EXE "rozszerzymy<br />
ofertę" do dwu podstawowych typów:<br />
* Standardowe główne okno programu (Default main window).<br />
To takie właśnie okno, jakie dostały nasze pierwsze aplikacje<br />
WIN1.EXE.<br />
* Okienkiem dialogowym (Dialog box),<br />
a dokładniej najprostszym rodzajem okienek dialogowych - tzw.<br />
okienkami komunikatów - Message Box.<br />
Zastosowanie okienka dialogowego pozwoli nam na wprowadzenie do<br />
akcji klawiszy (buttons).<br />
________________________________________________________________<br />
UWAGA:<br />
Niestety nie wszystkie tradycyjne funkcje typu printf(),<br />
scanf(), gets() itp. zostały zaimplementowane dla Windows!<br />
Pisząc własne programy możesz przekonać się o tym dzięki opisowi<br />
funkcji w Help. Funkcję należy odszukać w Help | Index. Oprócz<br />
przykładu zastosowania znajdziesz tam tabelkę typu:<br />
DOS Unix Windows ANSI C <strong>C++</strong> Only<br />
cscanf Yes<br />
fscanf Yes Yes Yes Yes<br />
scanf Yes Yes Yes<br />
sscanf Yes Yes Yes Yes<br />
[Yes] oznacza "zaimplementowana". Dlatego właśnie w dalszych<br />
programach przykładowych dla wersji 3.0 należy np. stosować np.<br />
makro getchar() zamiast tradycyjnego getch() zaimplementowane<br />
dla Windows już w wersji B<strong>C++</strong> 3.0.<br />
________________________________________________________________<br />
Dla przykładu spróbujmy skompilować i uruchomić w środowisku<br />
Windows jeden z wcześniejszych programów - tabliczkę mnożenia.<br />
Zwróć uwagę na dołączony dodatkowy plik WINDOWS.H i nowy typ<br />
wskaźnika. Zamiast zwykłego<br />
char *p ...<br />
LPSTR p ...<br />
LPSTR - to Long Pointer to STRing - daleki wskaźnik do łańcucha<br />
tekstowego. Jest to jeden z "ulubionych" typów Windows.<br />
- 366-
* WIN2.CPP: */<br />
/* - Tablica dwuwymiarowa<br />
- Wskazniki do elementów tablicy */<br />
#include <br />
#include <br />
#include <br />
int T[10][10], *pT, i, j, k;<br />
char spacja = ' ';<br />
LPSTR p1 = " TABLICZKA MNOZENIA (ineksy)\n";<br />
LPSTR p2 = " Inicjujemy i INKREMENTUJEMY wskaznik:\n";<br />
LPSTR p3 = "... nacisnij cokolwiek (koniec)...";<br />
void main()<br />
{<br />
printf(p1);<br />
for (i = 0; i < 10; i++)<br />
{<br />
for (j = 0; j < 10; j++)<br />
{ T[i][j] = (i + 1)*(j + 1);<br />
if (T[i][j] < 10) cout
Zastosujemy teraz najprostszy typ okienka dialogowego - okienko<br />
kamunikatów (Message Box), nasze następne aplikacje mogą być już<br />
nie jedno- a dwupoziomowe. Typowe postępowanie okienkowych<br />
aplikacji bywa takie:<br />
* program wyświetla w głównym oknie to, co ma do powiedzenia;<br />
* aby zadawać pytania stosuje okienka dialogowe, bądź okienka<br />
komunikatów;<br />
* funkcja okienkowa (u nas MessageBox()) zwraca do programu<br />
decyzję użytkownika;<br />
* program główny analizuje odpowiedź i podejmuje w głównym oknie<br />
stosowne działania.<br />
Prześledźmy ewolucję powstającej w taki sposób aplikacji.<br />
STADIUM 1. Tekst w głównym oknie.<br />
Zaczniemy tworzenie naszej aplikacji tak:<br />
/* WINR1.CPP: */<br />
/* Stadium 1: Dwa okienka w jednym programie */<br />
# include <br />
# include <br />
char *p1 = "Teraz dziala \n funkcja \n MessageBox()";<br />
char *p2 = "START";<br />
int wynik;<br />
void main()<br />
{<br />
printf(" Start: Piszemy w glownym oknie \n");<br />
printf(" ...nacisnij cosik...");<br />
getchar();<br />
MessageBox(0, p1, p2, 0);<br />
printf("\n\n\n Hello World dla WINDOWS!");<br />
printf("\n\t...dowolny klawisz... ");<br />
getchar();<br />
}<br />
Moglibyśmy zrezygnować z metod typowych dla aplikacji DOSowskich<br />
i zatrzymania (i zapytania) makrem getchar() (odpowiednik<br />
getch() dla Windows). To działanie możemy z powodzeniem<br />
powierzyć funkcji okienkowej MessageBox(). Funkcja MessageBox()<br />
pobiera cztery parametry:<br />
int Message Box(hwndParent, lpszText, lpszTitle, Style)<br />
HWND hwndParent - identyfikator macieżystego okna (głównego okna<br />
aplikacji). Ponieważ nie wiemy póki co pod jakim numerem<br />
(identyfikatorem) Windows zarejestrują naszą aplikację -<br />
- 368-
wpisujemy 0<br />
LPCSTR lpszText - daleki wskaźnik do łańcucha tekstowego<br />
wewnątrz okienka.<br />
LPCSTR lpszTitle - daleki wskażnik do łańcucha tekstowego -<br />
tytułu okienka komunikatu.<br />
UINT Style - UINT = unsigned int; numer określający zawartość<br />
okienka.<br />
int Return Value - identyfikator klawisza, który wybrał<br />
użytkownik w okienku komunikatu.<br />
[!!!] UWAGA<br />
________________________________________________________________<br />
Deklaracje wskaźników do tekstów powinny wyglądać tak:<br />
LPCSTR p1 = "Napis1", p2 = "Tekst2";<br />
ale <strong>C++</strong> może samodzielnie dokonać forsowania typów i zamienić<br />
typ char* na typ LPCSTR (lub LPSTR).<br />
________________________________________________________________<br />
/* WINR2.CPP: */<br />
/* Stadium 2: Dwa okienka ze zmienną zawarością */<br />
# include <br />
# include <br />
char *p2, *p1 = "Dopisywanie:";<br />
char napisy[4][20] = { "<strong>Borland</strong> ", "<strong>C++</strong> ", "dla ", "Windows" };<br />
void main()<br />
{<br />
printf("\n\n\n Hello World dla WINDOWS!");<br />
printf("\n AUTOR: ...................");<br />
for( int i = 0; i < 4; i++)<br />
{<br />
p2 = &napisy[i][0];<br />
MessageBox(0, p2, p1, MB_OK);<br />
printf("\n %s", napisy[i]);<br />
}<br />
MessageBox(0, "I to juz \n wszystko...", "KONIEC", MB_OK);<br />
}<br />
W tym stadium stosujemy:<br />
- główne okno aplikacji<br />
- dwa okienka komunikatów (Dopisywanie i KONIEC)<br />
- jeden klawisz - [OK]<br />
Łańcuchy tekstowe przeznaczone do pola tekstowego okienka<br />
pobierane są z tablicy napisy[4][20] (cztery napisy po max. 20<br />
znaków) przy pomocy wskaźnika p2. MB_OK to predefiniowana stała<br />
(Message Box OK - key identifier - identyfikator klawisza [OK]<br />
dla okienek komunikatów).<br />
/* WINR3.CPP: */<br />
/* Stadium 3: Dwa okienka sterują pętlą */<br />
- 369-
# include <br />
# include <br />
char *p2, *p1 = "Dopisywanie:";<br />
char napisy[4][20] = { "<strong>Borland</strong> ", "<strong>C++</strong> ", "dla ", "Windows" };<br />
void main()<br />
{<br />
printf("\n\n\n Hello World dla WINDOWS!");<br />
printf("\n AUTOR: ...................");<br />
for( int i = 0; i < 4; i++)<br />
{<br />
p2 = &napisy[i][0];<br />
if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)<br />
printf("\n %s", napisy[i]);<br />
else<br />
printf("\n ...?");<br />
}<br />
MessageBox(0, "I to juz \n wszystko...", "KONIEC", MB_OK);<br />
}<br />
W tym stadium stosujemy:<br />
- główne okno aplikacji<br />
- dwa okienka komunikatów (Dopisywanie i KONIEC)<br />
- dwa klawisze - [OK] i [Anuluj] (OK/Cancel)<br />
- jedną ikonę [STOP]<br />
Zwróć uwagę, że tym razem sprawdzamy, który klawisz wybrał<br />
użytkownik w okienku. Odbywa się to tak:<br />
if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)<br />
IDOK jest predefiniowaną stałą - kodem klawisza [OK] (ang.<br />
OK-key IDentifier - identyfikator klawisza OK). Identyfikatory<br />
różnych zasobów Windows są liczbami całkowitymi. Jeśli jesteś<br />
dociekliwy Czytelniku, możesz sprawdzić - jaki numer ma klawisz<br />
[OK] rozbudowując tekst aplikacji np. tak:<br />
int Numer;<br />
...<br />
Numer = MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL);<br />
printf("\nKlawisz [OK] ma numer: %d", Numer);<br />
if(Numer == IDOK) ...<br />
Zwróć uwagę na sposób wykorzystania zasobów w funkcji<br />
MessageBox(). Identyfikatory zasobów, które chcemy umieścić w<br />
okienku są wpisywane jako ostatni czwarty argument funkcji i<br />
mogą być sumowane przy pomocy znaku | (ang. ORing), np.:<br />
MessageBox(0,..,.., MB_ICONSTOP | MB_OKCANCEL);<br />
- 370-
oznacza umieszczenie ikony STOP i klawiszy [OK] i [Anuluj]. Kod<br />
zwracany przez funkcję może być wykorzystywany we wszelkich<br />
konstrukcjach warunkowych (switch, case, for, while, if-else,<br />
itp.).<br />
/* WINR4.CPP: */<br />
/* Stadium 4: Okienka sterują 2 pętlami, przybywa zasobów. */<br />
# include <br />
# include <br />
char *p2, *p1 = "Dopisywanie:";<br />
char *p3 = "I to by bylo na tyle...\n Konczymy ???";<br />
char *p4 = "UWAGA: KONIEC ?";<br />
char napisy[5][20] = { "<strong>Borland</strong> ", "<strong>C++</strong> ", "dla ", "Microsoft",<br />
"Windows" };<br />
main()<br />
{<br />
printf("\n\n\n Grafoman dla WINDOWS!");<br />
printf("\n AUTOR: (jak wyzej)");<br />
puts("_____________________________\n");<br />
do<br />
{<br />
for( int i = 0; i < 5; i++)<br />
{<br />
p2 = &napisy[i][0];<br />
if( MessageBox(0, p2, p1, MB_ICONSTOP | MB_OKCANCEL) == IDOK)<br />
printf("\n %s", napisy[i]);<br />
else<br />
printf("\n ...?");<br />
}<br />
} while<br />
(MessageBox(0,p3,p4,MB_ICONQUESTION | MB_OKCANCEL)==IDCANCEL);<br />
return 0;<br />
}<br />
W tym stadium stosujemy:<br />
- główne okno aplikacji<br />
- dwa okienka komunikatów (Dopisywanie i KONIEC)<br />
- dwa klawisze - [OK] i [Anuluj] (OK/Cancel)<br />
- dwie ikonki [STOP] i [PYTAJNIK]<br />
Tekst jest przewijany w głównym oknie programu i po zakończeniu<br />
roboczej części programu i przejściu w stan nieaktywny<br />
(inactive) możesz przy pomocy paska przewijania pionowego<br />
obejrzeć napisy - historię Twoich zmagań z programem. Zwróć<br />
uwagę, że pojemność głównego okna jest ograniczona. Jeśli<br />
napisów będzie zbyt dużo, tekst przewinięty poza okno może<br />
ulegać obcięciu (ang clip on). Zwróć również uwagę na<br />
naprzemienne przekazywanie aktywności (focus) pomiędzy oknami<br />
aplikacji:<br />
- 371-
MainWindow MessageBox<br />
Warto w tym momencie zwrócić uwagę na kilka typowych dla<br />
okienkowych aplikacji mechanizmów.<br />
* Jeśli naciśniemy klawisz na klawiaturze, bądź klawisz myszki,<br />
obsługa takiego zdarzenia może następować na dwa sposoby.<br />
Najpierw Windows pobierają kod klawisza i dokonują<br />
"kolejkowania" (podobnie jak DOS-owski bufor klawiatury).<br />
Następnie przekazują kod klawisza aplikacji do obsługi. Jeśli<br />
aplikacja czeka na klawisz i potrafi obsłużyć takie zdarzenie<br />
(np. funkcja MessageBox(), bądź makro getchar(), czy operator<br />
cin >> w programie głównym), obsługa zdarzenia zostaje<br />
zakończona. Jeśli aplikacja nie potrafi obsłużyć zdarzenia -<br />
obsługa przekazywaba jest stadardowym funkcjom obsługi (Event<br />
Handler) Windows.<br />
* Kiedy na ekranie pojawia się okienko dialogowe (tu:<br />
komunikatów) zostaje mu przekazany tzw. focus - czyli aktywność.<br />
Naciśnięcie [Entera] spowoduje zadziałanie tego klawisza w<br />
okienku, który właśnie ten focus otrzymał (tu zwykle pierwszego<br />
z lewej).<br />
* jeśli naciśniemy klawisz specjalny, którego obsługę w sposób<br />
standardowy powinny załatwiać Windows - obsługa takiego<br />
zdarzenia zostaje przekazana domyślnej funkcji Windows (ang.<br />
Default Event Handler). Tak jest w przypadku klawiszy ze<br />
strzałkami (przewijanie w oknie), [Tab], [Alt]+[F4], itp.<br />
/* WINR5.CPP: */<br />
/* Stadium 5: Zmiana wielkości i nazwy okienka. */<br />
# include <br />
# include <br />
# include <br />
char tytul[80] = "Dopisywanie: ";<br />
char *p0, *p2;<br />
char *p1 = "UWAGA: Ponawianie proby \n oznacza: WYDRUKUJE I<br />
ZAPYTAM";<br />
char *p3 = "I to by bylo na tyle...\n Konczymy ???";<br />
char *p4 = "UWAGA: KONIEC ?";<br />
char napisy[5][20] = { "<strong>Borland</strong> ", "<strong>C++</strong> ", "dla ", "Microsoft",<br />
"Windows" };<br />
main()<br />
{<br />
cout
{<br />
p2 = &napisy[i][0];<br />
strcat(p0, p2);<br />
int decyzja = MessageBox(0, p1, p0, MB_ICONHAND |<br />
MB_ABORTRETRYIGNORE);<br />
if (decyzja == IDABORT) break;<br />
else<br />
if (decyzja == IDRETRY)<br />
{<br />
cout
LEKCJA 39. STRUKTURA PROGRAMU PROCEDURALNO - ZDARZENIOWEGO<br />
PRZEZNACZONEGO DLA WINDOWS.<br />
________________________________________________________________<br />
W trakcie tej lekcji poznasz ogólną budowę interfejsu API<br />
Windows i dowiesz się, co z tego wynika dla nas - autorów<br />
programów przeznaczonych dla Windows.<br />
________________________________________________________________<br />
W przeciwieństwie do długich liniowych programów przeznaczonych<br />
dla DOS, w naszych programach dla Windows będziemy pisać coś<br />
na kształt krótkich odcinków programu i przekazywać sterowanie<br />
Windows. Jest to bardzo ważna cecha - kod programu jest zwykle<br />
silnie związany z Windows w taki sposób, że użytkownik może w<br />
dużym stopniu decydować o sposobie (kolejności) wykonywania<br />
programu. Praktycznie robi to poprzez wybór opcji-klawiszy w<br />
dowolnej kolejności. Przy takiej filozofii w dowolnym momencie<br />
powinniśmy mieć możliwość przełączenia się do innego programu<br />
(innego okna) i nasz program powinien (bez zauważalnej zwłoki)<br />
przekazać sterowanie, nie zagarniając i nie marnując czasu CPU.<br />
Z tego powodu kod programu powinien być bardzo<br />
"zmodularyzowany". Każda sekcja kodu powinna być odseparowana i<br />
każda, po wykonaniu powinna przekazywać sterowanie do Windows.<br />
NOTACJA WĘGIERSKA I NOWE TYPY DANYCH.<br />
Tworzenie zdarzeniowych programów dla Windows wymaga kilku<br />
wstępnych uwag na temat nowych typów danych. Okienkowe typy są<br />
definiowane w plikach nagłówkowych (WINDOWS.H, WINDOWSX.H, OWL.H<br />
itp) i mają postać najczęściej struktury, bądź klasy. Typowe<br />
sposoby deklaracji w programach okienkowych są następujące:<br />
HWND hWnd - WiNDow Handle - identyfikator okna<br />
HWND hWnd - typ (predefiniowany), hWnd - zmienna<br />
HINSTANCE hInstance - Instance Handle - identyfikator danego<br />
wystąpienia (uruchomienia) programu<br />
PAINTSTRUCT - struktura graficzna typu PAINTSTRUCT<br />
ps - nasza robocza struktura (zmienna)<br />
WNDCLASS - struktura (a nie klasa wbrew mylącej nazwie)<br />
POINT - struktura (współrzędne punktu - piksela na ekranie)<br />
RECT - struktura (współrzędne prostokąta)<br />
BOOL - typ signed int wykorzystywany jako flaga (TRUE/FALSE)<br />
WORD - unsigned int<br />
DWORD - unsigned long int<br />
LONG - long int<br />
HANDLE, HWND, HINSTANCE - unsigned int (jako nr - identyfikator)<br />
UINT - j. w. - unsigned int.<br />
W programach okienkowych stosuje się wiele predefiniowanych<br />
stałych, których znaczenie sugeruje przedrostek i nazwa, np:<br />
WM_CREATE - Windows Message: Create! - Komunikat Windows:<br />
Utworzyć! (np. okno)<br />
WS_VISIBLE - Window Style: Visible - Styl Okna: Widoczne<br />
- 374-
ID_...<br />
MB_...<br />
- IDentifier - IDentyfikator<br />
- Message Box - elementy okienka komunikatów<br />
W środowisku Windows stosuje się specjalną notację nazwaną od<br />
narodowości swojego wynalazcy Karoja Szimoni - notacją<br />
węgierską. Sens notacji węgierskiej polega na dodaniu do nazwy<br />
zmiennej określonych liter jako przedrostka (prefix).<br />
Litery-przedrostki stosowane w notacji węgierskiej zebrano w<br />
Tabeli poniżej. Pomiędzy nazewnictwem Microsofta a <strong>Borland</strong>a<br />
istnieją wprawdzie drobne rozbieżności, ale ogólne zasady można<br />
odnieść zarówno do BORLAND <strong>C++</strong> 3+...4+, jak i Microsoft <strong>C++</strong><br />
6...7, czy Visual <strong>C++</strong>.<br />
Notacja węgierska<br />
________________________________________________________________<br />
Prefix Skrót ang. Znaczenie<br />
________________________________________________________________<br />
a array tablica<br />
b bool zmienna logiczna (0 lub 1)<br />
by unsigned char znak (bajt)<br />
c char znak<br />
cb count of bytes liczba bajtów<br />
cr color reference value określenie koloru<br />
cx, cy short (count x, y len.) x-ilość, y-długość (short)<br />
dw unsigned long liczba długa bez znaku<br />
double word podwójne słowo<br />
fn function funkcja<br />
pfn pointer to function wsk. do funkcji<br />
h handle "uchwyt" - identyfikator<br />
i integer całkowity<br />
id identifier identyfikator<br />
n short or int krótki lub całkowity<br />
np near pointer wskaźnik bliski<br />
p pointer wskaźnik<br />
l long długi<br />
lp long pointer wskaźnik typu long int<br />
lpfn l. p. to function daleki wskaźn. do funkcji<br />
s string łańcuch znaków<br />
sz string terminated '\0' łańcuch ASCIIZ<br />
tm text metric miara tekstowa<br />
w unsigned int (word) słowo<br />
x,y short x,y coordinate współrzędne x,y (typ: short)<br />
________________________________________________________________<br />
O PROGRAMOWANIU PROCEDURALNO - ZDARZENIOWYM DLA WINDOWS.<br />
W proceduralno-sekwencyjnych programach DOS'owskich sterowanie<br />
jest przekazywane mniej lub bardziej kolejno kolejnym<br />
instrukcjom w taki sposób, jak życzył sobie tego programista. W<br />
Windows program-aplikacja prezentuje użytkownikowi wszystkie<br />
dostępne opcje w formie widocznych na ekranie obiektów (visual<br />
objects) do wyboru przez użytkownika. Program funkcjonuje zatem<br />
według zupełnie innej koncepcji nazywanej "programowaniem<br />
zdarzeniowym" (ang. event-driven programming). Można powiedzieć,<br />
- 375-
że za przebieg wykonania programu nie jest odpowiedzialny tylko<br />
programista lecz część tej odpowiedzialności przejmuje<br />
użytkownik i to on decyduje w jaki sposób przebiega wykonanie<br />
programu. Użytkownik może wybrać w dowolnym momencie dowolną<br />
spośród wszystkich oferowanych mu opcji a program powinien<br />
zawsze zareagować poprawnie i równie szybko. Jest oczywiste, że<br />
pisząc program nie możemy przewidzieć w jakiej kolejności<br />
użytkownik będzie wybierał opcje/rozkazy z menu. Przeciwnie<br />
powiniśmy napisać program w taki sposób by dla każdego rozkazu<br />
istniał oddzielny kod. Jest to ogólna koncepcja, na której<br />
opiera się programowanie zdarzeniowe.<br />
W przeciwieństwie do programów proceduralno - sekwencyjnych,<br />
które należy czytać od początku do końca, programy dla Windows<br />
muszą zostać pocięte na na mniejsze fragmenty - sekcje - na<br />
zasadzie jedna sekcja - obsługa jednego zdarzenia. Jeśli<br />
zechcesz wyświetlić napis "Hello, World", sekcja zdarzeniowego<br />
programu obsługująca takie zdarzenie może wyglądać np. tak:<br />
Funkcja_Obsługi_Komunikatów_o_Zdarzeniach(komunikat)<br />
{<br />
switch (komunikat_od_Windows)<br />
{<br />
case WM_CREATE:<br />
...<br />
TextOut(0, 0, "Napis: np. Hello world.", dlugosc_tekstu);<br />
break;<br />
...<br />
case WM_CLOSE:<br />
.... break;<br />
..................... itd.<br />
}<br />
// CLOSE - zamknąć okno<br />
a w przypadku obiektowego stylu programowania - metoda<br />
obsługująca to zdarzenie (należąca np. do obiektu<br />
Obiekt_Główne_Okno - TMainWindow) może wyglądać np. tak:<br />
void TMainWindow::RysujOkno()<br />
{<br />
TString Obiekt_napis = "Hello, World";<br />
int dlugosc_tekstu = sizeof(Obiekt_napis);<br />
TextOut(DC, 10, 10, Obiekt-napis, dlugosc_tekstu);<br />
}<br />
Taki fragment kodu programu jest specjalnie przeznaczony do<br />
obsługi jednego zdarzenia (ewent-ualności). W okienku wykonuje<br />
się operacja PAINT (maluj). "Malowanie" okna może się odbywać<br />
albo po raz pierwszy, albo na skutek przesunięcia. Programy<br />
zdarzeniowe tworzone w <strong>C++</strong> dla Windows będą zbiorem podobnych<br />
"kawałków" następujących w tekście programu sekcja za sekcją.<br />
Oto jak działa program zdarzeniowy: kod programu, podzielony na<br />
sekcje obsługujące poszczególne zdarzenia koncentruje się wokół<br />
interfejsu.<br />
- 376-
FUNKCJE WinMain() i WindowProc().<br />
W programach pisanych w standardowym C dla Windows używane są<br />
dwie najważniejsze funkcje: WinMain() i WindowProc().<br />
________________________________________________________________<br />
UWAGA:<br />
Funkcji WindowProc() można nadać dowolną nazwę, ale WinMain()<br />
musi się zawsze nazywać WinMain(). Jest to nazwa zastrzeżona<br />
podobnie jak main() dla aplikacji DOSowskich.<br />
________________________________________________________________<br />
Funkcja WinMain() powoduje utworzenie okna programu umożliwiając<br />
zdefiniowanie i zarejestrowanie struktury "okno" (struct<br />
WNDCLASS) a następnie powoduje wyświetlenie okna na ekranie. Od<br />
tego momentu zarządzanie przejmuje funkcja WindowProc(). W<br />
typowej proceduralno - zdarzeniowej aplikacji dla Windows to<br />
właśnie funkcja WindowProc() obsługuje pobieranie informacji od<br />
użytkownika (np. naciśnięcie klawisza lub wybór z menu). Funkcja<br />
WindowProc() robi to dzięki otrzymywaniu tzw. komunikatów (ang.<br />
Windows message).<br />
W Windows zawsze po wystąpieniu jakiegoś zdarzenia (event)<br />
następuje przesłanie komunikatu (message) o tym zdarzeniu do<br />
bieżącego aktywnego w danym momencie programu w celu<br />
poinformowania go, co się stało. Jeśli został naciśnięty<br />
klawisz, komunikat o tym zdarzeniu zostanie przesłany do funkcji<br />
WindowProc(). Tak funkcjonuje interfejs pomiędzy aplikacją a<br />
Windows. W programach tworzonych w C prototyp funkcji<br />
WindowProc() wygląda następująco:<br />
LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message,<br />
WORD wParam, LONG lParam);<br />
Słowa FAR i PASCAL oznaczają, że:<br />
FAR - kod funkcji znajduje się w innym segmencie niż kod<br />
programu;<br />
PASCAL - kolejność odkładania argumentów na stos - odwrotna (jak<br />
w Pascalu).<br />
________________________________________________________________<br />
UWAGA:<br />
Prototyp funkcji może zostać podany również tak:<br />
LONG FAR PASCAL WndProc(HWND, unsigned, WORD, LONG);<br />
________________________________________________________________<br />
- 377-
Pierwszy parametr hWnd jest to tzw. identyfikator okna (ang.<br />
window handle). Ten parametr zawiera informację, dla którego<br />
okna przeznaczony jest komunikat. Zastosowanie takiego<br />
identyfikatora jest celowe, ponieważ funkcje typu WindowProc()<br />
mogą obsługiwać przesyłanie komunikatów do wielu okien. Jeśli<br />
okien jest wiele, okno jest identyfikowane przy pomocy tego<br />
właśnie identyfikatora (numeru).<br />
Następny parametr to sam komunikat o długości jednego słowa<br />
(word). Ten parametr przechowuje wartość z zakresu<br />
zdefiniowanego w pliku nagłówkowym WINDOWS.H. W zależności od<br />
tego co się zdarzyło, Windows mogą nam przekazać ok. 150 różnych<br />
komunikatów a w tym np.:<br />
WM_CREATE Utworzono okno<br />
WM_KEYDOWN Naciśnięto klawisz<br />
WM_SIZE Zostały zmienione wymiary okna<br />
WM_MOVE Okno zostało przesunięte<br />
WM_PAINT Okno należy narysować (powtórnie) - (re)draw<br />
WM_QUIT Koniec pracy aplikacji<br />
itp.<br />
Przedrostek WM_ to skrót od Windows Message - komunikat Windows.<br />
Wymiana komunikatów w środowisku Windows może przebiegać w różny<br />
sposób - zależnie od źródła wywołującego generację komunikatu i<br />
od charakteru zdarzenia. Ze względu na źródło można komuniakty<br />
umownie podzielić na następujące grupy:<br />
1. Działanie użytkownika (np. naciśnięcie klawisza) powoduje<br />
wygenerowanie komunikatu.<br />
2. Program - aplikacja wywołuje funkcję Windows i powoduje<br />
przesłanie komunikatu do aplikacji.<br />
3. Środowisko Windows przesyła komunikat do programu.<br />
4. Dwie aplikacje związane mechanizmem dynamicznej wymiany<br />
danych (Dinamic Data Exchange - DDE) wymieniają komunikaty.<br />
Komunikaty Windows można także podzielić umownie na następujące<br />
kategorie:<br />
1. Komunikaty dotyczące zarządzania oknami (Windows Managenent<br />
Msg.):<br />
WM_ACTIVATE (zaktywizuj lub zdezaktywizuj okno), WM_PAINT,<br />
WM_MOVE, WM_SIZE, WM_CLOSE, WM_QUIT.<br />
Bardzo istotnym szczegółem technicznym jest problem<br />
przekazywania aktywności pomiędzy oknami. Szczególnie często<br />
występuje potrzeba przekazania aktywności do elementu<br />
sterującego. Jeśli hEditWnd będzie identyfikatorem (window<br />
handle) okienka edycyjnego:<br />
- 378-
case WM_SETFOCUS:<br />
SetFocus(hEditWnd);<br />
break;<br />
funkcja SetFocus() spowoduje, że wszystkie komunikaty dotyczące<br />
zdarzeń klawiatury będą kierowane do okna sterującego, jeżeli<br />
okno macieżyste jest aktywne. Ponieważ zmiana rozmiaru okna<br />
głównego nie pociąga za sobą automatycznej zmiany rozmiaru okna<br />
sterującego, potrzebna jest dodatkowo obsługa wiadomości<br />
WM_SIZE wobec okna elementu sterującego.<br />
2. Komunikaty inicjacyjne dotyczące konstrukcji np. menu<br />
aplikacji:<br />
WM_INITMENU - zainicjuj menu (wysyłany przed zainicjowaniem),<br />
WM_INITDIALOG - zainicjuj okienko dialogowe.<br />
3. Komunikaty generowane przez Windows w odpowiedzi na wybór<br />
rozkazu z menu, zegar, bądź naciśnięcie klawisza:<br />
WM_COMMAND - wybrano rozkaz z menu,<br />
WM_KEYDOWN - naciśnięto klawisz,<br />
WM_MOUSEMOVE - przesunięto myszkę,<br />
WM_TIMER - czas minął.<br />
4. Komunikaty systemowe. Aplikacja nie musi odpowiadać na<br />
rozkazy obsługiwane przez domyślną procedurę Windows -<br />
DefWindowProc(). Szczególnie dotyczy to rozkazów nie odnoszących<br />
się do roboczego obszaru okna - Non Client Area Messages.<br />
5. Komunikaty schowka (Clipborad Messages).<br />
Sens działania funkcji WindowProc() w C/<strong>C++</strong> polega na<br />
przeprowadzeniu analizy, co się stało i podjęciu stosownej<br />
akcji. Można to realizować przy pomocy drabinki if-else-if, ale<br />
najwygodniejsze jest stosowanie instrukcji switch.<br />
LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message,<br />
WORD wParam, LONG lParam)<br />
{<br />
switch (Message)<br />
{<br />
case WM_CREATE:<br />
.....<br />
break; /* Koniec obsługi komunikatu WM_CREATE */<br />
case WM_MOVE:<br />
.... /* Kod obsługi komunikatu WM_MOVE */<br />
break; /* Koniec obsługi WM_MOVE. */<br />
case WM_SIZE:<br />
.... /* Kod obsługi sytuacji WM_SIZE */<br />
break; /* Koniec obsługi WM_SIZE */<br />
.......... /* Inne, pozostałe możliwe sytuacje */<br />
case WM_CLOSE: /* Zamknięcie okna */<br />
....<br />
- 379-
eak;<br />
default: /* wariant domyślny: standardowa obsługa<br />
.... przez standardową funkcję Windows */<br />
}<br />
}<br />
________________________________________________________________<br />
UWAGA:<br />
Ponieważ komunikatów "interesujących" daną aplikację może być<br />
ponad 100 a sposobów reakcji użytkownika jeszcze więcej, w<br />
"poważnych" aplikacjach tworzone są często struktury decyzyjne o<br />
większym stopniu złożoności. Jeśli istnieje potrzeba<br />
optymalizacji działania programów stosuje się struktury dwu<br />
typów:<br />
* hierarchia wartości (Value Tree) i<br />
* drzewo analizy zdarzeń (Event Tree).<br />
Utworzone w taki sposób tzw. "Drzewo decyzyjne" nazywane także<br />
"Drzewem analizy zdarzeń" może być wielopoziomowe. Widoczny<br />
powyżej pierwszy poziom drzewa (pierwszy przesiew) realizowany<br />
jest zwykle przy pomocy instrukcji switch a następne przy pomocy<br />
drabinek typu if-else-if-break. Schemat if-else-if-break często<br />
bywa zastępowany okienkami dialogowymi.<br />
________________________________________________________________<br />
Parametry wParam i lParam przechowują parametry istotne dla<br />
danego komunikatu. wParam ma długość pojedynczego słowa (word) a<br />
lParam ma długość podwójnego słowa (long). Jeśli, dla przykładu,<br />
okno zostało przesunięte, te parametry zawierają nowe<br />
współrzędne okna.<br />
Jeżeli program ma być programem zdarzeniowym, powinniśmy przed<br />
podjęciem jakiejkolwiek akcji zaczekać aż Windows przyślą nam<br />
komunikat o tym, jakie zdarzenie nastąpiło. Wewnątrz Windows<br />
tworzona jest dla komunikatów kolejka (ang message queue).<br />
Dzięki istnieniu kolejkowania otrzymujemy komunikaty pobierane z<br />
kolejki pojedynczo. Jeśli użytkownik przesunie okno a następnie<br />
przyciśnie klawisz, to Windows wywołają funkcję WindowProc()<br />
najpierw z parametrem WM_MOVE a następnie z parametrem<br />
WM_KEYDOWN.<br />
Jednym z najważniejszych zadań funkcji WinMain() jest utworzenie<br />
kolejki dla komunikatów i poinformowanie Windows, że komunikaty<br />
do naszego programu należy kierować pod adresem funkcji<br />
WindowProc(). W tym celu stosuje się daleki wskaźnik do<br />
procedury okienkowej lpfn (Long Pointer to Function). Poza tym<br />
funkcja WinMain() tworzy okno (okna) i wyświetla je na ekranie w<br />
pozycji początkowej. Kiedy program zostaje po raz pierwszy<br />
załadowany i uruchomiony - Windows najpierw wywołują funkcję<br />
- 380-
WinMain().<br />
Windows manipulują komunikatami posługując się strukturą MSG (od<br />
messages - komunikaty). Struktura MSG jest zdefiniowana w pliku<br />
WINDOWS.H w następujący sposób:<br />
typedef struct tagMSG<br />
{<br />
HWND hwnd;<br />
WORD message;<br />
WORD wParam;<br />
LONG lParam;<br />
DWORD time;<br />
POINT pt;<br />
} MSG;<br />
Na pierwszym polu tej struktury znajduje się "identyfikator"<br />
(kod) okna, dla którego przeznaczony jest komunikat (każdy<br />
komunikat może być przesłany tylko do jednego okna). Na drugim<br />
polu struktury przechowywany jest sam komunikat. Komunikat jest<br />
zakodowany przy pomocy predefiniowanych stałych w rodzaju<br />
WM_SIZE, WM_PAINT czy WM_MOUSEMOVE. Kolejne dwa pola służą do<br />
przechowania danych-parametrów towarzyszących każdemu<br />
komunikatowi: wParam i lParam. Na następnym polu przechowywany<br />
jest w zakodowanej postaci czas - moment, w którym wystąpiło<br />
zdarzenie. Na polu pt przechowywane są współrzędne kursora<br />
myszki na ekranie w momencie w którym został wygenerowany<br />
komunikat o wystąpieniu zdarzenia. Należy zwrócić tu uwagę, że<br />
typ POINT oznacza strukturę. Struktura POINT (punkt) w Windows<br />
wygląda tak:<br />
typedef struct tagPOINT<br />
{<br />
int x;<br />
int y;<br />
} POINT;<br />
Aby mieć pewność, że otrzymaliśmy wszystkie komunikaty, które<br />
zostały do nas skierowane, w programie wykonywana jest pętla<br />
pobierania komunikatów (message loop) wewnątrz funkcji<br />
WinMain(). Na początek wywoływana jest zwykle okienkowa (czyli<br />
należącą do Windows API) funkcja GetMessage(). Ta funkcja<br />
wypełnia strukturę komunikatów i zwraca wartość. Zwracana przez<br />
funkcję wartość jest różna od zera, jeżeli otrzymany właśnie<br />
komunikat był czymkolwiek za wyjątkiem WM_QUIT. Komunikat<br />
WM_QUIT jest komunikatem kończącym pracę każdej aplikacji dla<br />
Windows. Jeśli otrzymamy komunikat WM_QUIT powinniśmy przerwać<br />
pętlę pobierania komunikatów i zakończyć pracę funkcji<br />
WinMain(). Taka sytuacja oznacza, że więcej komunikatów nie<br />
będzie. Po uwzględnieniu tych warunków pętla może wyglądać tak:<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \<br />
LPSTR lpszCmdLine, int nCmdShow)<br />
....<br />
- 381-
while(GetMessage(&msg,NULL,0,0)) //Poki nie otrzymamy WM_QUIT<br />
{<br />
....<br />
}<br />
Po naciśnięciu przez użytkownika klawisza generowany jest<br />
komunikat WM_KEYDOWN. Jednakże z faktu otrzymania komunikatu<br />
WM_KEYDOWN nie wynika, który klawisz został przyciśnięty, czy<br />
była to duża, czy mała litera. Funkcję TranslateMessage()<br />
(PrzetłumaczKomunikat) stosuje się do przetłumaczenia komunikatu<br />
WM_KEYDOWN na komunikat WM_CHAR. Komunikat WM_CHAR przekazuje<br />
przy pomocy parametru wParam kod ASCII naciśniętego klawisza.<br />
Funkcję TranslateMessage() stosujemy w pętli pobierania<br />
komunikatów tak:<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance, \<br />
LPSTR lpszCmdLine, int nCmdShow)<br />
....<br />
while(GetMessage(&msg, 0, 0, 0))<br />
{<br />
TranslateMessage(&msg);<br />
....<br />
}<br />
W tym stadium program jest gotów do przesłania komunikatu do<br />
funkcji - procedury okienkowej WindowProc(). Posłużymy się w tym<br />
celu funkcją DispatchMessage() (ang. dispatch - odpraw, przekaż,<br />
DispatchMessage = OtprawKomunikat). Funkcja WinMain()<br />
poinformowała wcześniej Windows, że odprawiane komunikaty<br />
powinny trafić właśnie do WindowProc().<br />
while(GetMessage(&msg, NULL, NULL, NULL))<br />
{<br />
TranslateMessage(&msg);<br />
DispatchMessage(&msg);<br />
}<br />
Tak funkcjonuje pętla pobierająca komunikaty od Windows i<br />
przekazująca je funkcji WindowProc(). Pętla działa do momentu<br />
pobrania komunikatu WM_QUIT (Koniec!). Otrzymanie komunikatu<br />
WM_QUIT powoduje przerwanie pętli i zakończenie pracy programu.<br />
Komunikaty systemowe (system messages), które są kierowane do<br />
Windows także trafiają do tej pętli i są przekazywane do<br />
WindowProc(), ale ich obsługą powinna się zająć specjalna<br />
funkcja DefWindowProc() - Default Window Procedure, umieszczona<br />
na końcu (wariant default).<br />
Jest to standardowa dla aplikacji okienkowych postać pętli<br />
pobierania komunikatów.<br />
Jak widać, wymiana informacji pomiędzy użytkownikiem,<br />
środowiskiem a aplikacją przebiega tu trochę inaczej niż w DOS.<br />
Program pracujący w środowisku tekstowym DOS nie musi np.<br />
- 382-
ysować własnego okna.<br />
[Z]<br />
________________________________________________________________<br />
1. Uruchom Windows i popatrz świadomym, fachowym okiem, jak<br />
przebiega przekazywanie aktywności (focus) między okienkami<br />
aplikacji.<br />
________________________________________________________________<br />
- 383-
LEKCJA 40. JAK TWORZY SIĘ APLIKACJĘ DLA Windows?<br />
________________________________________________________________<br />
W trakcie tej lekcji dowiesz się, jak "poskładać" aplikację dla<br />
Windows z podstawowych funkcji interfejsu API i jakie komunikaty<br />
są najważniejsze dla naszych aplikacji.<br />
________________________________________________________________<br />
Przy tworzeniu programu zwróćmy szczególną uwagę na to, co<br />
dzieje się w programie po otrzymaniu komunikatu WM_PAINT (należy<br />
narysować okno). Jest to żądanie ze strony Windows, by program<br />
narysował obszar roboczy (client area) swojego okna. Program<br />
otrzyma komunikat WM_PAINT zawsze na początku, kiedy powinien<br />
narysować swoje okno po raz pierwszy i później powtórnie, za<br />
każdym razem, gdy trzeba będzie odtworzyć okno na ekranie. Jeśli<br />
inne okno przesuwane po ekranie przysłoni okno naszego programu,<br />
po odsłonięciu naszego okna Windows prześlą do programu<br />
komunikat WM_PAINT - odtwórz swoje okno - narysuj go powtórnie<br />
(redraw, repaint). Jeśli zechcemy wyprowadzić na ekran napis<br />
"Hello World" także będziemy musieli narysować okno od nowa. Nie<br />
zawsze "odświeżenia" wymaga całe okno. W każdej z sytuacji:<br />
- całe okno zostało przysłonięte i odsłonięte<br />
- część okna wymaga odświeżenia<br />
- okno jest rysowane po raz pierwszy<br />
Windows prześlą do programu ten sam komunikat - WM_PAINT.<br />
Jeśli odtworzenia wymaga tylko część okna, taka część okna<br />
nazywa się nieważną-nieaktualną (ang. invalid). W Windows takie<br />
nieaktualne fragmenty okna zawsze mają kształt prostokątów.<br />
Wyobraźmy sobie, że jakieś inne okno przesłoniło narożnik okna<br />
naszego programu. Jeśli użytkownik usunie to przesłaniające<br />
okno, odsłonięty obszar będzie potraktowany przez Windows jako<br />
nieaktualny. Windows prześlą do aplikacji komunikat WM_PAINT<br />
żądający odtworzenia okna. Żądając odtworzenia okna Windows<br />
powinny nas poinformować która część naszego okna została na<br />
ekranie "zepsuta". Współrzędne prostokąta na ekranie Windows<br />
przekażą przy pomocy specjalnej struktury nazywanej strukturą<br />
rysunku (ang. paint structure - PAINTSTRUCT).<br />
Strukturę rysunku możemy nazwać w programie np.:<br />
PAINSTRUCT ps;<br />
W funkcji WindowProc() obsługa komunikatu WM_PAINT rozpoczyna<br />
się od wyczyszczenia pól struktury rysunku ps. Struktura<br />
predefiniowanego typu PAINTSTRUCT (w WINDOWS.H) zawiera<br />
informacje o rysunku.<br />
PAINTSTRUCT ps;<br />
{<br />
switch (Message)<br />
{<br />
case WM_CREATE:<br />
..... break;<br />
case WM_MOVE:<br />
- 384-
.... break;<br />
case WM_SIZE:<br />
.... break;<br />
case WM_PAINT: /* Obsługa rysowania okna */<br />
memset(&ps, 0x00, sizeof(PAINTSTRUCT);<br />
....<br />
break; //Koniec obsługi WM_PAINT<br />
case WM_CLOSE:<br />
.... break;<br />
default: .....<br />
}<br />
}<br />
Następnie pola struktury rysunku zostają wypełnione poprzez<br />
okienkową funkcją BeginPaint() - RozpocznijRysowanie. Zwróć<br />
uwagę, że do poprawnego działania funkcji potrzebne są<br />
informacje o tym, które okno trzeba odświeżyć (Windows powinny<br />
wiedzieć wobec którego okna żądamy informacji o "zepsutym"<br />
prostokącie) i adres naszej struktury rysunku. Aby przekazać te<br />
informacje postępujemy tak:<br />
case WM_PAINT:<br />
memset(&ps, 0x00, sizeof(PAINTSTRUCT));<br />
hDC = BeginPaint(hWnd, &ps);<br />
....<br />
Teraz funkcja BeginPaint() może wypełnić naszą strukturę rysunku<br />
ps danymi. Pola struktury typu PAINTSTRUCT wyglądają<br />
następująco:<br />
typedef struct tagPAINTSTRUCT<br />
{<br />
HDC hdc;<br />
BOOL fErase;<br />
RECT rcPaint;<br />
BOOL fRestore;<br />
BYTE rgbReserved[16];<br />
} PAINTSTRUCT;<br />
Przy pomocy pola typu RECT (ang. rectangle - prostokąt) Windows<br />
przekazują do programu współrzędne wymiary (ang. dimensions)<br />
"zepsutego" na ekranie prostokąta. Typ RECT oznacza następującą<br />
strukturę:<br />
typedef struct tagRECT<br />
{<br />
int left; //współrzędna lewa - x<br />
int top; //współrzędna górna - y<br />
int right; //współrzędna prawa - x<br />
int bottom; //współrzędna dolna - y<br />
} RECT;<br />
- 385-
Górny lewy róg nieaktualnego prostokąta (invalid rectangle) ma<br />
dwie współrzędne (left, top) a dolny prawy róg prostokąta ma<br />
współrzędne (right, bottom). Te współrzędne ekranowe mierzone są<br />
w pikselach i są to współrzędne względne - względem lewego<br />
górnego narożnika okna aplikacji. Lewy górny narożnik okna<br />
aplikacji ma więc współrzędne (0,0).<br />
Zwróćmy uwagę na wartość zwracaną przez funkcję BeginPaint() -<br />
zmienną hDC:<br />
case WM_PAINT:<br />
memset(&ps, 0x00, sizeof(PAINTSTRUCT));<br />
hDC = BeginPaint(hWnd, &ps);<br />
....<br />
Wszystnie operacje graficzne będą wymagać nie kodu okna hWnd a<br />
właśnie kodu-identyfikatora kontekstowego hDC.<br />
Na początku pracy programu, gdy okno jest rysowane po raz<br />
pierwszy, Windows generują komunikat WM_PAINT i cały obszar<br />
roboczy okna jest uznawany za nieaktualny. Kiedy program otrzyma<br />
ten pierwszy komunikat, możemy wykorzystać to do umieszczenia w<br />
oknie np. napisu. Jeśli tekst ma rozpoczynać się od lewego<br />
górnego narożnika okna aplikacji, funkcja TextOut() używana w<br />
Windows do wykreślania tekstu (w trybie graficznym) powinna<br />
rozpoczynać wyprowadzanie tekstu od punktu o (pikselowych)<br />
współrzędnych (0,0).<br />
case WM_PAINT:<br />
...<br />
TextOut(hDC, 0, 0, (LPSTR) "Tekst", strlen("Tekst"));<br />
EndPaint(hWnd, &ps);<br />
break;<br />
Funkcja TextOut() (wyprowadź tekst) pobiera pięć parametrów:<br />
hDC - identyfikator-kod prostokąta, który należy narysować<br />
x - współrzędna pozioma (w pikselach)<br />
y - współrzędna pionowa początku naszego napisu<br />
W tym przypadku współrzędne wynoszą (0,0).<br />
LPSTR - wskaźnik do łańcucha znaków "Hello world."<br />
LPSTR = long pointer to string (wskaźnik typu far).<br />
Wskaźnk ten przekazujemy do funkcji poprzez forsowanie typu:<br />
... (LPSTR) "Tekst";<br />
Zgodnie z definicją typu w pliku WINDOWS.H spowoduje to zamianę<br />
wskaźnika do łańcucha typu near char* (bliski) na wskaźnik typu<br />
far (daleki). Ostatni parametr funkcji to długość wyprowadzanego<br />
tekstu - tu obliczana przez funkcję strlen().<br />
Prześledźmy etapy powstawania aplikacji.<br />
- 386-
Funkcja MainWin() rejestruje i tworzy główne okno programu oraz<br />
inicjuje globalne zmienne i struktury. Funkcja WinMain() zawiera<br />
pętlę pobierania komunikatów. Każdy komunikat przeznaczony dla<br />
głównego okna (lub ewentualnych nastepnych okien potomnych) jest<br />
pobierany, ewentualnie poddawany translacji i przekazywany do<br />
funkcji obsługującej dialog z Windows. Przed zakończeniem<br />
programu funkcja WinMain() kasuje utworzone wcześniej obiekty,<br />
zwalnia pamięć i pozostałe zasoby.<br />
UWAGA: "Obiekty" nie są tu użyte w sensie stosowanym w OOP.<br />
"Obiekt" oznacza tu np. strukturę.<br />
int PASCAL WinMain(HANDLE hInstance, hPrevInstance,<br />
LPSTR lpszCmLine, int nCmdShow)<br />
{ ...<br />
HANDLE hInstance - identyfikator bieżącego pojawienia się danej<br />
aplikacji. Ponieważ w Windows program może być uruchamiany<br />
wielokrotnie, stosuje sie pojecie tzw. "Instancji" - wystąpienia<br />
- uruchomienia programu.<br />
HANDLE hPrevInstance - identyfikator poprzedniego wystąpienia<br />
danej aplikacji<br />
LPSTR lpszCmdLine - daleki wskaźnik do parametrów wywołania<br />
programu z linii rozkazu<br />
int nCmdShow - sposób początkowego wyświetlenia okna<br />
(pełne okno, bądź ikona)<br />
Deklaracja struktury typu MSG (Message) do przechowywania<br />
komunikatów.<br />
MSG<br />
msg;<br />
Nadanie nazwy aplikacji:<br />
strcpy(szAppName, "Nazwa Aplikacji");<br />
Rejestrujemy struktury okien jeśli jest to pierwsze uruchomienie<br />
danej aplikacji i sprawdzamy, czy rejestracja powiodła się:<br />
if(!PrevInstance)<br />
{<br />
if((int nRc = RegisterClass() ...<br />
Utworzenie głównego okna programu (może się nie udać):<br />
hWndMain = CreateWindow(....);<br />
if(hWndMain == NULL)<br />
- 387-
{<br />
MessageBox(0, "Klops", "Koniec", MB_OK);<br />
return (FALSE);<br />
}<br />
Wyświetlenie głównego okna na ekranie:<br />
ShowWindow(hWndMain, nCmdShow);<br />
Pętla komunikatów wykrywająca komunikat WM_QUIT:<br />
while(GetMessage(&msg, 0, 0, 0))<br />
{<br />
TranslateMessage(&msg);<br />
DispatchMessage(&msg);<br />
}<br />
Główna procedura obsługi okna WindowProc().<br />
Instrukcja switch przełącza do odpowiedniego wariantu działania<br />
- obsługi odpowiedniego komunikatu. Muszą tu znajdować sie<br />
procedury obsługi wszystkich interesujacych nas działań<br />
uzytkownika i ogólnych komunikatow Windows (np. WM_CLOSE). Jeśli<br />
wystąpi taki komunikat, którego obsługa nie została<br />
przewidziana, obsługa jest przekazywana, do funkcji okienkowej<br />
DefWindowProc() - obsługę przejmują Windows.<br />
Komunikaty inicjowane przez użytkownika są rozpatrywane<br />
zasadniczo jako WM_COMMAND. Rozkaz wybrany z menu lub<br />
odpowiadająca mu kombinacja klawiszy jest przekazywana przy<br />
pomocy pierwszego parametru komunikatu - wParam. Kod<br />
odpowiadający rozkazowi z menu nazywa sie "control menu ID", a<br />
identyfikator kombinacji klawiszy - "accelerator ID". Procedura<br />
obsługi komunikatów powinna zawierać<br />
case (WM_COMMAND): ..... break;<br />
Wewnątrz przy pomocy instrukcji switch{...} należałoby<br />
rozpatrywać kolejne warianty, wykorzystując identyfikator<br />
wybranego z menu rozkazu - ID. Obsługa komunikatow świadczących<br />
o wyborze przez użytkownika rozkazu z menu stanowi zwykle główną<br />
roboczą cześć programu.<br />
LONG FAR PASCAL WindowProc(HWND hWnd, WORD Message, WORD wParam,<br />
LONG lParam)<br />
{<br />
HMENU hMenu=0; /* Identyfikator menu */<br />
HBITMAP hBitmap=0; /* Identyfikator mapy bitowej */<br />
HDC hDC; /* Identyfikator kontekstowy */<br />
PAINSTRUCT ps; /* Struktura rysunku */<br />
int nRc=0; /* Zwrot kodu przez funkcje */<br />
switch (message)<br />
- 388-
{<br />
case WM_CREATE:<br />
Gdy okno jest tworzone Windows przesyłają jeden raz komunikat<br />
WM_CREATE do okna. Procedura obsługi nowego okna (new window<br />
procedure) otrzymuje ten komunikat po utworzeniu okna, ale<br />
jeszcze zanim okno pojawi sie na ekranie.<br />
lParam - Wskaźnik do struktury CREATESTRUCT o postaci:<br />
typedef struct {<br />
LPSTR lpCreateParams;<br />
HANDLE hInst;<br />
HANDLE hMenu;<br />
HWND hwndParent;<br />
int cy;<br />
int cx;<br />
int y;<br />
int x;<br />
LONG style;<br />
LPSTR lpszName;<br />
LPSTR lpszClass;<br />
DWORD dwExStyle;<br />
} CREATESTRUCT; */<br />
Kod obsługi powiekszania/zmniejszania case WM_SIZE.<br />
wParam zawiera kod operacji - zmniejsz/powiększ<br />
lParam zawiera nową wysokość i szerokość okna<br />
case WM_PAINT:<br />
Pobranie kontekstowego identyfikatora urządzenia. Funkcja<br />
BeginPaint() spowoduje w razie potrzeby wysłanie komunikatu<br />
WM_ERASEBKGND (Erase Background - skasuj tło).<br />
memset(&ps, 0x00, sizeof(PAINTSTRUCT));<br />
hDC = BeginPaint(hWnd, &ps);<br />
Set Background Mode - ustaw rodzaj tła (tu: przezroczyste):<br />
SetBkMode(hDC, TRANSPARENT);<br />
Aplikacja powinna wykreślić obszar roboczy okna posługując sie<br />
grafiką GDI i (Graficzny Interfejs Urządzenia - analogia do<br />
graficznego standardu BGI w środowisku DOS). Struktura ps typu<br />
PAINSTRUCT zwrócona przez BeginPaint() wskazuje prostokąt do<br />
zamalowania.<br />
Wypisanie tekstu w głównym oknie aplikacji:<br />
TextOut(hDC, 0, 0, (LPSTR) "Hello, world.", strlen("Hello,<br />
world."));<br />
- 389-
Funkcja TextOut() pracuje w trybie graficznym, więc (podobnie<br />
jak inne funkcje graficzne Windows API) otrzymuje jako argument<br />
tzw. "kontekst urządzenia" - hDC.<br />
Zamykanie okna:<br />
case WM_CLOSE:<br />
DestroyWindow(hWnd);<br />
if (hWnd == hWndMain)<br />
PostQuitMessage(0);<br />
Jeśli zamknięte zostało główne okno aplikacji, funkcja<br />
PostQuitMessage() wysyła do Windows komunikat, że aplikacja<br />
zakończyła działanie i okno aplikacji zostało usunięte. W tym<br />
stadium stosuje się funkcje PostQuitMessage() i<br />
PostAppMessage(). Pozostale przypadki są obsługiwane przez<br />
wariant domyślny - default. Przekazanie komunikatu do obsługi<br />
przez Windows.<br />
default:<br />
return (DefWindowProc(hWnd, Message, wParam, lParam));<br />
Funkcja rejestrująca wszystkie klasy wszystkich okien związanych<br />
z bieżącą aplikacja (nazwiemy ją roboczo FRegisterClasses()).<br />
Jesli operacja sie powiodła - funkcja zwraca kod błędu.<br />
int FRegisterClasses(void)<br />
{<br />
WNDCLASS wndclass; /* Struktura do definiowania klas okien. */<br />
memset(&wndclass, 0x00, sizeof(WNDCLASS));<br />
Ustawienie parametrów okna w strukturze:<br />
wndclass.style = CS_HRDRAW | CS_VRDRAW;<br />
wndclass.lpfnWindowProc = WindowProc;<br />
Dodatkowa pamięć dla klasy Window i obiektów klasy Window.<br />
Dołączanie innych zasobów odbywa się przy pomocy funkcji:<br />
LoadBitmap() - załaduj mapę bitową<br />
LoadIcon() - załaduj ikonkę<br />
LoadCurcor(), LoadMenu(), itp. ...<br />
wndclass.cbClsExtra = 0;<br />
wndclass.cbWndExtra = 0;<br />
wndclass.hInstance = hInst;<br />
wndclass.hIcon = LoadIcon(NULL, ID_ICON);<br />
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);<br />
Utworzenie pędzla (brush) dla tła:<br />
wndclass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);<br />
wndclass.lpszMenuName = szAppName;<br />
wndclass.lpszClassName = szAppName;<br />
- 390-
if (!RegisterClass(&wndclass)) return -1;<br />
}<br />
Typowe obiekty ze składu Windows to<br />
HBRUSH Pędzel; i<br />
HPEN Ołówek;<br />
Należy tu zwrócić uwagę jeszcze na dwa szczegóły techniczne. DC<br />
i GDI - Device Context, Graphics Device Interface - to tzw.<br />
kontekst urządzenia i graficzny interfejs urządzenia. Pozwala to<br />
Windows działać skutecznie w trybie "Device Independent"<br />
(niezależnym od sprzętu).<br />
- 391-
LEKCJA 41. KOMPILATORY "SPECJALNIE DLA Windows".<br />
________________________________________________________________<br />
Z tej lekcji dowiesz się, czym różnią się kompilatory<br />
przeznaczone dla pracy w środowisku Windows.<br />
________________________________________________________________<br />
W IDE i w sposobie zachowania zaszły istotne zmiany. Posługując<br />
się Turbo <strong>C++</strong> z pakietu BORLAND <strong>C++</strong> 3.0 lub BCW z pakietu 3.1<br />
możemy korzystać z uroków i usług Windows szerzej niż do tej<br />
pory. Możemy otwierać wiele okien i uruchamiać bezpośrednio z<br />
poziomu IDE okienkowe aplikacje. W głównym menu kompilatora<br />
zaszły pewne zmiany (sygnalizujące obiektowo- i okienkowo -<br />
zorientowaną ewolucję pakietów <strong>Borland</strong>a), na które warto zwrócić<br />
uwagę.<br />
Zniknęło menu Debug (co wcale nie oznacza, że nie możemy<br />
korzystać z Debuggera), pojawiło się natomiast nowe menu Browse<br />
(przeglądanie). Rozkazy, których tradycyjnie szukaliśmy w menu<br />
Debug zostały rozrzucone do innych menu. I tak:<br />
Menu Compile zawiera:<br />
Compile (kompilacja do *.OBJ),<br />
Make (kompilacja i konsolidacja do *.EXE),<br />
Link (konsolidacja bez powtórnej kompilacji),<br />
Build all (konsolidacja wszystkich modułów),<br />
Information... (informacja o przebiegu kompilacji),<br />
Remove messages (usuwanie komunikatów z pliku wynikowego)<br />
Menu Run zawiera:<br />
Run (uruchomienie i ewentualna rekompilcja),<br />
Arguments... (argumenty uruchomieniowe z wiersza rozkazu),<br />
Debugger (zamiast w Debug - TU!)<br />
Debugger arguments... (argumenty dla Debuggera)<br />
Menu Project zawiera:<br />
Open project<br />
- otwórz (nowy lub istniejący) plik projektu,<br />
Close project - zamknij projekt,<br />
Add item... - dodaj element (plik) do projektu,<br />
Delete item - usuń element (plik) z projektu,<br />
Include ˙˙files... ˙˙- ˙˙podaj ˙katalog ˙zawierający ˙dodatkowe<br />
dołączane do programu pliki nagłówkowe *.H<br />
W menu Options (zestaw znany już z <strong>Borland</strong> <strong>C++</strong>) warto zwrócić<br />
uwagę na pewną dodatkową możliwość. Jak wiemy z doświadczenia,<br />
uruchamiając program często dokonujemy zmian i korekt w pliku<br />
żródłowym *.C, czy *.CPP. Znacznie rzadziej jednak zmieniamy<br />
zestaw dołączanych do programu plików nagłówkowych *.H. Wiemy<br />
również, że kompilacja tych właśnie plików nagłówkowych zajmuje<br />
często lwią część czasu całej kompilacji i konsolidacji<br />
programu. <strong>Borland</strong> zauważył to i w okienku dialogowym:<br />
- 392-
Options | Compiler | Code generation --> Code Generation Options<br />
umieścił opcję Pre-compiled headers (pliki nagłówkowe wstępnie<br />
skompilowane wcześniej - i tylko jeden raz). Szczególnie w<br />
przypadku aplikacji okienkowych może to znacznie przyspieszyć<br />
proces uruchamiania i "szlifowania" naszych programów. Nie ma<br />
jednak nic za darmo. <strong>Borland</strong>/Turbo <strong>C++</strong> po skompilowaniu plików<br />
nagłówkowych tworzy na dysku roboczy plik *.SYM nadając mu nazwę<br />
zgodną z nazwą bieżącego projektu (jest to zwykle nazwa głównego<br />
modułu *.CPP) i do poprawnego działania wymaga kilkadziesiąt lub<br />
nawet kilkaset kilobajtów dodatkowej przestrzeni na dysku.<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
Jeśli przenosisz projekt na dyskietkę i tam kontynuujesz pracę<br />
nad projektem, pamiętaj, że może zabraknąć miejsca na<br />
prekompilowany plik .SYM.<br />
________________________________________________________________<br />
Czytelnik zechce sam sprawdzić w jakim stopniu przyspieszy to<br />
kompilację naszego własnego programu proceduralno -<br />
zdarzeniowego WINPZ1.CPP:<br />
WINZ1.CPP. Jednomodułowa aplikacja proceduralno - zdarzeniowa<br />
dla Windows.<br />
________________________________________________________________<br />
#include <br />
#pragma argused<br />
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpszCmdLine, int nCmdShow )<br />
{<br />
WNDCLASS Okno1;<br />
MSG komunikaty;<br />
HWND NrOkna;<br />
LPSTR LongPtr1 = "Okno 1";<br />
LPSTR lp2 = "AM: B<strong>C++</strong> 3..4/Reczne sterowanie (1)";<br />
if (hPrevInstance == 0)<br />
{<br />
Okno1.style= CS_HREDRAW | CS_VREDRAW ;<br />
Okno1.lpfnWndProc= WndProc;<br />
Okno1.cbClsExtra = 0;<br />
Okno1.cbWndExtra= 0;<br />
Okno1.hInstance = hInstance;<br />
Okno1.hCursor = LoadCursor(0, IDC_CROSS );<br />
- 393-
Okno1.hbrBackground= GetStockObject(WHITE_BRUSH );<br />
Okno1.lpszMenuName= 0;<br />
Okno1.lpszClassName= LongPtr1;<br />
if (!RegisterClass(&Okno1))<br />
return 0;<br />
}<br />
NrOkna = CreateWindow(LongPtr1, lp2, WS_VISIBLE |<br />
WS_SYSMENU |<br />
WS_MINIMIZEBOX | WS_VSCROLL | WS_MAXIMIZEBOX,<br />
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,<br />
0, 0, hInstance, 0);<br />
ShowWindow(NrOkna, nCmdShow);<br />
UpdateWindow(NrOkna);<br />
while (GetMessage(&komunikaty, 0, 0, 0))<br />
{<br />
TranslateMessage(&komunikaty );<br />
DispatchMessage(&komunikaty );<br />
}<br />
return 0;<br />
}<br />
long FAR PASCAL WndProc (HWND NrOkna, unsigned KomunikatWindows,<br />
WORD wParam, LONG lParam)<br />
{<br />
HDC NrKontekstu;<br />
PAINTSTRUCT struktura_graficzna;<br />
RECT prostokat;<br />
switch(KomunikatWindows)<br />
{<br />
case WM_PAINT:<br />
{<br />
NrKontekstu = BeginPaint(NrOkna, &struktura_graficzna);<br />
GetClientRect(NrOkna, &prostokat);<br />
TextOut(NrKontekstu,80,50, ": Reczne sterowanie:", 20 );<br />
TextOut(NrKontekstu, 5,70, "Tu -->", 6);<br />
TextOut(NrKontekstu, 5, 85, "Blad:", 5);<br />
TextOut(NrKontekstu,75,70, "-----------------------------",<br />
40);<br />
TextOut(NrKontekstu,30,110, "<strong>Programowanie</strong> proceduralno -<br />
zdarzeniowe.", 41 );<br />
TextOut(NrKontekstu,30,135, "Szablon moze zostac rozbudowany<br />
o inne funkcje.", 47 );<br />
TextOut(NrKontekstu,30,180, "RECZNIE panujemy np. nad:", 25<br />
);<br />
TextOut(NrKontekstu,20,220, "paskiem tytulowym okna, tytulem<br />
ikonki...", 41);<br />
- 394-
TextOut(NrKontekstu, 100, 250, "!KONIEC - [Alt]+[F4]", 20);<br />
EndPaint(NrOkna,&struktura_graficzna);<br />
break;<br />
}<br />
case WM_DESTROY:<br />
{<br />
PostQuitMessage(0);<br />
break;<br />
}<br />
default:<br />
return DefWindowProc(NrOkna,KomunikatWindows,wParam,lParam);<br />
}<br />
}<br />
return 0;<br />
Program demonstruje opisane wyżej mechanizmy, może być<br />
uruchamiany wielokrotnie i sprowadzony do ikony. Z uwagi na brak<br />
zdefiniowanych dodatkowych zasobów (brak w projekcie plików:<br />
.RC - resources - zasoby<br />
.ICO - ikona<br />
.DEF - definicji<br />
.PRJ lub .IDE - projektu<br />
.DSK - konfiguracyjnego<br />
itp.)<br />
podczas kompilacji programu wystąpią dwa komunikaty<br />
ostrzegawcze. Komunikaty te można zignorować.<br />
A oto druga przykładowa aplikacja w tym samym stylu. Tym razem<br />
funkcja okienkowa reaguje na naciśnięcie lewego klawisza myszki,<br />
co powoduje wygenerowanie komunikatu WM_LEFTBUTTONDOWN.<br />
Program WINZ-2.CPP<br />
________________________________________________________________<br />
#include <br />
#include <br />
#pragma argused<br />
char napis[10];<br />
int X, Y;<br />
LONG FAR PASCAL WndProc (HWND, WORD, WORD, LONG);<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpszCmdLine, int nCmdShow )<br />
{<br />
WNDCLASSwndClass;<br />
MSGmsg;<br />
HWNDhWnd;<br />
LPSTR Lp1 = "Mysza1";<br />
LPSTR lp2 = "WINPZ2: Wykrywanie Lewego Klawisza<br />
- 395-
Myszki";<br />
if (!hPrevInstance)<br />
{<br />
wndClass.style= CS_HREDRAW | CS_VREDRAW ;<br />
wndClass.lpfnWndProc= WndProc;<br />
wndClass.cbClsExtra = 0;<br />
wndClass.cbWndExtra= 0;<br />
wndClass.hInstance = hInstance;<br />
wndClass.hIcon = 0;<br />
wndClass.hCursor= LoadCursor(0, IDC_ARROW );<br />
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );<br />
wndClass.lpszMenuName= 0;<br />
wndClass.lpszClassName= Lp1;<br />
if (!RegisterClass(&wndClass))<br />
exit(1);<br />
}<br />
hWnd = CreateWindow(Lp1, lp2, WS_OVERLAPPEDWINDOW,<br />
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,<br />
0, 0, hInstance, 0);<br />
ShowWindow(hWnd, nCmdShow);<br />
UpdateWindow(hWnd);<br />
while (GetMessage(&msg, 0, 0, 0))<br />
{<br />
TranslateMessage(&msg );<br />
DispatchMessage(&msg );<br />
}<br />
return 0;<br />
}<br />
LONG FAR PASCAL WndProc (HWND hWnd, WORD Message,<br />
WORD wParam, LONG lParam)<br />
{<br />
HDC hDC;<br />
PAINTSTRUCT ps;<br />
RECT rect;<br />
switch(Message)<br />
{<br />
case WM_SIZE:<br />
hDC = GetDC( hWnd );<br />
TextOut(hDC, 50, 100, "Wykrywanie nacisniecia", 22);<br />
TextOut(hDC, 50, 120, "lewego klawisza myszki.", 23);<br />
TextOut(hDC, 20, 140, "Komunikat o zdarzeniu: ", 22);<br />
TextOut(hDC, 20, 156, "Left Button Down - LBUTTONDOWN", 31);<br />
TextOut(hDC, 50, 170, "Po wcisnieciu klawisza,", 23);<br />
TextOut(hDC, 50, 190,"w biezacej pozycji kursora, pojawi sie<br />
napis
case WM_PAINT:<br />
hDC = BeginPaint(hWnd, &ps);<br />
TextOut(hDC, X,Y, napis, strlen(napis));<br />
EndPaint(hWnd, &ps);<br />
break;<br />
case WM_LBUTTONDOWN:<br />
strcpy(napis,"
Kompilacja przebiegnie poprawnie (pamiętaj o Opcjach i<br />
Katalogach), mimo to pojawią się jednak dwa komunikaty<br />
ostrzegawcze. W okienku "Compile Status" (stan/przebieg<br />
kompilacji) pojawi się zawartość:<br />
Lines 3832 (znakomita większość to WINDOWS.H,<br />
prekompilacja byłaby celowa)<br />
Warnings: 1<br />
Errors: 0<br />
Jeśli wybierzesz klawisz [OK] w okienku "focus" (aktywność)<br />
zostanie przekazana do okienka komunikatów "Message" a tam<br />
pojawi się napis:<br />
Warning: Parameter 'lspzCmdLine' is never used.<br />
Wskaźnik do parametrów uruchomieniowych programu (Arguments)<br />
pobieranych z wiersza rozkazu nie został ani raz użyty w<br />
programie. Na to nic nie możemy poradzić. Po prostu argumenty<br />
uruchomieniowe nie są nam potrzebne. Wykonujemy więc "klik"<br />
(przekazanie "focusa") w okienku edycyjnym i możemy przejść do<br />
następnej czynności:<br />
4. Konsolidacja: Compile | Link.<br />
W okienku "Message" znów pojawi się ostrzeżenie:<br />
Linker Warning: No module definition file specified:<br />
using defaults.<br />
(brak wyspecyfikowanego pliku definicji .DEF; stosuję wartości<br />
domyślne)<br />
I tu już możemy coś zaradzić. Możemy zatem pokusić się o<br />
stworzenie naszego pierwszego pliku definicji (nazwa jest trochę<br />
myląca - chodzi o zdefiniowanie sposobu wykorzystania zasobów<br />
środowiska Windows).<br />
Aby utworzyć plik .DEF (jest to plik ASCII) należy:<br />
1. Otworzyć nowe okienko edycyjne (nie wychodząc z IDE):<br />
File | New<br />
Otworzy się okienko NONAMExx.CPP. Ta nazwa nie jest oczywiście<br />
najodpowiedniejsza, więc umieszczamy plik we właściwym katalogu<br />
(tym samym, co główny program *.CPP) przy pomocy rozkazu File |<br />
Save as...<br />
i nadajemy plikowi stosowną nazwę i rozszerzenie *.DEF. Okieno<br />
pozostaje puste, ma jednak "focus" i nową nazwę, np.<br />
C:\..\PR.DEF.<br />
3. Redagujemy nasz pierwszy plik definicji, np. tak:<br />
NAME JAKAKOLWIEK //
EXETYPE WINDOWS //
7. Wpisujemy nazwę kolejnego pliku wchodzącego w skład projektu<br />
(w tym przypadku WINZ2.DEF).<br />
8. Wybieramy klawisz [+Add] w okienku.<br />
UWAGA: Czynności 7) i 8) w przypadku bardziej złożonych<br />
projektów będą powtarzane wielokrotnie.<br />
9. Wybieramy klawisz [Done] w okienku (zrobione/gotowe).<br />
Konfigurowanie projektu zostało zakończone.<br />
10. Przy pomocy rozkazów Compile, Link, Make, Build all, Run<br />
możemy teraz skompilować, skonsolidować i uruchomić nasz program<br />
w postaci projektu. Ostrzeżenie Linkera zniknie.<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
W dolnej części ekranu w stadium tworzenia projektów ( i póżniej<br />
po załadowaniu pliku projektu [Open Project] pojawi się lista<br />
plików. Do trybu edycji pliku możesz przjść poprzez dwukrotne<br />
klinięcie pliku na tej liście.<br />
Zwróć uwagę, że pliki projektów .PRJ ( w <strong>Borland</strong> 4+ .IDE)<br />
przechowują również informacje o konfiguracji. Najważniejsza z<br />
nich to informacja o katalogach, z których korzysta kompilator:<br />
Options | Directories... | Include<br />
Options | Directories... | Library<br />
Options | Directories... | Output<br />
________________________________________________________________<br />
Najwygodniej przechowywać wszystkie pliki wchodzące w skład<br />
jednego projektu w odrębnym katalogu dyskowym. Dla wprawy załóż<br />
odrębny katalog i zapisz tam pliki:<br />
*.CPP<br />
*.DEF<br />
*.PRJ<br />
(lub *.IDE)<br />
dla swoich pierwszych dwóch projektów, które właśnie powstały.<br />
[!!!] UWAGA<br />
________________________________________________________________<br />
Ten sam plik definicji możesz wykorzystywać do tworzenia<br />
następnych przykładowych aplikacji typu Windows EXE.<br />
________________________________________________________________<br />
- 400-
LEKCJA 42. Elementy sterujące i zarządzanie programem.<br />
________________________________________________________________<br />
Jak sterować pracą aplikacji. Jak umieszczać elementy<br />
graficzne-sterujące w oknie aplikacji. Najczęściej stosowane<br />
funkcje API Windows.<br />
________________________________________________________________<br />
Elementy sterujące pracą aplikacji w Windows (ang. controls) są<br />
również swoistymi okienkami (tyle, że potomnymi - Child Window<br />
wobec głównego okna aplikacji - Parent Window).<br />
Do utworzenia takiego specjalnego okna również można użyć<br />
funkcji CreateWindow(). Jeśli okno ma stać się nie głównym oknem<br />
aplikacji, lecz oknem sterującym przebiegiem programu, funkcja<br />
wymaga podania następujących argumentów:<br />
- rodzaj klasy sterującej (ang. control class)<br />
- rodzaj elementu sterującego (ang. control style)<br />
Typowe rodzaje elementów (obiektów) starujących w środowisku<br />
Windows:<br />
BUTTON<br />
- klawisz rozkazu, prostokątne okno typu<br />
Child, reprezentujące przycisk, który<br />
użytkownik może włączyć; przycisk może<br />
być opatrzony etykietą (text label).<br />
COMBOBOX<br />
- okienko dialogowe kombinowane. Jest<br />
złożeniem klasy EDIT i LISTBOX;<br />
LISTBOX<br />
STATIC<br />
- oknienko z listą (zwykle element<br />
składowy okienka dialogowego typu<br />
Combo Box.<br />
- pole statyczne (bez prawa edycji).<br />
Niewielkie okno zawierające tekst lub<br />
grafikę; służy z reguły do oznaczania<br />
innych okien sterujących.<br />
SCROLLBAR<br />
- pasek przewijania (pionowy - Vertical<br />
Scroll Bar; poziomy - Horizontal<br />
Scroll Bar).<br />
Style klawiszy sterujących (Button Styles):<br />
BS_PUSHBUTTON - Klawisz. Okno sterujące wysyła, po<br />
każdym wyborze klawisza<br />
(kliknięcie), wiadomość do okna<br />
macieżystego (Parent Window).<br />
BS_RADIOBUTTON - Okrągły przełącznik działający<br />
zwykle na zasadzie @tylko jeden<br />
z grupy".<br />
- 401-
BS_CHECKBOX -<br />
- prostokątny przełącznik [X]<br />
włączający (aktywna) lub<br />
wyłączający (nieaktywna)<br />
opcję. Działa niezależnie od<br />
pozostałych.<br />
Inne style określają np. sposób edycji tekstu (ES_LEFT,<br />
ES_MULTILINE, itp.) Szczegóły - patrz system Help - Windows API.<br />
Oto przykład utworzenia okna elementu sterującego typu "Klawisz"<br />
(BUTTON to nazwa typu):<br />
hControlWnd = CreateWindow ("BUTTON", " Napis_na_Klawiszu ",<br />
BS_PUSHBUTTON |WS_CHILD | WS_VISIBLE,<br />
10, 20, 30, 40,<br />
hWnd, ID_Elem, hInstance, 0);<br />
Identyfikator ID_Elem jest potrzebny, gdy w jednym oknie<br />
znajduje się kilka elementów sterujących - pozwala na ich<br />
rozpoznawanie w programie. Sposób przekazywania informacji o<br />
kliknięciu klawisza przypomnę na przykładzie okienka<br />
komunikatów:<br />
if(IDOK==MessageBox(0, "", "", MB_OK)) ...<br />
IDOK to predefiniowany w Windows identyfikator klawisza [OK].<br />
Oto krótkie wyjaśnienie pozostałych elementów:<br />
10, 10, 30, 20, - współrzędne. x, y, szerokość, wysokość<br />
hWnd, - oznacznik okna macieżystego<br />
Przesuwanie i zmiana wielkości elementu sterującego.<br />
Funkcja MoveWindow() przesuwa okno we wskazane miejsce:<br />
MoveWindow(hKlawisz, 10, 10, 20, 30, TRUE);<br />
Ponieważ okno elementu sterującego ma zadane względne<br />
współrzędne w oknie macieżystym, gdy okno macierzyste zostanie<br />
przesunięte - element sterujący będzie przesunięty<br />
automatycznie. Również po zmianie rozmiarów okna macieżystego<br />
okno elementu sterującego zmienia położenie, zawsze jednakowe<br />
względem lewego górnego rogu.<br />
Usuwanie okna sterującego<br />
Okienko elementu sterującego możemy usunąć (jak i każde inne<br />
okna) przy pomocy funkcji:<br />
DestroyWindow(hKlawisz);<br />
Przekazywanie informacji do- i z- okna elementu sterującego<br />
- 402-
Zdarzenie w oknie elementu sterującego - np. kliknięcie klawisza<br />
- powoduje wygenerowanie komunikatu WM_COMMAND. Towarzyszące<br />
komunikatowi parametry przenoszą istotne informacje:<br />
wParam<br />
lParam<br />
- identyfikator elementu sterującego,<br />
- dla wciśniętego klawisza będzie to BN_CLICKED.<br />
Niektóre komunikaty Windows mogą być kierowane do okna elementu<br />
sterującego i wymuszać pewne operacje. Dla przykładu komunikat<br />
WM_GETTEXTLENGTH przesłany do okienka edycyjnego typu Text Edit<br />
Box (element sterujący klasy EDIT) jest żądaniem podania<br />
długości tekstu wpisanego właśnie do okienka. Aby Windows<br />
wygenerowały komunikat i przesłały go do naszego elementu<br />
sterującego - musimy "poprosić" przy pomocy funkcji<br />
SendMessage() (WyślijKomunikat):<br />
DlugTekstu = SendMessage(hEditWnd, WM_GETTEXTLENGHT, 0, 0);<br />
gdzie:<br />
hEditWnd jest identyfikatorem elementu - okienka edycyjnego<br />
[???]Robi na "szaro'?<br />
________________________________________________________________<br />
Podobnie jak opcje w menu - klawisze także mogą zostać<br />
udostępnione (ang. enable), bądź zablokowane (ang. disable).<br />
Jeśli hKlawisz będzie identyfikatorem elementu sterującego,<br />
można go udostępnić (1), bądź zablokować (0) przy pomocy<br />
funkcji:<br />
EnableWindow(hKlawisz, 0);<br />
EnableWindow(hKlawisz, 1);<br />
________________________________________________________________<br />
Typowy projekt dla środowiska Windows składa się z kilku (czasem<br />
kilkunastu) plików: .H, .MNU, .DLG, .RC, .DEF, .PRJ, .ICO, .BMP,<br />
itp. Kompilator zasobów generuje na podstawie tego "składu"<br />
końcowy plik aplikacji.<br />
------------------Plik MEDYT-01.H-------------------------------<br />
#define szAppName "MEDYT-01"<br />
#define ID_EDIT 200<br />
------------------Plik główny: MEDYT-01.CPP---------------------<br />
#include <br />
#include "EDIT.H"<br />
#pragma argused<br />
HWND hEditWnd;<br />
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;<br />
- 403-
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpszCmdLine, int nCmdShow)<br />
{<br />
WNDCLASS wndClass;<br />
MSG msg;<br />
HWND hWnd;<br />
RECT rect;<br />
if ( !hPrevInstance )<br />
{<br />
wndClass.style= CS_HREDRAW | CS_VREDRAW ;<br />
wndClass.lpfnWndProc= WndProc;<br />
wndClass.cbClsExtra = 0;<br />
wndClass.cbWndExtra= 0;<br />
wndClass.hInstance = hInstance;<br />
wndClass.hIcon = LoadIcon(NULL, szAppName);<br />
wndClass.hCursor= LoadCursor(NULL, IDC_CROSS);<br />
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );<br />
wndClass.lpszMenuName= NULL;<br />
wndClass.lpszClassName= szAppName;<br />
if (!RegisterClass(&wndClass))<br />
return 0;<br />
}<br />
hWnd = CreateWindow(szAppName,<br />
"MEDYT-01", WS_OVERLAPPEDWINDOW,<br />
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,<br />
0, 0, hInstance, 0);<br />
GetClientRect(hWnd, (LPRECT) &rect);<br />
hEditWnd = CreateWindow ("Edit",NULL, WS_CHILD | WS_VISIBLE |<br />
ES_MULTILINE | WS_VSCROLL |<br />
WS_HSCROLL | ES_AUTOHSCROLL |<br />
ES_AUTOVSCROLL, 0, 0,(rect. right -<br />
rect. left),<br />
(rect. bottom - rect.<br />
top),hWnd,IDC_EDIT, hIstance,NULL);<br />
if( ! hEditWnd )<br />
{<br />
DestroyWindow(hWnd);<br />
return 0;<br />
}<br />
ShowWindow(hWnd, nCmdShow);<br />
UpdateWindow(hWnd);<br />
while (GetMessage(&msg, NULL, 0, 0))<br />
{<br />
TranslateMessage(&msg );<br />
DispatchMessage(&msg );<br />
}<br />
return 0;<br />
}<br />
long FAR PASCAL WndProc (HWND hWnd, unsigned Message,<br />
WORD wParam, LONG lParam)<br />
- 404-
{<br />
switch(Message)<br />
{<br />
case ID_EDIT:<br />
if(HIWORD(lParam)==EN_ERRSPACE)<br />
/* starsze słowo lParam zawiera właściwe dla okna edycyjnego<br />
wiadomości, jeżeli jest to EN_ERRSPACE - okno sterujące nie<br />
może alokować dodatkowego obszaru pamięci */<br />
{<br />
MessageBox (GetFocus(), "Brak pamieci", "MEDYT-01",<br />
MB_ICONSTOP | MB_OK);<br />
}<br />
break;<br />
case WM_SETFOCUS:<br />
SetFocus(hEditWnd);<br />
break;<br />
/* Pierwsze dwa parametry funkcji MoveWindow są ustawione na<br />
zero, dzięki temu po zastosowaniu tej funkcji nie zmieni się<br />
wzajemne położenie obu okien, a jedynie uaktualnianiu<br />
ulegnie okno sterujące. */<br />
case WM_SIZE:<br />
MoveWindows(hEditWnd, 0, 0, LOWORD(lParam));<br />
HIWORD(lParam), TRUE);<br />
break;<br />
case WM_DESTROY:<br />
PostQuitMessage(0);<br />
break;<br />
default:<br />
return (DefWindowProc(hWnd,Message,wParam,lParam));<br />
}<br />
return 0;<br />
}<br />
Jak sterować pracą aplikacji. Jak umieszczać elementy<br />
graficzne-sterujące w oknie aplikacji. Najczęściej stosowane<br />
funkcje API Windows.<br />
________________________________________________________________<br />
Elementy sterujące pracą aplikacji w Windows (ang. controls) są<br />
również swoistymi okienkami (tyle, że potomnymi - Child Window<br />
wobec głównego okna aplikacji - Parent Window).<br />
Do utworzenia takiego specjalnego okna również można użyć<br />
funkcji CreateWindow(). Jeśli okno ma stać się nie głównym oknem<br />
aplikacji, lecz oknem sterującym przebiegiem programu, funkcja<br />
wymaga podania następujących argumentów:<br />
- rodzaj klasy sterującej (ang. control class)<br />
- rodzaj elementu sterującego (ang. control style)<br />
- 405-
Typowe rodzaje elementów (obiektów) starujących w środowisku<br />
Windows:<br />
BUTTON<br />
- klawisz rozkazu, prostokątne okno typu<br />
Child, reprezentujące przycisk, który<br />
użytkownik może włączyć; przycisk może<br />
być opatrzony etykietą (text label).<br />
COMBOBOX<br />
- okienko dialogowe kombinowane. Jest<br />
złożeniem klasy EDIT i LISTBOX;<br />
LISTBOX<br />
STATIC<br />
- oknienko z listą (zwykle element<br />
składowy okienka dialogowego typu<br />
Combo Box.<br />
- pole statyczne (bez prawa edycji).<br />
Niewielkie okno zawierające tekst lub<br />
grafikę; służy z reguły do oznaczania<br />
innych okien sterujących.<br />
SCROLLBAR<br />
- pasek przewijania (pionowy - Vertical<br />
Scroll Bar; poziomy - Horizontal<br />
Scroll Bar).<br />
Style klawiszy sterujących (Button Styles):<br />
BS_PUSHBUTTON - Klawisz. Okno sterujące wysyła, po<br />
każdym wyborze klawisza<br />
(kliknięcie), wiadomość do okna<br />
macieżystego (Parent Window).<br />
BS_RADIOBUTTON - Okrągły przełącznik działający<br />
zwykle na zasadzie @tylko jeden<br />
z grupy".<br />
BS_CHECKBOX -<br />
- prostokątny przełącznik [X]<br />
włączający (aktywna) lub<br />
wyłączający (nieaktywna)<br />
opcję. Działa niezależnie od<br />
pozostałych.<br />
Inne style określają np. sposób edycji tekstu (ES_LEFT,<br />
ES_MULTILINE, itp.) Szczegóły - patrz system Help - Windows API.<br />
Oto przykład utworzenia okna elementu sterującego typu "Klawisz"<br />
(BUTTON to nazwa typu):<br />
hControlWnd = CreateWindow ("BUTTON", " Napis_na_Klawiszu ",<br />
BS_PUSHBUTTON |WS_CHILD | WS_VISIBLE,<br />
10, 20, 30, 40,<br />
- 406-
hWnd, ID_Elem, hInstance, 0);<br />
Identyfikator ID_Elem jest potrzebny, gdy w jednym oknie<br />
znajduje się kilka elementów sterujących - pozwala na ich<br />
rozpoznawanie w programie. Sposób przekazywania informacji o<br />
kliknięciu klawisza przypomnę na przykładzie okienka<br />
komunikatów:<br />
if(IDOK==MessageBox(0, "", "", MB_OK)) ...<br />
IDOK to predefiniowany w Windows identyfikator klawisza [OK].<br />
Oto krótkie wyjaśnienie pozostałych elementów:<br />
10, 10, 30, 20, - współrzędne. x, y, szerokość, wysokość<br />
hWnd, - oznacznik okna macieżystego<br />
Przesuwanie i zmiana wielkości elementu sterującego.<br />
Funkcja MoveWindow() przesuwa okno we wskazane miejsce:<br />
MoveWindow(hKlawisz, 10, 10, 20, 30, TRUE);<br />
Ponieważ okno elementu sterującego ma zadane względne<br />
współrzędne w oknie macieżystym, gdy okno macierzyste zostanie<br />
przesunięte - element sterujący będzie przesunięty<br />
automatycznie. Również po zmianie rozmiarów okna macieżystego<br />
okno elementu sterującego zmienia położenie, zawsze jednakowe<br />
względem lewego górnego rogu.<br />
Usuwanie okna sterującego<br />
Okienko elementu sterującego możemy usunąć (jak i każde inne<br />
okna) przy pomocy funkcji:<br />
DestroyWindow(hKlawisz);<br />
Przekazywanie informacji do- i z- okna elementu sterującego<br />
Zdarzenie w oknie elementu sterującego - np. kliknięcie klawisza<br />
- powoduje wygenerowanie komunikatu WM_COMMAND. Towarzyszące<br />
komunikatowi parametry przenoszą istotne informacje:<br />
wParam<br />
lParam<br />
- identyfikator elementu sterującego,<br />
- dla wciśniętego klawisza będzie to BN_CLICKED.<br />
Niektóre komunikaty Windows mogą być kierowane do okna elementu<br />
sterującego i wymuszać pewne operacje. Dla przykładu komunikat<br />
WM_GETTEXTLENGTH przesłany do okienka edycyjnego typu Text Edit<br />
Box (element sterujący klasy EDIT) jest żądaniem podania<br />
długości tekstu wpisanego właśnie do okienka. Aby Windows<br />
wygenerowały komunikat i przesłały go do naszego elementu<br />
sterującego - musimy "poprosić" przy pomocy funkcji<br />
SendMessage() (WyślijKomunikat):<br />
- 407-
DlugTekstu = SendMessage(hEditWnd, WM_GETTEXTLENGHT, 0, 0);<br />
gdzie:<br />
hEditWnd jest identyfikatorem elementu - okienka edycyjnego<br />
[???]Robi na "szaro'?<br />
________________________________________________________________<br />
Podobnie jak opcje w menu - klawisze także mogą zostać<br />
udostępnione (ang. enable), bądź zablokowane (ang. disable).<br />
Jeśli hKlawisz będzie identyfikatorem elementu sterującego,<br />
można go udostępnić (1), bądź zablokować (0) przy pomocy<br />
funkcji:<br />
EnableWindow(hKlawisz, 0);<br />
EnableWindow(hKlawisz, 1);<br />
________________________________________________________________<br />
Typowy projekt dla środowiska Windows składa się z kilku (czasem<br />
kilkunastu) plików: .H, .MNU, .DLG, .RC, .DEF, .PRJ, .ICO, .BMP,<br />
itp. Kompilator zasobów generuje na podstawie tego "składu"<br />
końcowy plik aplikacji.<br />
------------------Plik MEDYT-01.H-------------------------------<br />
#define szAppName "MEDYT-01"<br />
#define ID_EDIT 200<br />
------------------Plik główny: MEDYT-01.CPP---------------------<br />
#include <br />
#include "EDIT.H"<br />
#pragma argused<br />
HWND hEditWnd;<br />
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpszCmdLine, int nCmdShow)<br />
{<br />
WNDCLASS wndClass;<br />
MSG msg;<br />
HWND hWnd;<br />
RECT rect;<br />
if ( !hPrevInstance )<br />
{<br />
wndClass.style= CS_HREDRAW | CS_VREDRAW ;<br />
wndClass.lpfnWndProc= WndProc;<br />
wndClass.cbClsExtra = 0;<br />
wndClass.cbWndExtra= 0;<br />
wndClass.hInstance = hInstance;<br />
wndClass.hIcon = LoadIcon(NULL, szAppName);<br />
- 408-
wndClass.hCursor= LoadCursor(NULL, IDC_CROSS);<br />
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );<br />
wndClass.lpszMenuName= NULL;<br />
wndClass.lpszClassName= szAppName;<br />
if (!RegisterClass(&wndClass))<br />
return 0;<br />
}<br />
hWnd = CreateWindow(szAppName,<br />
"MEDYT-01", WS_OVERLAPPEDWINDOW,<br />
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,<br />
0, 0, hInstance, 0);<br />
GetClientRect(hWnd, (LPRECT) &rect);<br />
hEditWnd = CreateWindow ("Edit",NULL, WS_CHILD | WS_VISIBLE |<br />
ES_MULTILINE | WS_VSCROLL |<br />
WS_HSCROLL | ES_AUTOHSCROLL |<br />
ES_AUTOVSCROLL, 0, 0,(rect. right -<br />
rect. left),<br />
(rect. bottom - rect.<br />
top),hWnd,IDC_EDIT, hIstance,NULL);<br />
if( ! hEditWnd )<br />
{<br />
DestroyWindow(hWnd);<br />
return 0;<br />
}<br />
ShowWindow(hWnd, nCmdShow);<br />
UpdateWindow(hWnd);<br />
while (GetMessage(&msg, NULL, 0, 0))<br />
{<br />
TranslateMessage(&msg );<br />
DispatchMessage(&msg );<br />
}<br />
return 0;<br />
}<br />
long FAR PASCAL WndProc (HWND hWnd, unsigned Message,<br />
WORD wParam, LONG lParam)<br />
{<br />
switch(Message)<br />
{<br />
case ID_EDIT:<br />
if(HIWORD(lParam)==EN_ERRSPACE)<br />
/* starsze słowo lParam zawiera właściwe dla okna edycyjnego<br />
wiadomości, jeżeli jest to EN_ERRSPACE - okno sterujące nie<br />
może alokować dodatkowego obszaru pamięci */<br />
{<br />
MessageBox (GetFocus(), "Brak pamieci", "MEDYT-01",<br />
MB_ICONSTOP | MB_OK);<br />
}<br />
break;<br />
case WM_SETFOCUS:<br />
- 409-
SetFocus(hEditWnd);<br />
break;<br />
/* Pierwsze dwa parametry funkcji MoveWindow są ustawione na<br />
zero, dzięki temu po zastosowaniu tej funkcji nie zmieni się<br />
wzajemne położenie obu okien, a jedynie uaktualnianiu<br />
ulegnie okno sterujące. */<br />
case WM_SIZE:<br />
MoveWindows(hEditWnd, 0, 0, LOWORD(lParam));<br />
HIWORD(lParam), TRUE);<br />
break;<br />
case WM_DESTROY:<br />
PostQuitMessage(0);<br />
break;<br />
default:<br />
return (DefWindowProc(hWnd,Message,wParam,lParam));<br />
}<br />
return 0;<br />
}<br />
- 410-
LEKCJA 43. O Okienkach dialogowych.<br />
________________________________________________________________<br />
O tym, jak konstruuje się okienka dialogowe.<br />
________________________________________________________________<br />
Do wyświetlania okienek dialogowych w Windows API służy funkcja<br />
DialogBox(), a do zakończenia ich "życia na ekranie" -<br />
EndDialog(). Podobnie jak każde okno, również okno dialogowe<br />
musi mieć swoją funkcję, obsługi komunikatów Windows. Zamiast<br />
WindowProc() nazywa się ją tradycyjnie DlgProc():<br />
BOOL FAR PASCAL DlgProc(HWND hDLG, unsigned Message, WORD<br />
wParam, LONG lParam);<br />
{<br />
switch (message)<br />
{<br />
...<br />
default: return (0);<br />
}<br />
}<br />
Za wyjątkiem braku domyślnego handlera Windows -<br />
DefWindowProc(), który jest zbędny, w związku z wewnętrznie<br />
przyjmowanymi wartościami domyślnymi, funkcja podobna jest<br />
bardzo w swojej konstrukcji do WindowProc(). Funkcja zwraca<br />
wartość FALSE (czyli 0), jeśli przesłany komunikat nie został<br />
obsłużony. Typowymi komunikatami, które rozpatruje większość<br />
okienek dialogowych, są WM_INITDIALOG oraz WM_COMMAND.<br />
Przykład okienka dialogowego:<br />
------------------Plik: DLGBOX1.H-------------------------------<br />
#define szAppName "DLGBOX1"<br />
#define IDM_DLG1 100<br />
------------------Plik zasobów: DLGBOX1.RC----------------------<br />
#include "DLGBOX1.H"<br />
#include <br />
IDI_ICON ICON CONTROL.ICO<br />
DLGBOX1 MENU<br />
BEGIN<br />
MENUITEM "&O DlgBox" IDM_DLG1<br />
/* to menu pojawi się w oknie macieżystym */<br />
END<br />
DLGBOX1 DIALOG 30,30,200,100<br />
/* Pierwsze liczby to współrzędne lewego-górnego rogu okna, dwie<br />
następne - to szerokość i długość. Współrzędne są względne.<br />
Punkt (0,0) to narożnik okna macieżystego */<br />
- 411-
STYLE WS_POPUP | WS_DLGFRAME<br />
BEGIN<br />
LTEXT "Przyklad" -1, 0, 12, 160, 8<br />
CTEXT "DLGBOX1 - Przyklad" -1, 0, 36, 160, 8<br />
DEFPUSHBUTTON "OK" IDOK, 64, 60, 32,14, WS_GROUP<br />
END<br />
----------------------------------------------------------------<br />
Pomiędzy parą słów kluczowych BEGIN-END można umieszczać różne<br />
instrukcje sterujące. Definiują one, jaki rodzaj okna<br />
sterującego ukaże się w okienku dialogowym. Instrukcje te można<br />
stosować w następującym formacie:<br />
typ_okna "tekst" ID, x, y, szerokość, wysokość [styl]<br />
Parametr styl jest opcjonalny. Styl okna określają<br />
identyfikatory predefiniowane w API Windows (WS_...). Parametr<br />
ID jest odpowiednikiem identyfikatora dla okien potomnych typu<br />
Child Window; dla okien sterujących, które nie zwracają<br />
komunikatów do okna macierzystego, ma wartość -1. IDOK<br />
wykorzystaliśmy jako identyfikator dla okna sterującego typu<br />
BUTTON. Zostanie on wysłany do funkcji okienkowej jako wartość<br />
parametru wParam, gdy użytkownik kliknie klawisz.<br />
------------------Plik główny: DLGBOX1.CPP----------------------<br />
#include <br />
#include <br />
#include <br />
#include "DLGBOX1.H"<br />
#pragma argused<br />
HANDLE<br />
hInst;<br />
long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ;<br />
BOOL FAR PASCAL ControlProc (HWND, unsigned, WORD, LONG) ;<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpszCmdLine, int nCmdShow )<br />
{<br />
WNDCLASS wndClass;<br />
MSG msg;<br />
HWND hWnd;<br />
if ( !hPrevInstance )<br />
{<br />
wndClass.style= CS_HREDRAW | CS_VREDRAW ;<br />
wndClass.lpfnWndProc= WndProc;<br />
wndClass.cbClsExtra = 0;<br />
wndClass.cbWndExtra= 0;<br />
wndClass.hInstance = hInstance;<br />
wndClass.hIcon = LoadIcon(NULL, szAppName);<br />
wndClass.hCursor= LoadCursor(NULL, IDC_ARROW );<br />
- 412-
wndClass.hbrBackground= GetStockObject(WHITE_BRUSH );<br />
wndClass.lpszMenuName= szAppName;<br />
wndClass.lpszClassName= szAppName;<br />
if (!RegisterClass(&wndClass))<br />
return 0;<br />
}<br />
hInst = hInstance;<br />
hWnd = CreateWindow(szAppName, "DLGBOX1", WS_OVERLAPPEDWINDOW,<br />
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hInstance, 0);<br />
ShowWindow(hWnd, nCmdShow);<br />
UpdateWindow(hWnd);<br />
while (GetMessage(&msg, 0, 0, 0))<br />
{<br />
TranslateMessage(&msg );<br />
DispatchMessage(&msg );<br />
}<br />
return 0;<br />
}<br />
BOOL FAR PASCAL ControlProc (HWND hDlg, unsigned Message,<br />
WORD wParam, LONG lParam)<br />
{<br />
switch(msg)<br />
{<br />
case WM_INITDIALOG:<br />
return TRUE;<br />
break;<br />
case WM_COMMAND:<br />
switch(wParam)<br />
{<br />
case IDOK:<br />
case IDCANCEL:<br />
EndDialog(hDlg,0);<br />
return TRUE;<br />
}<br />
break;<br />
}<br />
return (0);<br />
}<br />
long FAR PASCAL WndProc (HWND hWnd, unsigned msg,<br />
WORD wParam, LONG lParam)<br />
{<br />
FARPROC lpControlProc;<br />
switch(Message)<br />
{<br />
case WM_COMMAND:<br />
switch(wParam)<br />
{<br />
case IDM_ABOUT:<br />
lpControlProc = MakeProcInstance((FARPROC) ControlProc, hInst);<br />
- 413-
DialogBox(hInst, "DLGBOX1", hWnd, lpControlProc);<br />
return 0;<br />
}<br />
break;<br />
case WM_DESTROY:<br />
hDC = BeginPaint(hWnd , &ps);<br />
TextOut(hDC, 30, 50,"Demo okienka dialogowego", 25);<br />
TextOut(hDC, 30, 70,"Zastosuj menu...", 17);<br />
EndPaint(hWnd, &ps);<br />
break;<br />
case WM_DESTROY:<br />
PostQuitMessage(0);<br />
break;<br />
default:<br />
return (DefWindowProc(hWnd,Message,wParam,lParam));<br />
}<br />
return 0;<br />
}<br />
Stosując okienko edycyjne możemy użyć następujących<br />
predefiniowanych parametrów:<br />
CONTROL<br />
- określa okno elementu sterującego zdefiniowane<br />
przez użytkownika.<br />
CONTROL "tekst", klasa, styl, x, y, szerokość, wysokość<br />
LTEXT - element sterujący: okienko tekstowe<br />
Wyrównywanie tesktu: do lewej.<br />
RTEXT - j. w. Wyrównywanie tesktu: do prawej<br />
CTEXT - j. w. Wyrównywanie tesktu: centrowanie w okienku<br />
CHECKBOX - pole tekstowe po prawej stronie przełącznika typu<br />
Check Box.<br />
PUSHBUTTON - Klawisz z napisem.<br />
LISTBOX - okienko z listą<br />
GROUPBOX - grupa elementów sterujących typu BUTTON; zgrupowanie<br />
kilku elementów sterujących i otoczenie ramką. Tekst<br />
zostanie umieszczony w lewym górnym rogu.<br />
DEFPUSHBUTTON - Klawisz domyślny w stylu BS_DEFPUSHBUTTON.<br />
RADIOBUTTON - analogicznie jak dla stylu BS_RADIOBUTTON.<br />
EDITTEXT - tworzy okno oparte na klasie EDIT.<br />
COMBOBOX - tworz okno oparte na klasie COMBOBOX.<br />
ICON - definiuje ikonę opartą na klasie STATIC; w<br />
okienku dialogowym.<br />
SCROLLBAR - tworzy okno oparte na klasie SCROLLBAR.<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
W niektórych przypadkach okienko dialogowe może być głównym<br />
oknem aplikacji.<br />
- 414-
________________________________________________________________<br />
- 415-
LEKCJA 44. Dołączanie zasobów - menu i okienka dialogowe.<br />
________________________________________________________________<br />
Jak dodać menu i okienka dialogowe do aplikacji.<br />
________________________________________________________________<br />
Aby dodać do aplikacji menu należy utworzyć plik (ASCII) zasobów<br />
*.RC, który zostanie użyty w projekcie. Pierwszą instrukcją jest<br />
MENU, "NazwaMenu",<br />
MENU i para słów kluczowych (znanych z Pascala) BEGIN oraz END,<br />
między którymi znajdzie się kombinacja instrukcji MENUITEM oraz<br />
POPUP.<br />
MENUITEM definiuje pozycję na głównym pasku menu - określa - jak<br />
będzie wyglądać i jaki identyfikator będzie ją reprezentował.<br />
Instrukcja POPUP pozwala, rozwinąć listę pozycji wchodzących w<br />
skład danego menu. Nazwa menu może być użyta podczas rejestracji<br />
klasy danego okna jako wpisana w odpowiednie pole struktury na<br />
której oparte jest okno. W ten sposób uzyskamy menu dla<br />
wszystkich okien danej klasy.<br />
BEGIN<br />
POPUP "Rozkaz"<br />
BEGIN<br />
MENUITEM "Rozkaz 1", IDM_R1<br />
MENUITEM "Rozkaz 2", IDM_R2<br />
MENUITEM "Rozkaz 3", IDM_R3<br />
END<br />
POPUP "Kolor"<br />
BEGIN<br />
MENUITEM "Czarny", IDM_BLACK<br />
MENUITEM "Niebieski", IDM_BLUE<br />
MENUITEM "Zielony", IDM_GREEN<br />
END<br />
MENUITEM "Koniec", IDM_EXIT<br />
END<br />
Każda pozycja ma własny identyfikator, np. IDM_EXIT, IDM_BLUE,<br />
który Windows przekazują do aplikacji, gdy zostaje ona wybrana<br />
przez użytkownika z systemu menu. Ponieważ każdy identyfikator<br />
powinien być unikalny, najlepiej jest go zdefiniować w pliku<br />
zasobów .RC lub własnym pliku nagłówkowym .H:<br />
#define IDM_EXIT 100<br />
#define IDM_BLUE 101<br />
#define IDM_R1 102<br />
...<br />
Mamy już zdefiniowane menu w pliku zasobów, należy je teraz<br />
dołączyć do aplikacji na jeden z dwóch sposobów:<br />
- Można określić menu jako menu danej klasy okien, gdy klasa ta<br />
jest rejestrowana. W ten sposób dołączymy menu do każdego<br />
okna opartego na tej klasie. Aby to wykonać, wystarczy<br />
- 416-
przypisać odpowiedniemu polu struktury nazwę naszego menu.<br />
Jeżeli obiekt klasy WNDCLASS nazwiemy Window1, to:<br />
Window1.lpszMenuName = "NazwaMenu";<br />
Gdy klasa zostanie zarejestrowana, każde okno tej klasy będzie<br />
miało to samo menu, chyba że dostarczymy odpowiedni<br />
identyfikator menu w momencie tworzenia okna funkcją<br />
CreateWindow().<br />
- Drugim sposobem jest dołączenie menu w momencie tworzenia<br />
okna, wtedy tylko tworzone okno będzie miało dane menu.<br />
Należy najpierw załadować menu przy użyciu funkcji LoadMenu(),<br />
która zwraca jego identyfikator:<br />
HMENU hMenu = LoadMenu(hInstance, "NazwaMenu");<br />
hWnd = CreateWindow(szAppName,<br />
"Nazwa Aplikacji",<br />
WS_OVERLAPPEDWINDOW,<br />
CW_USEDEFAULT,<br />
CW_USEDEFAULT,<br />
CW_USEDEFAULT,<br />
CW_USEDEFAULT,<br />
NULL,<br />
hMenu,
kopii zmiennej do funkcji).<br />
Nie wszystkie pozycje w menu są w danym stadium pracy aplikacji<br />
sensowne (możliwe do wykonania). Zaraz przekonasz się, jak to<br />
się dzieje, że niektóre pozycje "robi się na szaro". W API<br />
Windows służy do tego funkcja:<br />
EnableMenuItem (hMenu, IDM_R1, MF_DISABLED);<br />
EnableMenuItem (hMenu, IDM_R1, MF_GRAYED);<br />
EnableMenuItem (hMenu, IDM_R1, MF_ENABLED);<br />
Rozkaz R1 skojarzony z identyfikatorem IDM_R1 i znajdujący się w<br />
systemie menu o oznaczniku hMenu stanie się kolejno zablokowany,<br />
widoczny-lecz-niedostępny, dostępny.<br />
Dodawanie i usuwanie pozycji w menu<br />
Dodawanie pozycji do menu może być wykonane dwoma sposobami:<br />
przez wstawienie pomiędzy istniejące pozycje lub na końcu listy.<br />
W pierwszym przypadku należy użyć funkcji InsertMenu(). Funkcja<br />
ta pozwala jednocześnie określić status pozycji, między innymi<br />
czy będzie umieszczone nowe pole można określić dwoma sposobami:<br />
przez identyfikator pozycji mającej być przed nową lub przez<br />
numerację poszczególnych, licząc id lewej skrajnej pozycji (<strong>C++</strong><br />
tradycyjnie liczy od zera). Sposób "odliczania" pozycji w<br />
systemie menu określa tryb (BYCOMMAND lub BYPOSITION - rozkaz,<br />
bądź pozycja):<br />
InsertMenu(hMenu, IDM_R1, MF_BYCOMMAND |MF_DISABLED, IDM_R5,<br />
"Rozkaz 5");<br />
InsertMenu(hMenu, 1, MF_ENABLED, IDM_R5, "Rozkaz 5");<br />
Funkcja wstawi za pozycją "Rozkaz 1" nową pozycję "Rozkaz 5",<br />
jednocześnie ustawia jej status. Drugą funkcją dodającą pozycję<br />
do utworzonego systemu menu jest:<br />
AppendMenu(hMenu, MF_ENABLED, IDM_R4, "Rozkaz 4");<br />
Poniżej przykład zdefiniowania menu aplikacji w taki właśnie<br />
sposób:<br />
case WM_CREATE:<br />
hMenu = CreateMenu(); //Utworzenie menu<br />
AppendMenu(hMenu, MF_ENABLED, IDM_R1, "Rozkaz 1");<br />
AppendMenu(hMenu, MF_ENABLED, IDM_R2, "Rozkaz 2");<br />
AppendMenu(hMenu, MF_ENABLED, IDM_R3, "Rozkaz 3");<br />
SetMenu(hWnd, hMenu); //Wyświetlenie menu<br />
...<br />
break;<br />
- 418-
Usuwanie pozycji z menu można przeprowadzić dwoma sposobami:<br />
- poprzez wskazanie numeru pozycji w systemie menu:<br />
DeleteMenu(hMenu, 1, MF_BYPOSITION); //usunięta zostanie druga<br />
//pozycja z systemu menu<br />
- przez wyszczególnienie identyfikatorem pozycji<br />
DeleteMenu(hMenu, IDM_R3, MF_BYCOMMAND);<br />
Po usunięciu pozycji z menu Window usunie również wszystkie<br />
związane z nią submenu.<br />
Zaznaczanie pozycji w menu (mark).<br />
Obok pozycji w menu można umieścić znak markujący ("ptaszek").<br />
Znak markujący można zainicjować w pliku zasobów .RC. Dzięki<br />
temu, użytkownik w momencie otwarcia okna dowie się z wyglądu<br />
menu o początkowym ustawieniu opcji.<br />
MENUITEM "Rozkaz 2", IDM_R2, CHECKED<br />
W trakcie pracy aplikacji należy posłużyć się funkcją<br />
CheckMenuItem(). Zwykle najpierw kasujemy "ptaszka" przy<br />
poprzedniej pozycji:<br />
CheckMenuItem( hMenu, IDM_R2, MF_UNCHECKED);<br />
CheckMenuItem(hMenu, IDM_R3, MF_CHECKED);<br />
Zmiany pozycji menu<br />
Funkcja ModyfyMenu() pozwala na zmianę nazwy pozycji i jej<br />
atrybutów. Oto przykłady użycia tej funkcji:<br />
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND, IDM_R2, "Polecenie 2");<br />
Identyfikator pozycji nie ulegnie zmianie, jedynie nazwa pola z<br />
"Rozkaz 2" na "Polecenie 2". Możemy zmienić jednocześnie i<br />
identyfikator, by nie pomylić się w programie:<br />
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND, IDM_P2, "Polecenie 2");<br />
Dodatkowo można ustawić za jednym zamachem i atrybuty:<br />
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND | MF_CHECKED | MF_GRAYED,<br />
IDM_R2, "Polecenie 2");<br />
Użycie grafiki w systemie menu.<br />
W systemie menu aplikacji możemy zamiast łańcucha znaków "Rozkaz<br />
2" umieścić element graficzny - np. w postaci mapy bitowej.<br />
Zamiast pola o nazwie "Pole", wprowadza mapę bitową:<br />
- 419-
HMENU hMenu = GetMenu(hWnd);<br />
HBITMAP hBitmap = LoadBitmap (hIstance, "Pole");<br />
ModifyMenu(hMenu, IDM_R2, MF_BYCOMMAND | MF_BITMAP, IDM_R2,<br />
(LPSTR) MAKELONG (hBitmap, 0));<br />
GetMenu() zwraca oznacznik aktualnego menu, potrzebny jako<br />
pierwszy parametr funkcji ModifyMenu(). Drugim parametrem tej<br />
funkcji jest identyfikator pozycji, którą chcemy zmienić.<br />
Trzecia określa, że zmiana ma być wykonana przez wyszukanie<br />
pozycji za pośrednictwem jej identyfikatora oraz że nową pozycję<br />
ma reprezentować mapa bitowa. Czwarty parametr określa<br />
identyfikator nowej pozycji. Ponieważ ostatnim parametrem nie<br />
jest już wskaźnik do łańcucha znakowego, należy przesłać<br />
oznacznik mapy bitowej jako mniej znaczące słowo tego parametru.<br />
W tym celu 16-bitowy oznacznik jest łączony z 16-bitową stałą, a<br />
następnie poddawany konwersji do typu Long Pointer to STRing.<br />
Zmiana menu aplikacji na kolejne.<br />
Aplikacja w różnych stadiach pracy może mieć na ekranie różne<br />
(kilka czasem kilkanaście) menu. Wymiany menu w oknie aplikacji<br />
można dokonać, załadowując nowe menu funkcją LoadMenu() i<br />
ustawiając je jako aktualne funkcją SetMenu():<br />
...<br />
hMenu2 = LoadMenu (hIstance, "Menu2");<br />
SetMenu (hWnd, hMenu2);<br />
DrawMenuBar(...);<br />
...<br />
Menu i Menu2 powinny być zdefiniowane w pliku zasobów *.RC.<br />
Po każdej zmianie menu należy użyć funkcji DrawMenuBar(), aby<br />
wprowadzone zmiany pojawiły się na ekranie. Oto przykład<br />
stosownego pliku zasobów:<br />
Menu1 MENU<br />
BEGIN<br />
POPUP "&File"<br />
BEGIN<br />
MENUITEM "&New" , IDM_NEW<br />
MENUITEM "&Save", IDM_SAVE<br />
MENUITEM "E&xit", IDM_EXIT<br />
END<br />
POPUP "&Options"<br />
BEGIN<br />
MENUITEM "Menu&1", IDM_M1,CHECKED<br />
MENUITEM "Menu&2" , IDM_M2<br />
END<br />
END<br />
Menu2 MENU<br />
- 420-
BEGIN<br />
POPUP "&File"<br />
BEGIN<br />
MENUITEM "&Open", IDM_OPEN<br />
MENUITEM "&New" , IDM_NEW<br />
MENUITEM "&Save", IDM_SAVE<br />
MENUITEM "Save &As", IDM_SAVEAS<br />
MENUITEM "&DOS shell", IDM_DOSSHELL<br />
MENUITEM "E&xit", IDM_EXIT<br />
END<br />
POPUP "&Options"<br />
BEGIN<br />
MENUITEM "Menu&1", IDM_M1,<br />
MENUITEM "Menu&2" , IDM_M2, CHECKED<br />
END<br />
END<br />
ZASTOSOWANIE Resource Worshop<br />
Takie pliki zasobów w <strong>Borland</strong> <strong>C++</strong> mało kto tworzy dziś "na<br />
piechotę". BORLAND <strong>C++</strong> oferuje do tego celu dwa<br />
narzędzia:<br />
Edytor zasobów - Resource Workshop<br />
Automatyczny generator - DialogExpert (wersje 4+)<br />
Najwygodniejszym sposobem jest zastosowanie edytora zasobów<br />
Resource Workshop. Jest to tym wygodniejsze, że Resource<br />
Workshop pozwala jednocześnie obserwować i źródłowy plik *.RC<br />
(ASCII) i efekt - menu w ruchu.<br />
W środowisku <strong>Borland</strong> <strong>C++</strong> okienka dialogowe tworzy się także<br />
zwykle przy pomocy Resource Worshop.<br />
Tworzenie okienek dialogowych przy pomocy Resource Workshop<br />
przypomina składanie budowli z gotowych klocków.<br />
Kolejne elementy sterujące możemy umieszczać w okienku<br />
dialogowym poprzez wybranie ich z palety narzędzi i<br />
przeniesienie do projektowanego okienka techniką "pociągnij i<br />
upuść" (drag & drop).<br />
Po skróconym omówieniu najważniejszych funkcji z API Windows<br />
przejdźmy to niemniej krótkiej prezentacji zasad tworzenia<br />
aplikacji przy pomocy biblioteki obiektów OWL.<br />
[Z]<br />
________________________________________________________________<br />
1. Przeanalizuj program w pełnej wersji (na dyskietce).<br />
2. Zmodyfikuj dowolną aplikację przykładową tak, by dołączyć do<br />
niej inną ikonę.<br />
3. Opracuj własne menu i własną ikonę przy pomocy Resource<br />
Workshop.<br />
________________________________________________________________<br />
Krótka instrukcja do Resource Workshop.<br />
________________________________________________________________<br />
- 421-
1. Uruchomienie: Ikonka Worshop w oknie grupowym <strong>Borland</strong> <strong>C++</strong>.<br />
2. Początek pracy: File | New Project...<br />
3. Rodzaje zasobów do wyboru w okienku dialogowym "New project":<br />
[ ] RC - plik zasobów<br />
[ ] CUR - kursor<br />
[ ] BMP - mapa bitowa<br />
[ ] RES - plik zasobów w formie skompilowanej<br />
[ ] ICO - ikonka<br />
[ ] FNT - czcionki (Fonts)<br />
Wybieramy odpowiednio: RC<br />
4. Zmieni się menu w głównym oknie Resource Workshop. Z menu<br />
wybieramy Resource | New<br />
W okienku dialogowym z listy Resource Type (rodzaj zasobów):<br />
ACCELERATORS, BITMAP, CURSOR, DIALOG, FONT, ICON, MENU, RCDATA,<br />
STRINGTABLE, VERSINFO<br />
wybieramy odpowiednio MENU lub DILOG.<br />
Kolejny raz zmieni się menu. W przypadku menu wybieramy:<br />
Menu:<br />
New pop-up - nowa pozycja POPUP<br />
New menu item - nowa pozycja MENUITEM<br />
Zwróć uwagę, że typowe menu File, Edit, Help jesy już gotowe do<br />
wstawienia (ukryte pod pozycjami New file pop-up, New edit<br />
pop-up...).<br />
W przypadku okienka dialogowego najważniejsze jest menu Control.<br />
Są tam wszyskie rodzaje podstawowych elementów sterujących (Push<br />
button, Radio button, scroll bar, List box, Combo box, Edit box,<br />
itd.). Projektując okienko możesz również wyświetlić siatkę<br />
(Grid).<br />
Przy pomocy Resource Workshop możesz poddawać edycji i<br />
modyfikować pliki zasobów zarówno należące do programów<br />
przykładowych zawartoch na dyskietce, jak i zasoby "firmowych"<br />
przykładów <strong>Borland</strong>a. W katalogach \SOURCE (kody źródłowe .CPP) i<br />
\EXAMPLES (przykłady - projekty) znajdziesz wiele rozmaitych<br />
przykładów. Możesz także poddawać edycji pliki .BMP, .ICO i inne<br />
niekoniecznie należące do pakietu <strong>Borland</strong> <strong>C++</strong>.<br />
________________________________________________________________<br />
- 422-
LEKCJA 45. O PROGRAMACH OBIEKTOWO - ZDARZENIOWYCH.<br />
________________________________________________________________<br />
Po aplikacjach sekwencyjnych, proceduralno-zdarzeniowych, jedno-<br />
i dwupoziomowych, pora rozważyć dokładniej stosowanie technik<br />
obiektowych.<br />
________________________________________________________________<br />
Programy pracujące w środowisku Windows tworzone są w oparciu o<br />
tzw. model trójwarstwowy. Pierwsza warstwa to warstwa<br />
wizualizacji, druga - interfejs, a trzecia - to właściwa<br />
maszyneria programu. W tej lekcji zajmiemy się "anatomią"<br />
aplikacji wielowarstwowych a następnie sposobami wykorzystania<br />
bogatego instrumentarium oferowanego przez <strong>Borland</strong>a wraz z<br />
kompilatorami B<strong>C++</strong> 3+...4+.<br />
Biblioteka OWL w wersjach BORLAND <strong>C++</strong> 3, 3.1, 4 i 4.5 zawiera<br />
definicje klas potrzebnych do tworzenia aplikacji dla Windows.<br />
Fundamentalne znaczenie dla większości typowych aplikacji mają<br />
następujące klasy:<br />
TModule (moduł - program lub biblioteka DLL)<br />
TApplication (program - aplikacja)<br />
TWindow (Okno)<br />
Rozpocznę od krótkiego opisu dwu podstawowych klas.<br />
KLASA TApplication.<br />
Tworząc obiekt klasy TNaszProgram będziemy wykorzystywać<br />
dziedziczenie od tej właśnie klasy bazowej:<br />
class TNaszProgram : public TApplication<br />
Podstawowym celem zastosowania tej właśnie klasy bazowej jest<br />
odziedziczenie gotowej funkcji - metody virtual InitMainWindow()<br />
(zainicjuj główne okno programu). Utworzenie obiektu klasy<br />
TNaszProgram następuje zwykle w czterech etapach:<br />
* Windows uruchamiają program wywołując główną funkcję WinMain()<br />
lub OwlMain() wchodzącą w skład każdej aplikacji.<br />
* Funkcja WinMain() tworzy przy pomocy operatora new nowy obiekt<br />
- aplikację.<br />
* Obiekt - aplikacja zaczyna funkcjonować. Konstruktor obiektu<br />
(własny, bądź odziedziczony po klasie TApplication) wywołuje<br />
funkcję - wirtualną metodę InitMainWindow().<br />
* Funkcja przy pomocy operatora new tworzy obiekt - okno<br />
aplikacji.<br />
Wskaźnik do utworzonego obiektu zwraca funkcja GetApplication().<br />
Dla zobrazowania mechanizmów poniżej przedstawiamy uproszczony<br />
- 423-
"wyciąg" z dwu opisywanych klas. Nie jest to dokładna kopia kodu<br />
źródłowego <strong>Borland</strong>a, lecz skrót tego kodu pozwalający na<br />
zrozumienie metod implementacji okienkowych mechanizmów wewnątrz<br />
klas biblioteki OWL i tym samym wewnątrz obiektów obiektowo -<br />
zdarzeniowych aplikacji.<br />
A oto najważniejsze elementy implementacji klasy TApplication:<br />
- Konstruktor obiektu "Aplikacja":<br />
TApplication::TApplication(const char far* name,<br />
HINSTANCE Instance,<br />
HINSTANCE prevInstance,<br />
const char far* CmdLine,<br />
int CmdShow,<br />
TModule*& gModule)<br />
{<br />
hPrevInstance = prevInstance;<br />
nCmdShow = CmdShow;<br />
MainWindow = 0;<br />
HAccTable = 0; //Accelerator Keys Table Handle<br />
BreakMessageLoop = FALSE;<br />
AddApplicationObject(this); //this to wskaźnik do własnego<br />
gModule = this; //obiektu, czyli do bież. aplikacji<br />
}<br />
Funkcja - metoda "Zainicjuj Instancję":<br />
void TApplication::InitInstance()<br />
{<br />
InitMainWindow();<br />
if (MainWindow)<br />
{<br />
MainWindow->SetFlag(wfMainWindow);<br />
MainWindow->Create();<br />
MainWindow->Show(nCmdShow);<br />
}<br />
Metoda "Zainicjuj główne okno aplikacji":<br />
void TApplication::InitMainWindow()<br />
{<br />
SetMainWindow(new TFrameWindow(0, GetName()));<br />
}<br />
Metoda Run() - "Uruchom program":<br />
int TApplication::Run()<br />
{<br />
int status;<br />
{<br />
if (!hPrevInstance) InitApplication();<br />
InitInstance();<br />
status = MessageLoop();<br />
- 424-
}<br />
A oto pętla pobierania komunikatów w uproszczeniu. "Pump" to po<br />
prostu "pompowanie" komunikatów (message) oczekujących (waiting)<br />
w kolejce. PeekMessage() to sprawdzenie, czy w kolejce oczekuje<br />
komunikat. PM_REMOWE to "brak komunikatu".<br />
BOOL TApplication::PumpWaitingMessages()<br />
{<br />
MSG msg;<br />
BOOL foundOne = FALSE;<br />
while (::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))<br />
{<br />
foundOne = TRUE;<br />
if (msg.message == WM_QUIT)<br />
{<br />
BreakMessageLoop = TRUE;<br />
MessageLoopResult = msg.wParam;<br />
::PostQuitMessage(msg.wParam);<br />
break;<br />
}<br />
if (!ProcessAppMsg(msg))<br />
{<br />
::TranslateMessage(&msg);<br />
::DispatchMessage(&msg);<br />
}<br />
}<br />
return foundOne;<br />
}<br />
int TApplication::MessageLoop()<br />
{<br />
long idleCount = 0;<br />
MessageLoopResult = 0;<br />
while (!BreakMessageLoop) {<br />
TRY {<br />
if (!IdleAction(idleCount++))<br />
::WaitMessage();<br />
if (PumpWaitingMessages())<br />
idleCount = 0;<br />
}<br />
if (MessageLoopResult != 0) {<br />
::PostQuitMessage(MessageLoopResult);<br />
break;<br />
}<br />
})<br />
}<br />
BreakMessageLoop = FALSE;<br />
return MessageLoopResult;<br />
}<br />
else if (::IsWindowEnabled(wnd)) {<br />
- 425-
*(info->Wnds++) = wnd;<br />
::EnableWindow(wnd, FALSE);<br />
}<br />
}<br />
return TRUE;<br />
}<br />
KLASA TWindow.<br />
Klasa TWindow (Okno) zawiera implementację wielu przydatnych<br />
przy tworzeniu aplikacji "cegiełek". Poniżej przedstawiono<br />
fragment pliku źródłowego (patrz \SOURCE\OWL\WINDOW.CPP). Łatwo<br />
można rozpoznać pewne znane już elementy.<br />
...<br />
extern LRESULT FAR PASCAL _export InitWndProc(HWND, UINT,<br />
WPARAM, LPARAM);<br />
...<br />
struct TCurrentEvent //Struktura BieżąceZdarzenie<br />
{<br />
TWindow* win; //Wskażnik do okna<br />
UINT message; //Komunikat<br />
WPARAM wParam;<br />
LPARAM lParam;<br />
};<br />
...<br />
DEFINE_RESPONSE_TABLE(TWindow)<br />
//Makro: Zdefiniuj tablicę odpowiedzi na zdarzenia<br />
//EV_WM_SIZE - Zdarzenie (EVent)-nadszedł komunikat WM_SIZE<br />
...<br />
EV_WM_SETCURSOR,<br />
EV_WM_SIZE,<br />
EV_WM_MOVE,<br />
EV_WM_PAINT,<br />
EV_WM_LBUTTONDOWN,<br />
EV_WM_KILLFOCUS,<br />
EV_WM_CREATE,<br />
EV_WM_CLOSE,<br />
EV_WM_DESTROY,<br />
EV_COMMAND(CM_EXIT, CmExit),<br />
...<br />
END_RESPONSE_TABLE;<br />
Funkcje - metody obsługujące komunikaty zaimplementowane zostały<br />
wewnątrz klasy TWindow tak:<br />
TWindow::EvCreate(CREATESTRUCT far&)<br />
{<br />
SetupWindow();<br />
return (int)DefaultProcessing();<br />
}<br />
void TWindow::EvSize(UINT sizeType, TSize&)<br />
{<br />
- 426-
if (Scroller && sizeType != SIZE_MINIMIZED)<br />
{<br />
Scroller->SetPageSize();<br />
Scroller->SetSBarRange();<br />
}<br />
}<br />
Metoda GetWindowClass() bardzo przypomina klasyczne<br />
zainicjowanie zanej już struktury WNDCLASS:<br />
void TWindow::GetWindowClass(WNDCLASS& wndClass)<br />
{<br />
wndClass.cbClsExtra = 0;<br />
wndClass.cbWndExtra = 0;<br />
wndClass.hInstance = *GetModule();<br />
wndClass.hIcon = 0;<br />
wndClass.hCursor = ::LoadCursor(0, IDC_ARROW);<br />
wndClass.hbrBackground = HBRUSH(COLOR_WINDOW + 1);<br />
wndClass.lpszMenuName = 0;<br />
wndClass.lpszClassName = GetClassName();<br />
wndClass.style = CS_DBLCLKS;<br />
wndClass.lpfnWndProc = InitWndProc;<br />
}<br />
Skoro te wszystkie "klocki" zostały już zaimplementowane<br />
wewnątrz definicji klas, nasze programy powinny tylko umiejętnie<br />
z nich korzystać a teksty źródłowe programów powinny ulec<br />
skróceniu i uproszczeniu.<br />
STADIA TWORZENIA OBIEKTOWEJ APLIKACJI.<br />
Ponieważ znakomita większość dzisiejszych użytkowników pracuje z<br />
Windows 3.1, 3.11, i NT - zaczniemy tworzenie aplikacji od<br />
umieszczenia na początku informacji dla OWL, że nasz docelowy<br />
program ma być przeznaczony właśnie dla tego środowiska:<br />
#define WIN31<br />
Jak już wiemy dzięki krótkiemu przeglądowi struktury bazowych<br />
klas przeprowadzonemu powyżej - funkcje API Windows są w istocie<br />
klasycznymi funkcjami posługującymi się mechanizmami języka C.<br />
<strong>C++</strong> jest "pedantem typologicznym" i przeprowadza dodatkowe<br />
testowanie typów parametrów przekazywanych do funkcji (patrz<br />
"Technika programowania w <strong>C++</strong>"). Aby ułatwić współpracę,<br />
zwiększyć poziom bezpieczeństwa i "uregulować" potencjalne<br />
konflikty - dodamy do programu:<br />
#define STRICT<br />
Chcąc korzystać z biblioteki OWL wypada dołączyć właściwy plik<br />
nagłówkowy:<br />
- 427-
#include <br />
Plik OWL.H zawiera już wewnątrz dołączony WINDOWS.H, który<br />
występował we wcześniejszych aplikacjach proceduralno -<br />
zdarzeniowych i jeszcze parę innych plików.<br />
Ponieważ chcemy skorzystać z gotowych zasobów - odziedziczymy<br />
pewne cechy po klasie bazowej TApplication. Zgodnie z zasadami<br />
programowania obiektowego chcąc utworzyć obiekt musimy najpierw<br />
zdefiniować klasę:<br />
class TOkno ...<br />
i wskazać po której klasie bazowej chcemy dziedziczyć:<br />
class TOkno : public TApplication<br />
{<br />
...<br />
Konstruktor obiektu klasy TOkno powinien tylko przekazać<br />
parametry konstruktorowi klasy bazowej - i już.<br />
class TOkno : public TApplication<br />
{<br />
public:<br />
TOkno(LPSTR name, HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nShow) : TApplication(name,<br />
hInstance, hPrevInstance, lpCmdLine, nShow)<br />
{<br />
return;<br />
}<br />
virtual void InitMainWindow();<br />
};<br />
Umieściliśmy w definicji klasy jeszcze jedną funkcję inicjującą<br />
główne okno aplikacji. Możemy ją zdefiniować np. tak:<br />
void TOkno::InitMainWindow(void)<br />
{<br />
MainWindow = new (TWindow(0, "Napis - Tytul Okna"));<br />
}<br />
Działanie funkcji polega na utworzeniu nowego obiektu (operator<br />
new) klasy bazowej TWindow. Główne okno stanie się zatem<br />
obiektem klasy TWindow (Niektóre specyficzne aplikacje posługują<br />
się okienkiem dialogowym jako głównym oknem programu. W takiej<br />
sytuacji dziedziczenie powinno następować po klasie TDialog).<br />
Konstruktorowi tego obiektu przekazujemy jako parametr napis,<br />
który zostanie umieszczony w nagłówku głównego okna aplikacji.<br />
Pierwszy argument (tu ZERO) to wskażnik do macieżystego okna,<br />
ponieważ w bardziej złożonych aplikacjach występują okna<br />
macieżyste (parent) i okna potomne (child). Okno macieżyste to<br />
zwykle obiekt klasy "główne okno" a okno potomne to najczęściej<br />
okienko dialogowe, bądź okienko komunikatów. W tym przypadku<br />
- 428-
wpisujemy zero, ponieważ program nie posiada w tym stadium<br />
wcześniejszego okna macieżystego.<br />
Pozostało nam jeszcze dodać funkcję WinMain() i pierwszy program<br />
obiektowy w wersji "Maszyna do robienia nic" jest gotów.<br />
Listing . Obiektowa "Maszyna do robienia nic"<br />
________________________________________________________________<br />
#define STRICT<br />
#define WIN31<br />
#include <br />
class TOkno : public TApplication<br />
{<br />
public:<br />
TOkno(LPSTR AName, HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
: TApplication(AName, hInstance, hPrevInstance, lpCmdLine,<br />
nCmdShow) {};<br />
void InitMainWindow(){MainWindow = new TWindow(NULL, Name);};<br />
};<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
{<br />
TOkno OBIEKT("Windows - Program PW1", hInstance,<br />
hPrevInstance, lpCmdLine, nCmdShow);<br />
OBIEKT.Run();<br />
return 0;<br />
}<br />
________________________________________________________________<br />
Wykonanie takiej aplikacji przebiega następująco. Windows<br />
wywołują główną funkcję WinMain(), która przekazuje swoje<br />
parametry do konstruktora klasy TOkno::TOkno(). Konstruktor<br />
przekazuje parametry do konstruktora klasy bazowej<br />
TApplication(). Po skonstruowaniu obiektu w pamięci funkcja<br />
wywołuje odziedziczoną metodę Run(). Funkcja Run() wywołuje<br />
metody InitApplication() (zainicjuj aplikację) i InitInstance()<br />
(zainicjuj dane wystąpienie programu). Metoda InitInstance()<br />
wywołuje funkcję InitMainWindow(), która buduje główne okno<br />
aplikacji na ekranie. Po pojawieniu się okna rozpoczyna<br />
działanie pętla pobierania komunikatów (message loop). Pętla<br />
komunikatów działa aż do otrzymania komunikatu WM_QUIT.<br />
Rozbudujmy aplikację o okienko komunikatów. Zastosujemy do tego<br />
funkcję MessageBox(). Funkcja zostanie użyta nie jako metoda<br />
(składnik obiektu), lecz jako "wolny strzelec" (stand alone<br />
function).<br />
Listing B. Maszyna rozszerzona o okienka komunikatów.<br />
________________________________________________________________<br />
#define WIN31<br />
#define STRICT<br />
- 429-
#include <br />
class TOkno : public TApplication<br />
{<br />
public:<br />
TOkno(LPSTR Nazwa, HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
: TApplication(Nazwa, hInstance, hPrevInstance, lpCmdLine,<br />
nCmdShow) {};<br />
void InitMainWindow(){MainWindow = new TWindow(NULL, "Okno<br />
PW2" );};<br />
};<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
{<br />
TOkno OBIEKT("Okno PW2", hInstance, hPrevInstance,<br />
lpCmdLine, nCmdShow);<br />
LPSTR p1 = "Jesli wybierzesz [Anuluj]\n- aplikacja nie<br />
ruszy!";<br />
LPSTR p2 = "START";<br />
if (MessageBox(NULL, p1, p2, MB_OKCANCEL) == IDCANCEL)<br />
MessageBox(NULL, "I juz..." , "KONIEC" , MB_OK);<br />
else<br />
OBIEKT.Run();<br />
return 0;<br />
}<br />
________________________________________________________________<br />
Uwagi techniczne.<br />
Ścieżki do katalogów:<br />
..\INCLUDE;..\CLASSLIB\INCLUDE;..\OWL\INCLUDE;<br />
..\LIB;..\CLASSLIB\LIB;..\OWL\LIB;<br />
Konsolidacja:<br />
Options | Linker | Settings: Windows EXE (typ aplikacji)<br />
Options | Linker | Libraries:<br />
- Container class Libraries: Static (bibl. klas CLASSLIB)<br />
- OWL: Static (bibl. OWL statycze .LIB)<br />
- Standard Run-time Lib: Static (bibl. uruchomieniowe .LIB)<br />
(.) None - oznacza żadne (nie zostaną dołączone);<br />
(.) Static - oznacza statyczne .LIB<br />
(.) Dinamic - oznacza dynamiczne .DLL<br />
________________________________________________________________<br />
JAK ROZBUDOWYWAĆ OBIEKTOWE APLIKACJE?<br />
Mimo całego uroku obiektowych aplikacji pojawia się tu wszakże<br />
drobny problem. Skoro komunikacja klawiatura/myszka -> program<br />
-> ekran nie odbywa się wprost, lecz przy pomocy wymiany danych<br />
pomiędzy obiektami różnych warstw - w jaki sposób (w którym<br />
miejscu programu) umieścić "zwyczajne" funkcje i procedury i jak<br />
zorganizować wymianę informacji. "Zwyczajne" funkcje będą<br />
przecież wchodzić w skład roboczych części naszych programów<br />
- 430-
(Engine). Rozważmy to na przykładzie aplikacji reagującej na<br />
naciśnięcie klawisza myszki. Najbardziej istotny -<br />
"newralgiczny" punkt programu został zaznaczony w tekście "
Konstruktor tradycyjnie wykorzystujemy do przekazania parametrów<br />
do konstruktora klasy bazowej. PTWindowsObject AParent to<br />
wskażnik (PoinTer) do obiektu "okno" a ATitle to string - tytuł.<br />
Obsługa komunikatów kierowanych do tego okna może być<br />
realizowana przy pomocy metod zaimplementowanych jako elemeny<br />
składowe klasy Główne Okno - TGOkno.<br />
Program graficzny powinien reagować raczej na myszkę niż na<br />
klawiaturę. Windows rozpoznają zdarzenia związane z myszką i<br />
generują komunikaty o tych zdarzeniach.<br />
Zdarzenia myszki (mouse events).<br />
________________________________________________________________<br />
Komunikat Zdarzenie<br />
________________________________________________________________<br />
WM_MOUSEMOWE - przesunięto myszkę (wewnątrz obszaru<br />
roboczego - inside the client area -<br />
ICA)<br />
WM_LBUTTONDOWN - naciśnięto LEWY klawisz myszki (ICA)<br />
WM_LBUTTONDBLCLK - naciśnięto dwukrotnie LEWY klaw. (ICA)<br />
WM_LBUTTONUP - puszczono LEWY klawisz (ICA)<br />
WM_RBUTTONDOWN - naciśnięto PRAWY klawisz myszki (ICA)<br />
WM_RBUTTONDBLCLK - naciśnięto dwukrotnie PRAWY klaw. (ICA)<br />
WM_RBUTTONUP - puszczono PRAWY klawisz (ICA)<br />
WM_MBUTTONDOWN - naciśnięto ŚRODK. klawisz myszki (ICA)<br />
WM_MBUTTONDBLCLK - naciśnięto dwukrotnie ŚROD. klaw. (ICA)<br />
WM_MBUTTONUP - puszczono ŚRODKOWY klawisz (ICA)<br />
WM_NCMOUSEMOVE - ruch myszki poza client area (NCA)<br />
WM_NLBUTTONDOWN - naciśnięto LEWY klawisz myszki poza<br />
obszarem roboczym - non-client area<br />
(NCA)<br />
WM_NCLBUTTONDBLCLK - naciśnięto dwukrotnie LEWY klaw. (NCA)<br />
WM_NCLBUTTONUP - puszczono LEWY klawisz (NCA)<br />
WM_NCRBUTTONDOWN - naciśnięto PRAWY klawisz myszki (NCA)<br />
WM_NCRBUTTONDBLCLK - naciśnięto dwukrotnie PRAWY klaw. (NCA)<br />
WM_NCRBUTTONUP - puszczono PRAWY klawisz (NCA)<br />
WM_NCMBUTTONDOWN - naciśnięto ŚR. klawisz myszki (NCA)<br />
WM_NCMBUTTONDBLCLK - naciśnięto dwukrotnie ŚRODK. klaw. (NCA)<br />
WM_LBUTTONUP - puszczono ŚRODKOWY klawisz (NCA)<br />
________________________________________________________________<br />
Następna tabelka zawiera (znacznie skromniejszy) zestaw<br />
komunikatów generowanych pod wpływem zdarzeń związanych z<br />
klawiaturą. Choćby z wizualnego porównaia wielkości tych tabel<br />
wyrażnie widać, że Windows znacznie bardziej "lubią" współpracę<br />
z myszką.<br />
Komunikaty Windows w odpowiedzi na zdarzenia związane z<br />
klawiaturą.<br />
_______________________________________________________________<br />
Komunikat Zdarzenie<br />
- 432-
_______________________________________________________________<br />
WM_KEYDOWN Naciśnięto (jakiś) klawisz.<br />
WM_KEYUP Puszczono klawisz.<br />
WM_SYSKEYDOWN Naciśnięto klawisz "systemowy".<br />
WM_SYSKEYUP Puszczono klawisz "systemowy".<br />
WM_CHAR Kod ASCII klawisza.<br />
________________________________________________________________<br />
Klawisz systemowy to np. [Alt]+[Esc], [Alt]+[F4] itp.<br />
________________________________________________________________<br />
Komunikaty Windows możemy wykorzystać w programie.<br />
...<br />
BOOL CanClose();<br />
void WMLButtonDown(RTMessage Msg)= [WM_FIRST + WM_LBUTTONDOWN];<br />
void WMRButtonDown(RTMessage Msg)= [WM_FIRST + WM_RBUTTONDOWN];<br />
};<br />
Nasze Główne_Okno potrafi obsługiwać następujące zdarzenia:<br />
* Funkcja CanClose() zwróciła wynik TRUE/FALSE,<br />
* Naciśnięto lewy/prawy klawisz myszki.<br />
Komunikat Msg zadeklarowany jako zmienna typu RTMessage jest w<br />
klasie macieżystej TWindow wykorzystywany tak:<br />
_CLASSDEF(TWindow)<br />
class _EXPORT TWindow : public TWindowsObject<br />
{<br />
...<br />
protected:<br />
virtual LPSTR GetClassName()<br />
{ return "OWLWindow"; }<br />
virtual void GetWindowClass(WNDCLASS _FAR & AWndClass);<br />
virtual void SetupWindow();<br />
virtual void WMCreate(RTMessage Msg) = [WM_FIRST +<br />
WM_CREATE];<br />
virtual void WMMDIActivate(RTMessage Msg) =<br />
[WM_FIRST + WM_MDIACTIVATE];<br />
...<br />
virtual void WMSize(RTMessage Msg) = [WM_FIRST + WM_SIZE];<br />
virtual void WMMove(RTMessage Msg) = [WM_FIRST + WM_MOVE];<br />
virtual void WMLButtonDown(RTMessage Msg) = [WM_FIRST +<br />
WM_LBUTTONDOWN];<br />
Zwróć uwagę na notację. Zamiast WM_CREATE pojawiło się [WM_FIRST<br />
+ WM_CREATE]. Komunikat WM_FIRST jest predefiniowany w OWLDEF.H<br />
i musi wystąpić w obiektowych aplikacjach w dowolnej klasie<br />
okienkowej, bądź sterującej (window class/controll class), która<br />
winna odpowiadać na określony komunikat. Oto fragment pliku<br />
OWLDEF.H zawierający definicje stałych tej grupy:<br />
- 433-
#define WM_FIRST 0x0000<br />
/* 0x0000- 0x7FFF window messages */<br />
#define WM_INTERNAL 0x7F00<br />
/* 0x7F00- 0x7FFF reserved for internal use */<br />
#define ID_FIRST 0x8000<br />
/* 0x8000- 0x8FFF child id messages */<br />
#define NF_FIRST 0x9000<br />
/* 0x9000- 0x9FFF notification messages */<br />
#define CM_FIRST 0xA000<br />
/* 0xA000- 0xFFFF command messages */<br />
#define WM_RESERVED WM_INTERNAL - WM_FIRST<br />
#define ID_RESERVED ID_INTERNAL - ID_FIRST<br />
#define ID_FIRSTMDICHILD ID_RESERVED + 1<br />
#define ID_MDICLIENT ID_RESERVED + 2<br />
#define CM_RESERVED CM_INTERNAL - CM_FIRST<br />
W tym momencie zwróćmy jeszcze uwagę, że funkcje z grupy<br />
MessageHandlers są typu void i zwykle są metodami wirtualnymi -<br />
przeznaczonymi "z definicji" do nadpisywania przez programistów<br />
w klasach potomnych. Wszystkie te metody mają zawsze jedyny<br />
argument - referencję do struktury TMessage zdefiniowanej<br />
następująco:<br />
struct TMessage<br />
{<br />
HWND Receiver; //Identyfikator okna - odbiorcy<br />
WORD Message; //sam komunikat<br />
union<br />
{<br />
WORD WParam; //Parametr WParam stowarzyszony z<br />
//komunikatem; ALBO (dlatego unia!)<br />
struct tagWP<br />
{<br />
BYTE Lo;<br />
BYTE Hi;<br />
} WP;<br />
union<br />
{<br />
DWORD lParam;<br />
struct tagLP<br />
{<br />
WORD Lo;<br />
WORD Hi;<br />
} LP;<br />
};<br />
long Result;<br />
};<br />
Po tych wyjaśnieniach możemy zaimplementować poszczególne<br />
funkcje.<br />
void TAplikacja::InitMainWindow()<br />
{<br />
MainWindow = new (0, Name);<br />
- 434-
}<br />
Jeśli wybrano klawisz [Yes] funkcja zwróci IDYES. Jeśli funkcja<br />
zwróciła IDYES - operator porównania zwróci TRUE (prawda) i ta<br />
też wartość zostanie zwrócona przez metodę CanClose:<br />
BOOL TMyWindow::CanClose()<br />
{<br />
return (MessageBox(HWindow, "Wychodzimy?",<br />
"Koniec", MB_YESNO | MB_ICONQUESTION) == IDYES);<br />
}<br />
Stosunkowo najciekawsza kombinacja odbywa się wewnątrz handlera<br />
komunikatu WM_LBUTTONDOWN. Ze struktury komunikatów pobierana<br />
jest zawartość młodszego słowa parametru lParam - Msg.LP.Lo i<br />
starszego słowa Msg.LP.Hi. Są to względne współrzędne graficzne<br />
kursora myszki (względem narożnika okna) w momencie naciśnięcia<br />
lewego klawisza myszki. Funkcja sprintf() zapisuje je w postaci<br />
dwu liczb dziesiętnych %d, %d do bufora znakowego char<br />
string[20]. Funkcja GetDC() (Get Device Context) określa<br />
kontekst urządzenia (warstwa sterownika urządzenia) i dalej<br />
obiekt może już stosując funkcję kontekstową "czuć się"<br />
niezależny od sprzętu. Dane te w postaci znakowej są pobierane<br />
przez funkcję kontekstową OutText() jako string a równocześnie<br />
pobierane są w formie liczbowej: Msg.LP.Hi. Msg.LP.Lo, aby<br />
wyznaczyć współrzędne tekstu na ekranie. Funkcja strlen()<br />
oblicza długość łańcucha znakowego - i to już ostatni potrzebny<br />
nam parametr.<br />
void TMyWindow::WMLButtonDown(RTMessage Msg)<br />
{<br />
HDC DC;<br />
char string[20];<br />
sprintf(string, "(%d, %d)", Msg.LP.Lo, Msg.LP.Hi);
LPSTR lpCmdLine, int nCmdShow)<br />
{<br />
TNAplikacja OBIEKT("Wspolrzedne w oknie", hInstance,<br />
hPrevInstance, lpCmdLine, nCmdShow);<br />
OBIEKT.Run();<br />
return (OBIEKT.Status);<br />
}<br />
Wyświetlanie współrzędnych jakkolwiek wartościowe z<br />
dydaktycznego punktu widzenia jest mało interesujące. Pokusimy<br />
się o obiektową aplikację umożliwiającą odręczne rysowanie w<br />
oknie (freehand drawing).<br />
[!!!]UWAGA<br />
________________________________________________________________<br />
Pakiety <strong>Borland</strong> <strong>C++</strong> 3..4.5 zawierają wiele gotowych "klocków" do<br />
wykorzystania. Oto przykład wykorzystania w pliku zasobów .RC<br />
standardowego okienka wejściowego (Input Dialog Box) i<br />
standardowego okienka typu Plik (File Dialog Box):<br />
#include <br />
#include <br />
rcinclude INPUTDIA.DLG<br />
rcinclude FILEDIAL.DLG<br />
ROZKAZY MENU LOADONCALL MOVEABLE PURE DISCARDABLE<br />
BEGIN<br />
POPUP "&File"<br />
BEGIN<br />
MENUITEM "&New" CM_FILENEW<br />
MENUITEM "&Open" CM_FILEOPEN<br />
MENUITEM "&Save" CM_FILESAVE<br />
END<br />
END<br />
Takie menu można zastosować w programie obiektowym umieszcając<br />
je w konstruktorze i dokonując nadpisania metody AssignMenu()<br />
(przypisz menu):<br />
TGOkno::TGOkno(PTWindowsObject AParent, LPSTR ATitle) :<br />
TWindow(AParent, ATitle)<br />
{<br />
AssignMenu("ROZKAZY");<br />
...<br />
}<br />
[S]<br />
rcinclude - dołącz zasoby<br />
LOADONCALL - załaduj po wywołaniu<br />
owlrc - zasoby biblioteki klas OWL<br />
- 436-
Gotowe "klocki" można wykorzystać nawet wtedy, gdy nie pasują w<br />
100%. Inne niż typowe odpowiedzi na wybór rozkazu implementujemy<br />
w programie głównym poprzez nadpisanie wirtualnej metody<br />
virtual void CMFileOpen(RTMessage msg) =<br />
[CM_FIRST + CM_FILEOPEN]<br />
TGOkno GOkno;<br />
void TGOkno::CMFileOpen(RTMessage)<br />
{<br />
... obsługa zdarzenia ...<br />
}<br />
________________________________________________________________<br />
[Z]<br />
________________________________________________________________<br />
1. Przeanalizuj gotowe zasoby dołączone do Twojej wersji <strong>Borland</strong><br />
<strong>C++</strong>.<br />
2. Uruchom kilka projektów "firmowych" dołączonych w katalogu<br />
\EXAMPLES. Zwróć szczególną uwagę na projekty STEPS (kolejne<br />
kroki w tworzeniu aplikacji obiektowej).<br />
________________________________________________________________<br />
- 437-
LEKCJA 46. APLIKACJA OBIEKTOWA - RYSOWANIE W OKNIE.<br />
________________________________________________________________<br />
W trakcie tej lekcji opracujemy obiektową aplikację psoługując<br />
się biblioteką klas Object Windows Library.<br />
________________________________________________________________<br />
Zaczniemy oczywiście od standardowych "klocków". Definicja klasy<br />
Nasza_Aplikacja i moduł prezentacyjno - uruchomieniowy będą<br />
wyglądać standardowo, nie musimy im zatem poświęcać zbytniej<br />
uwagi. Przytoczymy je jedynie. Pointer do napisu inicjujemy po<br />
to, by okienko komunikatu zawierało jakąś bardziej konkretną<br />
informację dla użytkownika. Rysunki z wnętrza tej aplikacji<br />
można przy pomocy Schowka przenieść jako pliki .CLP, bądź za<br />
pomocą PAINTBRUSH - jako .BMP, .PCX i drukować.<br />
#include <br />
LPSTR Ptr = "Jesli chcesz zapamietac rysunek, \<br />
powinienes przeniesc go do Clipboard'u \<br />
klawiszami [Print Screen] \<br />
lub [Alt]+[PrtScr].";<br />
class TNAplikacja : public TApplication<br />
{<br />
public:<br />
TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE<br />
hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
: TApplication(AName, hInstance, hPrevInstance, lpCmdLine,<br />
nCmdShow) {};<br />
virtual void InitMainWindow();<br />
};<br />
...<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
{<br />
TNAplikacja OBIEKT("Rysownik. Prawy klawisz umozliwia wyjscie.",<br />
hInstance, hPrevInstance, lpCmdLine, nCmdShow);<br />
OBIEKT.Run();<br />
return (OBIEKT.Status);<br />
}<br />
Nic specjalnie ciekawego nie dzieje się w funkcji inicjującej<br />
główne okno, ani w funkcji zamykającej aplikację. Zmieniły się<br />
tylko napisy w okienku komunikatów.<br />
void TNAplikacja::InitMainWindow()<br />
{<br />
MainWindow = new TGOkno(0, Name);<br />
}<br />
BOOL TGOkno::CanClose()<br />
- 438-
{<br />
return (MessageBox(HWindow, Ptr, "KONIEC",<br />
MB_YESNO | MB_ICONQUESTION) == IDYES);<br />
}<br />
Zajmiemy się teraz główną "maszynerią" programu. Rozbudujemy<br />
obsługę komunikatów przez handlery zaimplenmentowane w klasie<br />
Główne_Okno.<br />
_CLASSDEF(TGOkno)<br />
class TGOkno : public TWindow<br />
{<br />
public:<br />
HDC dc;<br />
BOOL ButtonDown;<br />
BOOL Flaga_Start;<br />
TGOkno(PTWindowsObject AParent, LPSTR ATitle);<br />
//Konstruktor<br />
virtual void WMLButtonDown(RTMessage Msg)<br />
= [WM_FIRST + WM_LBUTTONDOWN];<br />
virtual void WMLButtonUp(RTMessage Msg)<br />
= [WM_FIRST + WM_LBUTTONUP];<br />
virtual void WMMouseMove(RTMessage Msg)<br />
= [WM_FIRST + WM_MOUSEMOVE];<br />
virtual void WMRButtonDown(RTMessage Msg)<br />
= [WM_FIRST + WM_RBUTTONDOWN];<br />
virtual BOOL CanClose();<br />
};<br />
Konstruktor przekazuje parametry do konstruktora klasy bazowej i<br />
zeruje flagę ButtonDown - lewy klawisz myszki przyciśnięty.<br />
TGOkno::TGOkno(PTWindowsObject AParent, LPSTR ATitle)<br />
: TWindow(AParent, ATitle)<br />
{<br />
ButtonDown = FALSE;<br />
}<br />
Funkcja obsługująca zdarzenie WM_LBUTTONDOWN jeden raz inicjuje<br />
obsługę myszki i ustawia flagę. Funkcje SetCapture() i GetDC()<br />
załatwiją problem relacji kontekstowych i określają obszar<br />
roboczy (client area). Jeśli umieścimy te funkcje w<br />
konstruktorze za obszar client area uznany zostanie cały ekran.<br />
Po zadziałaniu tych funkcji komunikaty od myszki będą dotyczyć<br />
wyłącznie obszaru roboczego. Do naciśnięcia prawego klawisza nie<br />
będzie dostępu do "ramki" okna.<br />
void TGOkno::WMLButtonDown(RTMessage Msg)<br />
{<br />
if (!Flaga_Start)<br />
{<br />
- 439-
Flaga_Start = TRUE; //UWAGA:<br />
SetCapture(HWindow); //Jesli zainicjujemy SetCapture()<br />
dc = GetDC(HWindow); //w konstruktorze - mamy caly ekran<br />
}<br />
MoveTo(dc, Msg.LP.Lo, Msg.LP.Hi);<br />
ButtonDown = TRUE;<br />
}<br />
Funkcja MoweTo() powoduje przesunięcie kursora graficznego do<br />
aktualnej pozycji myszki (już względnej - z uwzględnieniem dc)<br />
bez rysowania linii. Flaga ButtnDown została ustawiona.<br />
Rysowanie scedujemy na metodę obsługującą WM_MOUSEMOVE -<br />
przesunięcie myszki.<br />
void TGOkno::WMMouseMove(RTMessage Msg)<br />
{<br />
if (ButtonDown)<br />
LineTo(dc, Msg.LP.Lo, Msg.LP.Hi);<br />
}<br />
Jeśli lewy klawisz jest naciśnięty - funkcja LineTo() będzie<br />
kreślić linię do kolejnych punktów "śledząc" ruch myszki. Jeśli<br />
użytkownik puści lewy klawisz - zerujemy flagę stanu klawisza<br />
ButtonDown
klawiszami [Print Screen] \<br />
lub [Alt]+[PrtScr].";<br />
class TNAplikacja : public TApplication<br />
{<br />
public:<br />
TNAplikacja(LPSTR AName, HANDLE hInstance, HANDLE<br />
hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
: TApplication(AName, hInstance, hPrevInstance, lpCmdLine,<br />
nCmdShow) {};<br />
virtual void InitMainWindow();<br />
};<br />
_CLASSDEF(TMyWindow)<br />
class TMyWindow : public TWindow<br />
{<br />
public:<br />
HDC dc;<br />
BOOL ButtonDown;<br />
BOOL Flaga_Start;<br />
TMyWindow(PTWindowsObject AParent, LPSTR ATitle);<br />
//Konstruktor<br />
virtual void WMLButtonDown(RTMessage Msg)<br />
= [WM_FIRST + WM_LBUTTONDOWN];<br />
virtual void WMLButtonUp(RTMessage Msg)<br />
= [WM_FIRST + WM_LBUTTONUP];<br />
virtual void WMMouseMove(RTMessage Msg)<br />
= [WM_FIRST + WM_MOUSEMOVE];<br />
virtual void WMRButtonDown(RTMessage Msg)<br />
= [WM_FIRST + WM_RBUTTONDOWN];<br />
virtual BOOL CanClose();<br />
};<br />
TMyWindow::TMyWindow(PTWindowsObject AParent, LPSTR ATitle)<br />
: TWindow(AParent, ATitle)<br />
{<br />
ButtonDown = FALSE;<br />
}<br />
void TMyWindow::WMLButtonDown(RTMessage Msg)<br />
{<br />
if ( !Flaga_Start )<br />
{<br />
Flaga_Start = TRUE; //UWAGA:<br />
SetCapture(HWindow); //Jesli zainicjujemy SetCapture()<br />
dc = GetDC(HWindow); //w konstruktorze - mamy caly ekran<br />
}<br />
MoveTo(dc, Msg.LP.Lo, Msg.LP.Hi);<br />
ButtonDown = TRUE;<br />
}<br />
void TMyWindow::WMMouseMove(RTMessage Msg)<br />
- 441-
{<br />
if ( ButtonDown )<br />
LineTo(dc, Msg.LP.Lo, Msg.LP.Hi);<br />
}<br />
void TMyWindow::WMLButtonUp(RTMessage)<br />
{<br />
if (ButtonDown) ButtonDown = FALSE;<br />
}<br />
void TMyWindow::WMRButtonDown(RTMessage)<br />
{<br />
InvalidateRect(HWindow, NULL, TRUE);<br />
ReleaseCapture();<br />
ReleaseDC(HWindow, dc);<br />
Flaga_Start = FALSE;<br />
}<br />
void TNAplikacja::InitMainWindow()<br />
{<br />
MainWindow = new TMyWindow(0, Name);<br />
}<br />
BOOL TMyWindow::CanClose()<br />
{<br />
return (MessageBox(HWindow, Ptr, "KONIEC",<br />
MB_YESNO | MB_ICONQUESTION) == IDYES);<br />
}<br />
int PASCAL WinMain(HANDLE hInstance, HANDLE hPrevInstance,<br />
LPSTR lpCmdLine, int nCmdShow)<br />
{<br />
TNAplikacja OBIEKT("Rysownik. Prawy klawisz umozliwia<br />
wyjscie.", hInstance, hPrevInstance,<br />
lpCmdLine, nCmdShow);<br />
OBIEKT.Run();<br />
return (OBIEKT.Status);<br />
}<br />
________________________________________________________________<br />
- 442-
LEKCJA 47. O PAKIETACH BORLAND <strong>C++</strong> 4/4.5.<br />
________________________________________________________________<br />
Z tej lekcji dowiesz się, czy warto kupić nowszą wersję <strong>Borland</strong><br />
<strong>C++</strong> 4/4.5 i jakie niespodzianki czekają Cię po zamianie<br />
kompilatora na nowszy.<br />
________________________________________________________________<br />
Czy warto sprawić sobie BORLAND <strong>C++</strong> 4/4.5 ?<br />
Kilka słów o tym, co oferuje <strong>Borland</strong> w pakietach "<strong>Borland</strong> <strong>C++</strong><br />
4/4.5" i jakie niespodzianki czekają nowych użytkowników przy<br />
instalacji i uruchamianiu.<br />
Wymagania sprzętowe i instalacja<br />
Aby instalacja i użytkowanie pakietu przebiegało poprawnie,<br />
zaleca się następującą konfigurację sprzętu:<br />
Wymagania sprzętowe <strong>Borland</strong> <strong>C++</strong> 4.<br />
________________________________________________________________<br />
Parametr minimum zalecane (pełna konfig.)<br />
________________________________________________________________<br />
* procesor 80386/33 MHZ 486 DX (lub 386 + 387)<br />
* miejsce na dysku 8 MB 80 MB (bez kompresji)<br />
* pamięć RAM 4 MB 8 MB i więcej<br />
* system DOS 4.01 DOS 6.0...6.22<br />
* MS Windows 3.1 Windows NT<br />
________________________________________________________________<br />
Częściowa instalacja <strong>Borland</strong> <strong>C++</strong> 4.<br />
________________________________________________________________<br />
Konfiguracja<br />
Dysk<br />
________________________________________________________________<br />
1. Kompilator BCC 16 bitowy (D+W) 9 MB<br />
2. Kompilator BCC 32 bitowy (D+W) 13 MB<br />
3. Środowisko IDE 16 bitowe 26 MB<br />
4. Środowisko IDE 32 bitowe 30 MB<br />
5. Tylko dla DOS (minimum) 8 MB<br />
________________________________________________________________<br />
* D+W - dla DOS i Windows<br />
Można próbować zainstalować <strong>Borland</strong> <strong>C++</strong> 4 na małym dysku, można<br />
także ograniczyć się do 4 MB RAM, ale generowanie 32-bitowych<br />
aplikacji będzie wtedy znacznie utrudnione a praca kompilatora<br />
wolniejsza. W przypadku stosowania kompresorów (np. SUPERSTOR,<br />
DOUBLE SPACE) należy pamiętać, że wtórna kompresja plików jest<br />
mało skuteczna i dysk zgłaszany jako 80 MB może okazać się<br />
"ciasny".<br />
<strong>Borland</strong> <strong>C++</strong> 4 można instalować z dyskietek, bądź z CD-ROM.<br />
Ponieważ pakiet B<strong>C++</strong> 4 jest "okienkowo - zorientowany", nawet<br />
- 443-
program instalacyjny wymaga obecności Windows. Uruchomienie<br />
programu instalacyjnego następuje zatem z poziomu Menedżera<br />
programów rozkazem File | Run... (w spolszczonej wersji Windows<br />
- Plik | Uruchom...) lub z DOS-owskiego wiersza rozkazu:<br />
C:\>WIN X:INSTALL<br />
Opcji jest trochę więcej - o najciekawszych z nich - kilka słów<br />
poniżej.<br />
Warto zwrócić uwagę na tzw. "rozszerzenie dla Windows"<br />
(extention to MS Windows) - Win32s. W programie INSTALL.EXE do<br />
zainstalowania tego pakietu (pakiet jest oryginalnym produktem<br />
Microsofta i wymaga 8 MB przestrzeni dyskowej) służy opcja<br />
[Install Win32s]. Najważniejszy plik-driver instaluje się w<br />
pliku SYSTEM.INI:<br />
device=X:\WINDOWS\SYSTEM\WIN32S\W32S.386<br />
Pozwala to na uruchamianie 32 - bitowych aplikacji pod Windows<br />
3.1. Jeśli masz Windows NT - jest to zbędne - o ten "drobiazg"<br />
zadbał już Microsoft.<br />
W przypadku instalacji w sieci, gdzie Windows zainstalowane są<br />
na serwerze należy pamiętać, że B<strong>C++</strong> 4 w trakcie instalacji<br />
modyfikuje nie tylko klasyczne parametry systemu:<br />
FILES=40<br />
BUFFERS=40<br />
PATH=...;X:\BC4\BIN;<br />
ale także pliki konfiguracyjne i inicjacyjne w katalogu WINDOWS:<br />
WIN.INI, PROGMAN.INI, SYSTEM.INI,<br />
oraz tworzy nowe własne pliki, które będzie próbował zapisać w<br />
katalogach \WINDOWS i \WINDOWS\SYSTEM, np. BCW.INI, TDW.INI,<br />
HELP.ICO, OWL.INI, BWCC.DLL, itp. (łącznie 18 nowych plików).<br />
Brak prawa zapisu na dysk serwera może uniemożliwić poprawną<br />
instalację i skonfigurowanie B<strong>C++</strong> 4/4.5 w sieci.<br />
<strong>Borland</strong> wraz z wersjami bibliotek dostarcza komplet kodów<br />
źródłowych. Jeśli chcesz - możesz sam wygenerować sobie całą<br />
bibliotekę, jeśli chcesz - możesz na własne oczy przekonać się<br />
jak to wszystko działa i jak jest zbudowane. Oprócz<br />
teoretycznych możliwości poznawczych daje to praktyczną<br />
możliwość dostosowania bibliotek do nowej wersji kompilatora, co<br />
w przypadku "czwórki" może okazać się dla wielu użytkowników<br />
bardzo przydatne (o czy dokładniej za chwilę).<br />
Oprócz klasycznego paska głównego menu zintegrowane środowisko<br />
(IDE) zostało wyposażone w rozbudowaną listwę narzędziową.<br />
- 444-
W skład pakietu wchodzą między innymi:<br />
* BCW - zintegrowane środowisko (IDE) dla środowiska Windows<br />
* TDW - Turbo Debugger for Windows<br />
* BCC - kompilator uruchamiany z DOS'owskiego wiersza rozkazu<br />
* BCC32 - kompilator 32 - bitowy (odpowiednik BCC)<br />
* BRCC - kompilator zasobów do kompilacji plików *.RC z zasobami<br />
do postaci binarnej *.RES<br />
* RLINK - konsolidator służący do dołączania plików z zasobami<br />
przy tworzeniu plików wykonywalnych *.EXE<br />
* TLINK - "zwykły" konsolidator<br />
* MAKE - program narzędziowy do automatyzacji kompilacji i<br />
konsolidacji, korzystający z tzw. plików instruktażowych<br />
(emulujący NMAKE Microsofta)<br />
* WINSIGHT - przeglądanie informacji o okienkach (dla Windows) i<br />
komunikatach<br />
* TDUMP - bezpośrednie przeglądanie informacji zawartych w<br />
plikach *.EXE i *.OBJ<br />
* TDSTRIP - narzędzie do usuwania tablicy symboli z plików<br />
wykonywalnych<br />
* IMPLIB - importer bibliotek z DLL<br />
* TDMEM - wyświetlanie informacji o zajętości pamięci<br />
* MAKESWAP - zarządzanie swapowaniem (tworzenie plików<br />
tymczasowych EDPMI.SWP o zadanej wielkości)<br />
i jeszcze parę narzędzi (np. tradycyjny bibliotekarz TLIB,<br />
TOUCH, GREP, itp.), o których tu nie wspominam.<br />
Czego robić nie należy?<br />
Przede wszystkim nie należy traktować <strong>Borland</strong> <strong>C++</strong> 4/4.5 jako<br />
"upgrade" do wcześniejszych wersji (3, czy 3.1). W kompilatorze<br />
dokonano sporych zmian (np. inaczej działa operator new). Nie<br />
wolno zatem "nadpisać" zawartości poprzednich katalogów i plików<br />
o tych samych nazwach. Szczególnie dotyczy to plików<br />
konfiguracyjnych BCCONFIG.BCW i TDCONFIG.TDW. Jeśli stare wersje<br />
tych plików nie zostaną przemianowane, bądź usunięte z pola<br />
widzenia (PATH) - pojawią się konflikty przy uruchamianiu B<strong>C++</strong>.<br />
Ze względu na wprowadzone zmiany pliki .OBJ tworzone przez<br />
wcześniejsze kompilatory C będą w zasadzie przenośne, natomiast<br />
pliki .OBJ i biblioteki utworzone przez wcześniejsze wersje<br />
kompilatorów <strong>C++</strong> (szczególnie <strong>Borland</strong> <strong>C++</strong> 3.1) będą sprawiać<br />
kłopoty (nie będą np. poprawnie wywoływane destruktory). Przy<br />
konsolidacji "starych" plików można stosować opcję -K2<br />
konsolidatora, co pozwoli zmniejszyć do minimum ryzyko<br />
konfliktów.<br />
Jeśli jest już <strong>Borland</strong> Pascal 7...<br />
Jeśli masz już zainstalowany <strong>Borland</strong> Pascal 7 należy pamiętać,<br />
- 445-
że poprawna praca obu kompilatorów w jednym systemie wymaga<br />
"uregulowania stosunków":<br />
1. Każdy kompilator musi mieć własną kopię debuggera TDW. Aby<br />
uniknąć konfliktu pascalowski debugger można przemianować np.:<br />
TDW.EXE --> PASTDW.EXE<br />
2. Należy usunąć stare pliki inicjujące TDW.INI. Można tu<br />
posłużyć się narzędziem TDWINI.EXE.<br />
3. Należy sprawdzić poprawność instalacji driverów w pliku<br />
SYSTEM.INI:<br />
DEVICE=X:\BC4\BIN\WINDPMI.386<br />
DEVICE=X:\BC4\BIN\TDDEBUG.386
- inną bibliotekę Runtime Library<br />
Kod żródłowy biblioteki znajduje się w katalogu:<br />
\BIN\TVISION\SOURCE<br />
Po (Uwaga!) wprowadzeniu kilku niewielkich zmian<br />
- do plików żródłowych .CPP<br />
- do pliku instruktażowego MAKEFILE<br />
oraz po skompilowaniu przy pomocy BCC 4 w DWU WERSJACH: TVO.LIB<br />
(z nakładką - Overlay) i TVNO.LIB (bez nakładki - No Overlay)<br />
biblioteka TVL może być nadal z powodzeniem stosowana z <strong>Borland</strong><br />
<strong>C++</strong> 4. Podobnie rekompilacji wymaga bibiloteka klas dołączona w<br />
wersji żródłowej w katalogu X:\BC4\SOURCE\CLASSLIB.<br />
O AUTOMATYZACJI - CASE.<br />
Prócz znanego już od dość dawna (w komputerologii kilka lat to<br />
cała epoka) tradycyjnego narzędzia Resource Worshop, w wersji<br />
BC4 występują jeszcze inne narzędzia CASE kategorii "wizard"<br />
(kreator aplikacji):<br />
- ClassExpert<br />
- ApplicationExpert<br />
- DialogExpert<br />
- TargetExpert<br />
Nazwa TargetExpert pochodzi od ang. "Target platform" - docelowa<br />
platforma pracy aplikacji (DOS, Win16, Win32).<br />
Biblioteka OWL 2.0 została wzbogacona o dodatkowe klasy VBX<br />
umożliwiające współpracę z Visual Basic i wykorzystanie<br />
elementów utworzonych przy pomocy VB.<br />
Wspomaganie tworzenie programu przy pomocy tych narzędzi<br />
(AppExpert podobnie jak inne narzędzie typu Wizard jest<br />
automatycznym generatorem aplikacji) wymaga od użytkownika<br />
wyboru z listy "zagadnienia" a z okienek docelowych cech<br />
programu. Przytoczę tu dla przykładu listę opcji z pojedynczego<br />
okienka AppExperta z krótkim wyjaśnieniem:<br />
________________________________________________________________<br />
Topics:<br />
(okienko z listą: Zagadnienia)<br />
Application (program docelowy)<br />
-- Basic Opttions (wybór opcji podstawowych)<br />
-- Advanced Options (opcje zaawansowane)<br />
-- Code Gen Control (sposób generacji kodu)<br />
-- Admin Options (opcje "administracyjne")<br />
Main Window (główne okno programu)<br />
-- Basic Options (podstawowe opcje)<br />
-- SDI Client (interf. jednego dokumentu)<br />
-- MDI Client (interf. wielu dokumentów)<br />
MDI Child/View (okna potomne, widok/edycja)<br />
-- Basic Options (opcje podstawowe)<br />
- 447-
Model:<br />
(Szkielet programu)<br />
[X] Multiple document interface - interfejs MDI<br />
[ ] Single document interface - interfejs SDI<br />
Features:<br />
(cechy)<br />
[.] SpeedBar (ma pasek narzędzi)<br />
[.] Status line (ma wiersz statusowy)<br />
[.] Drag/drop (obsługuje ciągnij/upuść)<br />
[.] Printing (obsługuje drukarkę)<br />
________________________________________________________________<br />
Po wybraniu w okienku klawisza [Generate] (wygeneruj) AppExpert<br />
generuje szkielet programu aplikacji o podanych własnościach.<br />
Wygenerowane zostaje od sześciu do dziewięciu (zależnie od<br />
ustawienia opcji i Twoich życzeń) plików projektu:<br />
*.IDE<br />
*.APX<br />
*.RC<br />
*.RH<br />
*.H<br />
*.CPP<br />
*.HPJ<br />
*.RTF<br />
*.ICO<br />
- plik projektu (lub .PRJ)<br />
- plik roboczy AppExpert'a (odpowiednik<br />
.PRJ)<br />
- plik zasobów<br />
- plik nagłówkowy zasobów<br />
- plik nagłówkowy, źródłowy<br />
- moduł główny źródłowy<br />
- plik pomocy<br />
- źródłowy pomocy kontekstowej<br />
- ikonka projektu<br />
Przy pomocy rozkazu Generate makefile można również<br />
automatycznie utworzyć plik instruktażowy MAKEFILE dla<br />
generatora MAKE.EXE.<br />
Uzyskany plik szkieletowy *.CPP należy tylko uzupełnić o obsługę<br />
interesujących nas zdarzeń/komunikatów. Przyspiesza to znacznie<br />
tworzenie typowych aplikacji.<br />
Programiści wszystkich krajów...<br />
B<strong>C++</strong> 4 zawiera bibliotekę LOCALE.DLL umożliwiającą obsługę<br />
angielsko- francusko- i niemiecko- języczną. <strong>Borland</strong> zapowiada,<br />
że następne wersje będą coraz bogatsze. Doczekaliśmy się<br />
spolszczenia Windows - może i <strong>Borland</strong> <strong>C++</strong> po polsku już tuż tuż?<br />
Póki co, najwygodniej podmienić czcionki.<br />
________________________________________________________________<br />
ZAKOŃCZENIE.<br />
I to już niestety koniec. Po przeanalizowaniu historii:<br />
programowania sekwencyjnego i strukturalnego<br />
oraz nowoczesnych styli programowania:<br />
obiektowego i zdarzeniowego<br />
- 448-
pozostał Ci już tylko wykonanie trzech rzeczy. Powinieneś teraz:<br />
1. Pisać własne aplikacje<br />
2. Pisać własne aplikacje<br />
3. Pisać własne aplikacje<br />
Tak naprawdę - jest to jedyny sposób, by zostać dobrym<br />
programistą.<br />
Przez pewien czas okaże Ci się zapewne przydatna dyskietka<br />
dołączona do książki. Znajdziesz tam jeszcze sporo programów<br />
przykładowych, które nie zmieściły się w książce.<br />
Przyjemnej pracy z programem MEDYT.<br />
- 449-