This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Ali rights reserved. No part ofthis book may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording or by any information storage
retrieval system, without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji .
Wszystkie znaki ich właścicieli.
występujące
w
tekście są zastrzeżonymi
znakami firmowymi
bądź
towarowymi
The Wrox Brand trade dres s is a trademark ofWiley Publishing, Inc. in the United States and/or other
countries. Used by permission.
Visual C++ is registered trademark of Microsoft Corporation in the United States and/or other
countries. Ali other trademarks are the property of their respective owners.
The Wrox Brand jest zastrzeżonym znakiem towarowym Wiley Publishing, Inc . na terenie Stanów
Zjednoczonych i innych krajów. Wykorzystano za zgodą właściciela .
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje
były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialnościani za ich wykorzystanie,
ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz
Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody
wynikłe z wykorzystania informacji zawartych w książce .
Wydawnictwo HELION
ul. Kościuszki lc , 44-100 GLIWICE
tel. 032 231 22 19, 032 230 98 63
e-mail : he/ion@he/ion.p/
WWW : http://he/ion.p/(księgarnia internetowa, kata
Drogi Czytelniku!
chcesz ocenić tę książkę, zajrzyj pod adres
http://he/ion.p//user/opinie?vcpppo Możesz tam wpisać swoje uwagi, spostrzeżenia , rec Jeżeli
Printed in Poland .
Ks iążkę dedykuję A/exandrowi Gi/bey owi.
Z n iecierpli woś cią oczekuję na jego komentarze,
a/e pewnie będę musial j eszcze troch ę p oczekać.
Podziękowania Za włożony wysiłek i wsparcie chciałbym podziękować wydawnictwu John Wiley & Sons oraz zespołowi edytorskiemu i produkcyjnemu wydawnictwa Wrox . W szczególności chciał bym tutaj wyróżnić redaktora naczelnego ds. rozwoju - Kevina Kenta, który wspierał mnie od samego początku do końca pisania tej książki. Chciałbym również podziękować edytorowi technicznemu - Johnowi Muellerowi za dokładne przeczytanie mojego tekstu i poprawienie, mam nadzieję, większości popełnionych przeze mnie błędów, wypróbowanie wszystkich fragmentów kodu oraz konstruktywne komentarze, które uczyniły tę książkę o wiele lepszą. Na koniec chciałbym podziękować mojej żonie , Eve, za jej cierpliwość , pogodę ducha oraz wspieranie mnie podczas długiego okresu przelewania moich myśli na karty tej książki. Jak już wielokrotnie wspominałem wcześniej - bez niej książka ta nie mogłaby powstać.
Spis treści oautorze
19
Wstęp
21
Rozdział
1. Programowanie przy użyciu Visual C++ 2005
Środow is ko programistyczne .NET
Common Łanguage Runtime (CLR) Pisanie programów w C++ Nauka programowania dla systemu Windows Nauka C++ Standardy C++ Aplikacje działające w trybie konsoli Koncepcje programowania w systemie Windows Czym jest zintegrowane środow isko programistyczne Składn iki systemu Używanie IDE Opcje paska narzędz i Dokowalne paski narzędzi Dokumentacja Projekty i rozwiązania Ustawianie opcji w Visual C++ 2005 Tworzenie i uruchamianie programów dla Windowsa Tworzenie aplikacji Windows Forms Podsumowan ie Rozdział 2.Dane. zmienne i dZiałania
arytmetyczne
Struktura programu w C++ Funkcja mainO Instrukcje programu Białe znaki Bloki instrukcj i Programy konsolowe generowane automatyczn ie Definiowanie zmiennych Zasady nadawania nazw zmiennym Deklarowanie zmiennych Wartość początkowa zmiennej Podstawowe typy danych Zmienne całkowite Znakowe typy danych Modyfikatory typu integer Typ logiczny
27
27
28
29
30
31
32
32
33
35
35
37
3 8
39
39
40
54
55
58
61
63
64
71
72
74
75
75
76
77
78
79
80
80
81
82
83
6
Visual C++ 2005. Od podstaw Typy zmienno pozycyjne Literały
Definiowanie synonimów typów danych Zmienne o okre ś lonych zbiorach wartości Określanie typu st-ałych wyliczeniowych Podstawowe operacj e wejś c ia -wyj śc ia Wprowadzanie danych z klawiatury Wysyłanie danych do wiersza poleceń Formatowan ie wysyłanych danych Kodowanie znaków specjalnych Wykonywanie obliczeń w C++ Instrukcja przypisania Działania arytmetyczne Obliczanie reszty Modyfikowanie zmiennej Operatory inkrem entacji i dekrementacji Kolejność wykonywania obliczeń Typy zmiennych i rzutowanie Zasady rzutowania operandów Rzutowanie w instrukcjach przypisania Rzutowanie jawne Rzutowanie w starym stylu Operatory bitowe Czas życi a i zas ięg zmiennych Zmienne automatyczne Pozycjonowanie deklaracj i zmiennych Zmienne globalne Zmienne statyczne Przestrzenie nazw Deklarowanie przest rzeni nazw Wielokrotne deklar acje przestrzeni nazw Programowanie w C++jCLI Fundamentalne typy danych w C++jCLI Wysyłan ie danych do wiersza poleceń w C++jCLI C++jCLI - formatowanie danych wyjściowych C++jCLI - wprowadzanie danych z klawiatury Bezpieczne rzutowanie Wyliczenia w C++jCLI Podsumowanie Ćwiczenia Rozdział 3.Decyzje i pętle
Porównywanie wartości Instrukcja warunkowa if Zagnieżdżanie inst rukcji warunkowych if Rozszerzona instrukcja warunkowa if Zagnieżdżanie instru kcji warunkowych lf-else Operatory logiczne i wyrażen ia Operator warunkowy Instrukcja switch Przejśc ie bezwarunkowe
:
84
85
86
87
88
89
89
90
91
92
94
9 4
95
100
101
102
104
106
106
107
108
109
109
116
116
119
119
123
123
125
126
128
128
133
133
136
137
138
141
142
145
145
147
148
150
152
154
158
159
162
Spis Ireści Powtarzanie bloków instrukcji Czym jest pętla Różne sposoby użycia pętli for Pętla while Pętla do-while Zagnieżdżanie pętli
Programowanie w C++jCLI Pętla for each Podsumowan ie Ćwiczenia
Rozdział 4. Tablice, łańcuchy
znaków i wskaźniki
Obsługa
wielu wartości danych tego samego typu Tablice Deklarowanie tablic Inicjalizacja tablic Tablice znakowe oraz obsługa łańcuchów Tablice wielowymiarowe Pośredni dostęp do danych Czym jest wskaźnik Deklarowanie wskaźników Używanie wskaźników , Inicjalizowanie wskaźników Operator sizeof Stałe wskaźniki oraz wskaźniki do stałych Wskaźniki i tablice Dynamiczne przydzielanie pamięci Pam ięć wolna, czyli sterta : Operatory new i delete Dynamiczne przydzielanie pamięc i tablicom Dynamiczne przydzielanie pamięci tablicom wielowymiarowym Używanie referencji Czym jest referencja Deklarowanie i inicjalizowanie referencji Programowanie w C++JCLI Uchwyty śledzące Tablice CLR Łańcuchy
Referencje
śledzące
Wskaźniki wewnętrzne
Podsumowan ie Ćwiczenia
Rozdział5. Wprowadzanie struktury do programu Zrozumieć
funkcje Do czego potrzebne są funkcje Struktura funkcji Używanie funkcji Przekazywanie argumentów do funkcji Mechanizm przekazywania przez wartość Wskaźniki jako argumenty funkcji
7 163
163
165
174
176
177
180
184
187
187
189
190
190
191
194
196
200
203
203
204
205
207
213
215
217
224
224
224
225
228
229
229
229
230
231
233
248
258
258
261
263
265
266
267
267
269
273
274
275
8
Visual C++ 2005. Od podstaw Przekazywa nie tabl ic do funkcji Referencje jako argumenty funkcj i Zastosowanie modyfikatora const Argumenty funkcji malm) Akceptowanie zmiennej liczby argumentów funkcji Zwracanie wartości przez funkcję Zwracanie wskaźnika Zwracanie referencji Zmienna statyczna w funkcji Wywołania funkcj i rekurencyjnej Stosowanie rekurencji Programowanie w C++/CLI Funkcje przyjmujące zmienną liczbę argumentów Argumenty funkcji maint) Podsumowan ie
277
281
283
285
287
289
289
292
295
297
300
300
301
302
303
304
Ćwiczenia
Rozdział 6. Ostrukturze programu -
ciąg
dalszJ
305
Wskaźniki
do funkcji Deklarowan ie wskaźników do funkcji Wskaźnik do funkcji jako argument Tablice wskaźników do funkcj i Inicjalizowan ie parametrów funkcji
Wyjątki Wywoływanie wyjątków
Przechwytywanie wyjątków MFC Obsługa błędów przydzielania pamięci Przeładowywaniefunkcji Czym jest przeładowywaniefunkcji Kiedy stosować przeładowywanie funkcji Szablony funkcji Stosowanie szablonu funkcji Przykład używania funkcji Implementacja kalkulatora Usuwanie spacji z łańcucha Obliczanie wartości wyrażen ia Obliczanie wartości składn ika Analizowanie liczby Składanie całego programu Rozszerzanie programu Wydobywanie podłańcucha Uruchamianie zmodyfikowanego programu Programowanie w C++/CLI Funkcje generyczne Kalkulator CLR Podsumowanie Obsługa wyjątków w
Ćwiczenia
:
306
306
309
311
312
314
316
316
318
318
320
321
323
323
324
326
326
330
330
333
334
337
339
340
343
343
345
351
357
358
Spis treści Rozdział 7. Deliniowanie własnych
Iypów danych
Struktury w języku C++ Czym jest struktura Definiowanie struktury Inicjali zowanie struktury Uzyskiwanie dostępu do pól struktury Pomoc mechanizmu Intellisense w pracy ze strukturami Struktura RECT Używanie wskaźników ze strukturam i Typy danych , obiekty, klasy i egzemplarze Zrozumieć klasy Definiowanie klasy Deklarowanie obiektów klasy Uzyskiwanie dostępu do zmiennych składowych klasy Funkcje składowe klasy Umiejscowienie definicji funkcji składowej Funkcje inline Konstruktory klas Czym jest konstruktor Konstruktor domyślny Przypisywanie domyślnych wartośc i parametrom umieszczonym w klasach Używanie listy inicjalizacyjnej w konstruktorze Prywatne składowe klasy Uzyskiwanie dostępu do prywatnych zmiennych składowych klasy Przyjaciele klasy Domyślny konstruktor kopiujący Wskaźnik th is Stałe obiekty klasy Stałe funkcje składowe klasy Definiowan ie funkcji składowej poza klasą Tablice obiektów klasy Składowe statyczne klasy Statyczne zmienne składowe klasy Statyczne funkcje składowe klasy Wskaźniki i referencje do obiektów klasy Wskaźn ik i do obiektów Referencje do obiektów Programowanie w C++/CLI Definiowanie typów klas wartości Definiowanie typów referencyjnych Właściwości klasy , Pola initonly Konstruktor statyczny Podsumowanie Ćwiczenia
9 359
360
360
360
361
361
365
366
367
369
372
373
373
374
376
378
379
380
380
382
385
387
387
390
391
394
395
398
399
.400
401
402
403
405
406
406
409
411
:
412
417
420
433
434
435
436
10
Visual C++ 2005. Od podslaw Rozdziala. Więcej na lemat klas Destruk tory klas Czym jest destruktor Destruktor domyślny Destruktory i dynamiczne przydzielani e pam ięci Implementacja konstruktora kopiującego Dzielenie pamięci pomiędzy zmiennymi Definiowanie unii Unie anonimowe Unie w klasach i strukturach Przeładowywanie operatorów Implementacja przeładowanego operatora Implementacja pełnej obsługi operatora Przeładowywanie operatora przypisania Przeładowywanie operatora dodawania Przeładowywanie operatorów inkrement acj i i dekrementacji Szablony klas Definiowanie szablonu klasy Tworzenie obiektów klasy szablonu Szablony klas z wieloma parametrami Używanie klas Interfejs klasy Definiowan ie problemu Implementacja klasy Definiowanie klasy CBox Zastosowanie klasy CBox Organizowanie kodu programu Nazewnictwo plików programu Programowanie w C++jCLI Przeładowywanie operatorów w klasach wartości Przeładowywanie operatorów inkrement acj i i dekrementacji Przeładowywanie operatorów w klasach referencyjnych Podsumowanie Ćwiczenia
Rozdzial9. Dziedziczenie i funkcje wirlualne Podstawy programowan ia zorientowanego obiektowo (OOP) Dziedziczenie w klasach Czym jest klasa bazowa Tworzenie klas pochodnych Kontrola dostępu do dziedziczonych składowych Działanie konstruktora w klasie pochodnej Deklarowan ie chronionych składowych klasy Poziom dostępu do dziedziczonych składowych klasy Konstruktor kopiujący w klasie pochodnej Składowe klasy jako przyjaciele Klasy zaprzyjaźnione Ograniczenia klas zaprzyjaźnionych Funkcje wirtualne Czym jest funkcja wirtualna Używanie wskaźników do obiektów klas Używanie referencji z funkcjami wirtualnymi
439
439
440
440
442
445
448
448
450
450
450
451
454
458
464
468
468
469
472
475
477
477
477
478
486
497
500
500
502
503
508
509
511
512
515
515
517
517
518
521
524
528
531
532
537
538
538
539
541
544
545
Spis treści Funkcje czysto wirtualne Klasy abstrakcyjne Pośrednie klasy bazowe Wirtualne destruktory Rzutowanie pomiędzy typam i klasowym i Klasy zagnieżdżone Programowanie w C++ /CLI Dziedziczenie w C++/CLI Klasy interfejsowe Definiowanie klas interfejsowych Klasy i asemblacje Definiowanie nowych funkcji Delegaty i zdarzenia Finalizatory i destruktory w klasach referencyjnych Klasy generyczne Podsumowanie Ćwiczenia
_
547
548
551
553
559
559
563
563
569
570
574
579
579
592
594
605
607
,
:
Rozdzial10. Debugowanie
611
Co znaczy debugowanie Błędyoprogramowania
Najczęściej spotykane błędy Podstawowe operacje debugowania Ustawianie punktów wstrzymania Ustawianie punktów śledzenia Rozpoczynanie debugowania Zmien ianie wartości zmiennej Dodawanie kodu debugującego Asercje Dodawanie własnego kodu debugowanła Debugowanie programu Stos wywołań Szukanie błędu krok po kroku Testowanie rozszerzonej klasy Odnajdywanie następnego błędu Debugowanie pamięci dynamicznej Funkcje sprawdzające obszar wolnej pamięci Sterowanie operacjami debugowanla obszaru wolnej Dane wyjściowe debuggera obszaru wolnej pamięci Debugowanie programów w C++/CLI Używanie klas Debug i Trace Podsumowanie
_
pam i ęci
Rozdzial11. Założenia programowania dla systemu Windows Podstawy programowania dla systemu Windows Elementy okna Programy dla Windowsa i system operacyjny Programy st erowane zdarzeniami Komunikaty Windowsa Windows API Typy danych w systemie Windows Notacj a w programach dla systemu Windows
11
611
613
614
615
617
619
620
625
625
626
627
633
633
635
638
641
641
642
643
644
650
650
659
661
:
662
663
664
665
665
666
666
667
12
Visual C++ 2005. Od podslaw Struktura programu dla systemu Windows Funkcja WinMalnt) Funkcje przetwarzania komunikatów Prosty program dla systemu Windows Organizacja programu dla systemu Windows Microsoft Foundation Classes Notacja MFC Jak jest ustrukturyzowany program MFC Korzystani e z formularzy systemu Windows Podsumowanie
668
669
681
686
686
689
689
690
693
695
Rozdzial12. PrOgramowanie dla systemu Windows
zwykorzystaniem Microsoft Foundation C1asses
699
Architektura dokument-widok w MFC Czym jest dokument Interfejsy dokumentu Czym jest wido k lączenie dokumentu i jego widoków Aplikacja a MFC Tworzenie aplikacji MFC Tworzenie apl ikacji SDI Wynik działania MFC Application Wizard Tworzenie aplikacji MDI Podsumowanie Ćwiczenia
700
700
700
70'1
702
702
705
707
710
721
724
724
Rozdzial13. Praca zmenu i paskami narzędzi Komun ikacja z systemem Windows Zrozumieć mapy komunikatów Kategorie komunikatów Obsługa komun ikatów w programie Rozwijanie programu Sketcher Elementy menu Tworzenie i edycja zasobów menu Dodawanie procedur obsługi dla komunikatów menu Wybieranie klasy obsługującej komunikaty menu Tworzenie funkcj i komunikatu menu Tworzenie kodu dla funkcji komunikatów menu Dodawanie procedur obsługi komunikatów uaktualniających interfejs Dodawanie przycisków paska narzędzi Edycja właściwośc i przycisku paska narzędzi Testowanie przycisków narzędzi Dodawanie wskazówek Podsumowanie Ćwiczenia
Rozdzial14. Rysowanie woknie Podstawy rysowania w oknie Obszar klienta okna Graphical Device Interface
727
użytkownika
727
728
731
732
733
734
734
739
740
741
743
747
752
753
754
755
756
757
759
759
760
761
SpiS treści Mechanizm rysowania w Visual C++ Klasa widoku w aplikacji Klasa CDC Rysowanie grafiki w praktyce Programowanie myszy Komunikaty z myszy Procedury obsługi komunikatów myszy Rysowanie za pomocą myszy Testowanie szkicownika Uruchamianie przykładu Przechwytywanie komunikatów myszy Podsumowanie Ćwiczenia Rozdział 15. Tworzenie dokumentu ipoprawianie
:
widoku
Czym są klasy kolekcji Typy kolekcji Klasy kolekcji z kontrolą typów Kolekcje obiektów Kolekcje wska źników z kontrolą typów Korzystanie z szablonu klasy CList Rysowanie krzywej Definiowanie klasy CCurve Implementacja klasy CCurve Sprawdzanie klasy CCurve Tworzenie dokumentu Używanie wzorca CTypedPtrList Poprawianie widoku Uaktualnianie wielokrotnych widoków Przewijanie widoków Korzystanie z trybu mapowania MM_LOENGLlSH Usuwanie i przesuwanie kształtów Implementacja menu kontekstowego Łączenie menu z klasą Wybieranie menu kontekstowego Podświetlanie elementów Obsługa komunikatów menu Rozwiązywanie problemu nakładających się elementów Podsumowanie Ćwiczenia Rozdział 16. Praca z oknami
dialogowymi ikontrolkami
Poznaj okna dialogowe Poznaj kontrolki Wspólne kontrolki Tworzenie zasobu okna dialogowego Dodawanie kontrolek do okna dialogowego Programowanie okna dialogowego Dodawanie klasy dialogu Modalne i niemodalne okna dialogowe Wyświetlanie okna dialogowego
13 763
763
765
774
776
777
779
781
805
806
807
808
809
811
811
812
813
813
823
825
826
827
829
830
831
831
837
837
840
844
846
847
848
850
855
860
867
869
870
871
871
872
874
874
875
877
877
878
878
14
Visual C++ 2005. Od podstaw Obs ł u ga
kont rolek okna dialogowego Inicjalizowanie kontrolek O b s ł u ga komunikatów przycisku opcj i Ko ń czenie operacj i okna dialogowego Dodawanie szerokośc i pióra do dokument u Dodawanie szerok ości pióra do elementów Tworzenie elementów w widoku Testowanie okna dialogowego U żywani e p okrętł a
Dodawanie element u menu Scale oraz przycisku paska Tworzenie po k rętła Generowanie klasy okna dialogowego Scale Wyśw i e tl an i e po k rętła
Korzystanie ze w spółc zynni ka skali Skalowalne tryby mapowania Ustawianie rozmiaru dokumentu Ust awianie t rybu mapowania Impleme ntowanie przewijania ze skalowaniem Praca z paskami stanu Dodawanie paska stanu do ramki U żyw ani e pól list Usuwanie okna dialogowego Scale Tworzenie kontro lki pola list Korzystan ie z kont rolki pola tekstowego Tworzenie zasobu pola tekst owego Tworzenie klasy okna dialogowego Dodawanie element u menu Text Definiowanie elementu Text Impleme ntacja klasy CText Tworzenie element u Text Podsumowanie Ćwiczeni a
Rozdział 17. Przechowywanie i drukowanie dokumenlów Poznaj seri al i z acj ę Serializowanie dokumentu Serializacja w definicji klasy dokumentu Serializacj a w implement acji klasy dokumentu Zestaw fun kcj i klas opartych na CObject Jak d zi ał a serializacj a Jak za im p l em entowa ć se r ia l izację klasy Stosowanie seri alizacji Rejest rowanie zmian dokumentu Serializowanie dokumentu Serializowanie klas elementów Testowani e seriali zacji Przenoszenie tekst u Drukowanie dokument u Proces drukowania Implement acja wielostron icowych wydruków Uzyskiwanie c ał kowitego rozmiaru dokum entu Przechowywanie danych drukowania
Spis treści Przygotowania do wydruku Porządkowanie po drukowaniu Przygotowywanie kont ekstu urządzenia Drukowanie dokumentu Drukowanie dokum entu Podsumowanie
945 947 947 948 952 95 3 954
Ćwiczenia
Rozdzial18. Tworzenie własnych plików DLL
955
Poznaj DLL Jak działają DLL Zawartość DLL Odmiany DLL Co umieścić w DLL Pisanie DLL Pisanie i używanie rozszerzającej DLL Eksportowan ie zmiennych i funkcj i z DLL Importowanie symbol i do programu Implementowanie eksportowania symboli z DLL Podsumowani e
955 957 960 961 962 963 963 970 971 972 974 975
Ćwiczenia
Rozdział 19. lączenie się
ze źródłami danych
Podstawy baz danych Nieco o języku SQL Pobieranie danych z użyciem języka SQL Łączenie tab el w języku SQL Sortowanie rekordów : Obsługa baz danych w MFC Klasy MFC obsługujące ODBC Tworzenie aplikacji bazodanowej Rejestrowanie bazy danych ODBC Generowan ie programu MFC ODBC Poznaj strukturę programu Testowanie przykładu Sortowanie zestawu rekordów Zmienianie podpisu okna Używanie drugiego obiektu zestawu rekordów Dodawanie klasy zestawu rekordów Dodawanie klasy widoku dla zestawu rekordów Dostosowywan ie zestawu rekordów Dostęp do wielu widoków tablic Przeglądanie zamówień na produkt Przeglądanie informacji o kliencie Dodawanie zestawu rekordów dla informacji o kliencie Tworzenie zasobu okna dialogowego z informacjami o kliencie Tworzenie klasy widoku dla informacji o kliencie Dodawanie filtra Implementacja parametru filtra Łączenie okna dialogowego Order z oknem dialogowym Customer Testowanie przeglądarki bazy danych Podsumowanie Ćwiczenia
VislJal C++ 2005. Od podslaw Rozdział20.lklualizacja źródeł danych
1033
Operacje aktual izacji Operacje aktualizacji CRecordSet Transakcje Prosty przykład uaktualnienia Dostosowywanie aplikacji Zarządzanie procesem aktualizacji Implementacja trybu uaktualniania Dodawanie wierszy do tabeli Proces wpisywania zamówienia Tworzenie zasobów Tworzenie zest awów rekordów Tworzenie widoków zestawu rekordów Dodawanie kontrolek do zasobów dialogu Implementacja przełączania okien dialogowych Tworzenie identyf ikatora zamówienia Przechowywanie danych zamówienia Wybieranie produktów dla zamówienia Dodawanie nowego zamówienia Podsumowanie Ćwiczenia
1033
1034
1036
1038
1040
10 42
1044
1052
1053
1054
1055
1055
1060
1064
1068
1073
1075
1077
1082
1082
Rozdział21.lplikacjewYkorzystująceWindows Forms
1083
Poznaj formularze systemu Windows Poznaj aplikacje Windows Forms Zmienianie właściwości formularza Jak startuje aplikacja Dostosowywanie GUl aplikacji Dodawanie kontrolek do formularza Dodawanie zakładek Korzystanie z kontrolki GroupBox Używanie kontrole k Button Korzystanie z kontrolki WebBrowser Sposób działania aplikacji Winning Application Dodawanie menu kontekstowego Tworzenie procedur obsługi zdarzeń , Obsługa zdarzeń dla menu l.lrnits Tworzenie okna dialogowego Używanie okna dialogowego Dodawanie drugiego okna dialogowego Implementacja elementu menu Help/About Obsługa kliknięcia przycisku Reagowanie na menu kontekstowe Podsumowanie
1083
1084
1086
1087
1088
1089
1092
1094
1097
1099
1100
1102
1102
1108
1109
1115
1120
1128
1128
1131
1138
1139
Ćwiczenia
Rozdział 22. Dostęp
do źródeł danych waplikacjach Windows Forms
Praca ze źródłam i danych Dostęp do danych i ich wyświetlanie Używanie kontrolki DataGridView Używanie kontrol ki DataGridView w trybie
niezwiązanym
1141
1142
1143
1143
1145
Spis Ireści Dostosowywan ie kontrolki DataGridView Dostosowywan ie komórek nagłówkowych Dostosowywanie pozostałych komórek Dynamiczne ustawianie stylów komórki Używanie trybu związanego Komponent BindingSource Korzystanie z kontro lki BindingNavigator W ią zan ie z pojedynczymi kontrolkami Praca z wielom a tabelami Podsumowani e Ćwiczenia
17 1151
1152
1153
1160
1165
1166
1171
1174
1178
1179
1180
Dodalek A Slowa kluczowe w Języku C++
1181
Dodatek B Kody ASCii
1183
Skorowidz
1189
18
Visual C++ 2005. Od podstaw
oautorze Ivor Horton z wyk ształcenia jest matematykiem, a do informatyki zwabiła go obietnica zarobków przy niewielkim nakładzie pracy. Mimo że rzeczywistość okazała się całkiem inna - dużo pracy i raczej średnie zarobki - komputerami Ivor zajmuje się do dziś. Pracował już jako programista, projektant systemów, konsultant oraz kierownik wdrażania projektów o dużym stopniu złożoności. dużych
Horton ma wieloletnie doświadczenie w tworzeniu i wdrażaniu systemów komputerowych zastosowanie w projektowaniu inżynierskim oraz w produkcji w różnych sekto rach gospodarki. Posiad a także duże doświadczenie w tworzeniu czasami przydatnych pro gramów w różnych językach programowania oraz nauczaniu, przede wszystkim naukow ców i inżynierów, robienia tego samego. Książki na temat programowania pisze już od ponad dziesięciu lat. Pośród jego naj nowszych publikacji znajdują się pozycje dotyczące języków C, C++ oraz Java. Obecnie, kiedy Ivor nie pisze książek o programowaniu i nie zajmuje się doradzaniem , jego głównymi zajęciami są łowienie ryb, podróżowanie oraz szlifowanie francuskiego . mających
20
Visual C++ 2005. Od podstaw
Wstęp
Witaj drogi Czytelniku. Dzięki temu egzemplarzowi możesz stać się efektywnym progra mistą C++. Najnowszy system firmy Microsoft Visual Studio 2005 pozwala na tworzenie programów w dwóch różnych, ale blisko spokrewnionych wersjach języka C++. Obsługuje on zarówno oryginalną, standardową wersję C++ ISO/ANSI, jak również jego nowszą wersję znaną pod nazwą C++/CLI, która została stworzona przez Microsoft i jest dzisiaj standardem ECMA. Obie te wersje uzupełniają się i pełnią różne role. CH ISO/ANSI służy do tworzenia wysoko wydajnych programów, które można uruchamiać natywnie na komputerze. Natomiast CH/CLI został przygotowany specjalnie z myślą o platformie .NET. Z książki tej nauczysz się programować w obu wersjach C++. Przy pisaniu programu w ISO/ANSI CH znaczna część kodu generowana jest automatycznie. Mimo tego ułatwienia istnieje także konieczność samodzielnego wpisywania jego dużych partii. Do tego celu trzeba dobrze rozumieć ideę programowania zorientowanego obiektowo, a także dysponować znaczną wiedzą na temat specyfiki programowania dla systemu Windows . Mimo że C++/CLI stworzony został dla platformy .NET , jest on także narzędziem wspoma gającym tworzenie aplikacji za pomocą biblioteki Windows Forms, przy użyciu której można tworzyć programy, pisząc niewielkie ilości kodu, a czasami nawet bez takiej potrzeby. Oczywiście, gdy zachodzi potrzeba dodania kodu do aplikacji Windows Forms, nawet nie wielkiej jego ilości, trzeba posiadać dogłębną wiedzę na temat języka CH/CLI. Język
C++ ISO/ANSI jest nadal wybierany przez wielu profesjonalistów, ale szybkość two rzenia programów, jaką oferuje język C++/CLI w połączeniu z biblioteką Windows Forms, powoduje, że ma on także duże znaczenie. Z tego też powodu zdecydowałem się przedstawić w tej książce oba rodzaje języka C++ .
Dla kogo iest ta książka Celem tej książki jest nauka pisania w języku C++ programów przeznaczonych dla systemu operacyjnego Microsoft Windows przy użyciu programu Visual C++ 2005 lub jednej z edycji środowiska Visual Studio 2005. Książka ta nie wymaga żadnego wcześniejszego doświad czenia w programowaniu w jakimkolwiek innym języku programowania. Książka ta jest dla Ciebie, jeśli: • Posiadasz niewielkie doświadczenie w programowaniu w innych językach, takich jak BASIC czy Pascal , i chcesz nauczyć się C++ oraz rozwinąć praktyczne zdolności programowania dla systemu Microsoft Windows.
22
Visual C++ 2005. Od podstaw • Masz pewne
doświadczenie w
środowisku niż
w
środowisku
programowaniu w C lub C++, ale w innym Windows, i chcesz poszerzyć swoje umiejętności o programowanie Windows przy użyciu naj nowszych narzędzi i technologii.
• Dopiero zaczynasz programować i masz wystarczająco dużo chęci, aby zgłębiać tajniki programowania w języku C++. Aby nauka była owocna, musisz przynajmniej znać podstawy działania komputera - jak wygląda organizacja pamięci oraz w jaki sposób przechowywane są dane i instrukcje.
oCZym jest ta książka Pisałem tę książkę z myślą nauczenia Czytelnika podstaw programowania w języku C++ przy użyciu obu technologii obsługiwanych przez Visual C++ 2005. Niniejszy egzemplarz stanowi szczegółowy przewodnik po obu typach języka C++. Zatem opisane tutaj zostało tworzenie programów dla systemu Windows w natywnym języku C++ ISO/ANSI przy użyciu biblioteki Microsoft Foundation Classes (MFC) oraz w języku C++/CLI przy użyciu biblioteki Windows Forms. Ze względu na wszechobecność technik bazodanowych w dzisiej szych czasach, książka ta zawiera również wprowadzenie do technik, za pomocą których można uzyskać dostęp do zasobów danych zarówno z poziomu programów utworzonych w technologii MFC, jak i Windows Forms. Programy MFC wymagają pisania większych ilości kodu w porównaniu z aplikacjami Windows Forms. Spowodowane jest to tym, że Win dows Forms pozwala na korzystanie z wysoko rozwiniętych narzędzi Visual C++, umoż liwiających utworzenie całości graficznego interfejsu użytkownika (ang. Graphical User Interface - GUl) w trybie graficznym przy automatycznym wygenerowaniu kodu. Z tego też względu większa część tej książki poświęcona została programowaniu w technologii MFC, a nie Windows Forms.
Organizacja książki •
Rozdział
1. stanowi wprowadzenie do podstawowych zagadnień, które należy móc tworzyć zarówno natywne, jak i korzystające z platformy .NET programy w języku C++. Dodatkowo wprowadzam także podstawy posługiwania się środowiskiem programistycznym Visual C++ 2005. Podstawy te pozwolą na wykorzystanie możliwości Visual C++ 2005 do tworzenia różnego typu programów w języku C++, o których będzie mowa w dalszych rozdziałach książki. zrozumieć, aby
•
Rozdziały
2. - 10. poświęcone zostały nauce obu wersj i języka C++ oraz podstawowych zagadnień i technik znajdowania błędów. Każdy z tych rozdziałów został napisany według tego samego wzoru. Pierwsza połowa poświęcona jest tematom związanym z C++ ISO/ANSI, a druga traktuje o C++/CLI.
•
Rozdział
11. stanowi opis struktury aplikacji systemu Microsoft Windows i opisuje oraz pokazuje najważniejsze komponenty, które posiada każdy taki program. W rozdziale tym znajdują się wyjaśnienia prostych przykładowych programów
Wstęp
23
korzystających z C++ ISO/ANSI oraz API Windows, jak również przykład prostego programu utworzonego w technologii C++ /CLI przy użyciu biblioteki Windows Forms.
•
Rozdziały 12. - 17. szczegółowo opisują możliwości , jakie daje MFC przy budowie GUl. Nauczysz się tworzyć i używać najczęściej spotykane kontolki do budowy graficznego interfejsu użytkownika swojego programu, a także obsługiwać zdarzenia będące rezultatem interakcji użytkownika z programem. W ten sposób utworzymy w pełni działającą aplikację. Dodatkowo na przykładzie tego programu nauczysz się drukować i zapisywać na dysku dokumenty przy użyciu biblioteki MFC.
•
Rozdział
18. zawiera niezbędne informacje potrzebne do tworzenia własnych bibliotek przy użyciu MFC. Dowiesz się, jakiego rodzaju biblioteki możesz tworzyć, oraz utworzysz przykładowe biblioteki, które będę współpracowały z programem napisanym w poprzednich sześciu rozdziałach.
•
Rozdziały 19. i 20. poświęcone zostały uzyskiwaniu dostępu do zasobów danych z poziomu aplikacji MFC. Nauczysz się uzyskiwać dostęp do bazy danych w trybie tylko do odczytu, a następnie poznasz podstawowe techniki programistyczne pozwalające uaktualniać zawartość bazy danych przy użyciu biblioteki MFC. W przykładach wykorzystano bazę danych Nortwind, którą można pobrać z sieci . Opisane techniki można również zastosować do własnego źródła danych .
• W rozdziale 21. za pomocą Windows Forms oraz C++/CLI piszemy przykładowy program, dzięki któremu nauczymy się tworzyć kontrolki Windows Forms, dopasowywać je do własnych potrzeb i ich używać. Program ten jest rozbudowywany w trakcie rozdziału, pozwalając Czytelnikowi na praktyczne zdobywanie doświadczenia. •
Rozdział 22. bazuje na wiedzy zdobytej w poprzednim rozdziale. Poświęcony został on kontrolkom służącym do uzyskiwania dostępu do źródeł danych oraz sposobom dopasowywania ich do własnych potrzeb. Nauczysz się także tworzyć programy z dostępem do bazy danych bez wpisywania kodu źródłowego.
Wszystkie techniki programistyczne opisane w poszczególnych rozdziałach zostały zilustro wane konkretnymi przykładami. Na końcu każdego rozdziału znajduje się podsumowanie najważniejszych zagadnień w nim poruszonych. Większość rozdziałów została dodatkowo uzupełniona o ćwiczenia pozwalające zastosować zdobytą wiedzę w praktyce. Rozwiązania do tych ćwiczeń można pobrać ze strony wydawcy. Część książki poświęcona
samemu językowi C++ bazuje na przykładowych programach trybie konsoli, z którymi komunikacja odbywa się z poziomu wiersza poleceń. Dz ięki temu można skupi ć s i ę na samych możliwościach oferowanych przez język C++, bez wprowadzania niepotrzebnego zamętu związanego z zawiłościami programowania GUl Windows. Programowanie dla systemu Windows jest możliwe dopiero wtedy, gdy zdobędzie się gruntowną wiedzę na temat języka programowania C++. działających w
sobie prostotę mogą rozpocząć naukę od samego C++ ISO/ANSI. Każdy z roz 2. - 10. najpierw opisuje określone możliwości języka C++ ISO/ANSI, a następnie wprowadza nowe właściwości dostępne w C++ /CLI dla tego samego kontekstu . Powodem takiej organizacji książki jest fakt , że C++/CLI został określony jako rozszerzenie C++
Osoby
ceniące
działów
24
Visual C++ 2005. Od podstaw ISO/ANSI. W związku z tym, aby zrozumieć C++/CLI , należy wpierw nauczyć się C++ ISO/ ANSI. Można zatem skupić s i ę jedynie na tematach opisujących s ta n dardow ą wersję języka C++ w rozdziałach od 2. do 20. i pominąć części dotyczące C++/CLI. Następnie można przejść do tworzenia programów dla systemu Windows w języku C++ ISO/ANS] bez konieczności pamiętania o drugiej wersji tego języka . Do C++/CLI można powrócić, gdy poczujemy się pewnie w standardowej wersji. Można oczywiście rozpocząć naukę obu wersji języka od samego początku , stopniowo zwiększając swoją wiedzę na ich temat.
Czego będZiesz
potrzebować
do tei książki
Aby móc w pełni korzystać z tej książki, potrzebujesz jednego z trzech programów: Visual Studio 2005 Standard Edition, Visual Studio 2005 Professional Edition lub Visual Studio 2005 Team System . Visual C++ Express 2005 nie przyda s i ę nam, ponieważ nie zawiera biblioteki MFC. Aby móc zainstalować oprogramowanie Visual Studio 2005, należy posiadać system Windows XP z zainstalowanym zestawem poprawek Service Pack 2 lub Windows 2000 z zainstalowanym Service Pack 4. Wymagania sprzętowe Visual Studio to co najmniej procesor z zegarem l GHz, 256 MB pamięci RAM oraz nie mniej niż 1 GB wolnej prze strzeni na dysku systemowym oraz 2 GB na dysku, na którym zostanie zainstalowany pro gram. Do zainstalowania pełnej dokumentacj i MSDN dostępnej razem z oprogramowaniem potrzebne będzie dodatkowe 1,8 GB na dysku instalacyjnym. Przykłady z bazą danych korzystają z bazy Northwind Traders. Można ją znaleźć, s zu k aj ąc frazy Northwind Traders na stronie http://msdn.microsoft.com, z której można ją pobrać . Można oczywiście skorzystać ze wszystkich opisywanych przykładów w pracy z dowolną inną bazą danych.
Aby odnie ść największe korzyści z czytania tej książki, należy mieć zapał do nauki oraz z uporem dążyć do opanowania naj potężniejszego dostępnego obecnie narzędzia programi stycznego dla systemu Windows. Musisz się poświęcić i wpisać wszystkie przykłady kodu oraz je przeanalizować, a także spróbować rozwiązać zadane ćwiczenia. Wszystko to brzmi o wiele gorzej, niż prezentuje się w rzeczywistości . Nie oprzesz się uczuciu zaskoczenia, jak wiele udało Ci się osiągnąć w krótkim czasie . Należy także pamiętać, że każdy, kto uczy się programować, od czasu do czasu przechodzi trudne chwile. Ale jeśli się nie poddasz, z czasem wszystko stanie się jasne. Książka ta pomoże Ci rozpocząć eksperymentowanie na własną rękę, a co za tym idzie - odnieść sukce s jako programista C++ .
Konwencie Aby umożliwi ć Czytelnikowi odniesienie jak największych korzyści z czytania tej i ułatwić zorientowanie się w temacie, w książce tej przyjęto kilka konwencji.
książki
Wstęp
Jako Spróbuj sam oznaczamy
ćwiczenia,
które powinny
zostać
25
wykonane na podstawie tekstu
książki.
Jak to działa Po kilku Spróbuj sam
następuje
Jak to działa, czyli
W ramkach takich jak ta przechowywane średnio do otaczającego je tekstu.
Wskazówki, porady, sztuczki i uwagi Style w
są
szczegółowe objaśnienie
są ważne
informacje
wpisanego kodu.
odnoszące się
lekko przesunięte i napisane
bezpo
taką czcionką.
tekście:
• Nowe terminy i ważne słowa pisane ich pierwszego pojawienia się. • Kombinacje klawiszy prezentowane
są pismem
pochylonym w momencie
są następująco:
• Nazwy plików, adresy URL oraz kod w
obrębie
Ctrl+A.
tekstu oznaczone są następująco:
persistence.properties. • Kod prezentowany jest na dwa
różne
sposoby:
Nowe i ważne fragmenty kodu w przykładach znajdują się w ramkach Ramka używana jest do oznaczania kodu mniej ważnego w określonym kontekście lub kodu. który był już wcześniej prezentowany. Pracując
z
przykładami
w
książce,
kod
można wpisywać ręcznie
lub
skorzystać
z zasobów
dostępnych dla tej książki. Wszystkie przykłady kodu z tej książki można pobrać ze strony
http://helion.pl/ksiazki/vcppo.htm.
•
26
Visual C++ 2005. Od podstaw
1
Programowanie
przy użyciu lisual C++ 2005
Programowanie dla systemu Window s nie jest trudne , a Microsoft Yisu al C++ 2005 sprawi a, staje się ono wręcz banalne, o czym przekonamy s i ę w trakcie czytania tej książk i . Jest tylko jeden problem: zanim zagłęb imy się w zawiło ści programowania dla Windowsa, najpierw musimy dokładnie zapozna ć si ę z możliwo ściam i oferowanymi przez język C++, a w szcze gólności z technikami programowania zorientowanego obiektowo w tym języku. Techniki te s ta now i ą podstawę efektywn ości wszy stkich narzędzi dostarczanych przez Yisual C++ 2005 dla programowania w systemie Windows. W zwi ązku z tym bardzo ważne jest ich dobr e zro zumienie. W rozdziale tym dowiemy się: że
są najw ażniejsze składniki środowiska
•
Jakie
•
Z czego
•
Czym
są rozwiązania
•
Czym
są
•
Jak
utworzyć
•
Jak
sk omp i l ow ać , sk o nso l i d ować
skład a się
programy
Yisual C++ 2005.
platforma .NET oraz jakie oferuje i proj ekty oraz j ak
działające
s ię je
korzyści .
tworzy.
w trybie konsoli.
i ed ytować program. i uruchomi ć program konsolowy w C++.
A zatem nadszedł czas, by włączyć komputer, wisko Yisu al C++ oraz rozpocząć przygodę.
uruchomić
system Windows i potężne
ś ro d o
Środowisko programistyczne .NET Środowisko programistyczne .NET stanowi cen t ra l n ą koncepcję Yisual C++ 2005, jak również wszystkich innych produktów firmy Microsoft wykorzystujących tę platformę. Środowisko .NET składa się z dwóch komponentów : Common Language Runtime (CLR) ,
28
Visual C++ 2005. Od podstaw w którym wykonywane są programy, oraz zbioru bibliotek, zwanych bibliotekami klas środo wiska .NET. Biblioteki klas platformy .NET do starczają funkcji potrzebnych do wykonania kodu w CLR bez względu na użyty język programowania. Oznacza to, że programy .NET napisane w C++, C# lub jakimkolwiek innym języku obsługującym platformę .NET korzy stają z tych samych bibliotek .NET. Za pomocą pakietu Visual C++ 2005 można tworzyć dwa podstawowe typy programów w C++. Istnieje możliwość napisania programu , który jest wykonywany natywnie, na tym samym komputerze - tego typu programy nazywamy programami natywnymi C++ i two rzymy je w C++ ISO/ANSI. Drugi typ to programy działające pod kontrolą CLR , które zostały napisane w rozszerzonej wersji C++, czyli C++/CLI. Programy te nazywamy programami CLR lub programami C++/CLI. Platforma .NET nie jest częścią Visual C++ 2005, ale raczej składnikiem systemu operacyj nego Windows, który ułatwia tworzenie oprogramowania oraz usług sieciowych. Platforma ta zapewnia większą niezawodność kodu i jego bezpieczeństwo,a także pozwala na integrację kodu C++ z kodem napisanym w ponad 20 innych językach programowania, które z nią współpracują. Jedną z wad programowania dla platformy .NET jest niewielka strata wydaj ności , ale w większości przypadków jest ona całkowicie niezauważalna.
Common Language Runtime (CLRJ CLR jest standardowym środowiskiem do wykonywania programów napisanych w wielu różnych językach wysokiego poziomu, takich jak Visual Basic, C# czy właśnie C++. Spe cyfikacja CLR w chwili obecnej zawiera się w standardzie CLI (ang. Common Language Infrastructurei, europejskiego stowarzyszenia producentów komputerów ECMA (ang . Euro pean Computer Manufacturersi - ECMA-335 , a także w równorzędnym standardzie ISO ISO/lEC 23271 , a więc CLR jest implementacją tego standardu. Można łatwo odgadnąć , dlaczego język C++ dla CLR nazywany jest C++/CLI - jest to C++ dla Common Language Infrastructure (wspólna infrastruktura dla języków). Dzięki temu kompilatory C++/CLI można spotkać także w innych systemach operacyjnych, które posiadają implementację CLI.
Wszelkie informacje o standardach ECMA dostępne są pod adresem: http ://www. ecma -international.org. Z tej strony można nieodpłatniepobrać standard ECMA-335. CLI jest w rzeczywistości specyfikacją maszyny wirtualnej, która umożliwia uruchamianie programów napisanych w różnych językach programowania wysokiego poziomu w różnych systemach operacyjnych bez zmiany lub ponownej kompilacji kodu źródłowego. CLI defi niuje standardowy język pośredni dla maszyny wirtualnej , do którego kompilowany jest kod napisany w jednym z języków programowania wysokiego poziomu. Na platformie .NET język ten nazywany jest Microsoft Intermediate Language (MSIL). Kod pośredni jest ostatecznie mapowany na kod maszynowy "w locie" przez kompilator typu JIT podczas wykonywania programu. Kod pośredni CLI można oczywiście uruchomić w dowolnym śro dowisku posiadającym implementację CLI .
Rozdzial1. • Programowanie przy użyciu Visual C++ 2005
29
CLI definiuje także wspólny zbiór typów danych, zwany Common Type System (CTS), którego należy używać przy pisaniu programów w językach mających na celu implementację CLI. CTS określa sposób używania typów danych w CLR i zawiera zestaw predefiniowanych typów. Można także definiować własne typy danych, ale należy trzymać się określonych reguł , aby zachować zgodność z CLR (o tym za chwilę). Standardowy system reprezentacji typów danych pozwala na jednolitą obsługę danych z poziomu komponentów napisanych w różnych językach programowania, a także na ich integrację w obrębie jednej aplikacji . CLR znacznie zwiększa bezpieczeństwo danych i niezawodność kodu, częściowo ze względu na fakt, że dynamiczne przydzielanie i zwalnianie pamięci odbywa się w pełni automatycznie, a częściowo ponieważ kod MSIL jest dokładnie sprawdzany i poddawany walidacji przed wykonaniem programu. CLR jest tylko jedną implementacją specyfikacji CLI , która jest wykonywana w systemie Microsoft Windows na komputerach osobistych. Bez wątpienia implementacje CLI dla innych systemów operacyjnych i platform sprzętowych także będą się pojawiać. Terminy CLI i CLR mogą być czasami stosowane zamiennie, ale należy pamiętać , że nie oznaczają one dokładnie tego samego. CLI jest specyfikacją standardu, CLR zaś stwo rzonąprzez firmę Microsoft implementacją CLI.
Pisanie programów wC++ Visual C++ 2005 umożliwia tworzenie wszelkiego rodzaju programów i ich składników . Jak już wspominałem wcześniej, w systemie Windows mamy do wyboru dwa typy aplika cji: programy wykonywane za pomocą CLR oraz programy kompilowane bezpośrednio do kodu maszynowego i wykonywane natywnie na komputerze . Tworząc aplikacje oparte na oknach dla CLR, jako podstawę GUl wykorzystuje się Windows Forms, które dostarczane są w bibliotekach platformy .NET. Korzystanie z Windows Forms znacznie przyspiesza tworze nie graficznego interfejsu użytkownika, gdyż tworzy się go w trybie graficznym ze standar dowych komponentów, a kod generowany jest automatycznie. Programiście pozostaje już tylko dopasowanie tak powstałego kodu do własnych potrzeb w celu uzyskania wymaganej funkcjonalności. Tworząc
kod wykonywany natywnie, do wyboru mamy kilka opcji . Jedną z nich jest użycie biblioteki Microsoft Foundation Classes (MFC) służącej do zaprogramowania interfejsu użytkownika aplikacji Windows. Biblioteka MFC zawiera w sobie API systemu operacyjnego Windows do tworzenia i kontrolowania GUl, a także znacznie ułatwia proces rozwoju pro gramu. API Windows powstało dużo wcześniej niż język C++, a więc nie zawiera żadnych cech właściwych technice programowania zorientowanego obiektowo, a byłoby tak, gdyby zostało napisane dzisiaj. Oczywiście, nie ma obowiązku używania MFC. Jeśli chcemy zyskać na wydajności , możemy napisać kod C++ z bezpośrednim dostępem do API Windows. Kod C++ wykonywany w CLR opisywany jest jako CH zarządzany (ang . managed C++), dane i kod są zarządzane przez CLR. W programach CLR zwalnianie pamięci przydzielonej dynamicznie odbywa się automatycznie. W ten sposób eliminuje się ryzyko wystąpienia błędów typowych dla natywnych aplikacji CH. Kod CH, który wykonywany jest poza CLR, zwany jest czasami przez Microsoft CH niezarządzanym (ang. unmanaged C++), ponieważ CLR nie bierze udziału w jego wykonywaniu. Korzystając z niezarządzanego ponieważ
30
Visual C++ 2005. Od podslaw C++, trzeba samodzielnie przydzielać i zwalniać pamięć podczas wykonywania programu . Należy się także liczyć z obniżeniem poziomu bezpieczeństwa, które daje CLR . Niezarządzany C++ może być czasami nazywany natywnym C++, gdyż kompilowany jest on wprost do kodu maszynowego . Na rysunku 1.1 pokazano podstawowe
możliwości
tworzenia programów w C++.
Rysunek 1.1
System operacyjny
Sprzęt
Rysunek 1.1 nie przedstawia jednak pełnego obrazu. Program może składać się jednocześnie z kodu napisanego w zarządzanym i natywnym C++ , a więc nie musimy trzymać się sztywno jednego stylu programowania. Oczywiście, mieszając dwa różne typy kodu , tracimy pewne rzeczy, a więc podejście to powinno być stosowane wyłącznie wtedy, gdy jest to konieczne, na przykład gdy chcemy przekonwertować istniejący już program napisany w natywnym C++ na program działający pod kontrolą CLR. Korzyści wynikające z używania zarządzanego C++ nie są oczywiście dostępne z poziomu C++ natywnego, a komunikacja pomiędzy składnikami programu napisanymi w tych dwóch typach języka może być znacznie wydłu żona. Możliwość łączenia zarządzanego i niezarządzanego kodu w jednej aplikacji może oka zać się jednak nie do przecenienia, gdy zajdzie potrzeba rozwinięcia lub rozszerzenia istnieją cego niezarządzanego kodu przy jednoczesnym korzystaniu z zalet używania CLR. Oczywiś cie, tworząc program od początku, przed rozpoczęciem jego pisania należy zdecydować się, jakiego typu aplikacją ma on być .
Nauka programowania dla slslemu Windows Tworzenie programów wykonywanych w systemie Windows oparte jest zawsze na dwóch podstawowych aspektach działania: utworzeniu graficznego interfejsu użytkownika (GUl), z którym użytkownik wchodzi w interakcje, oraz wykonaniu kodu przetwarzającego te inte
Rozdzial1. • Programowanie przy użyciU Visual C++ 2005
31
rakcje w celu zapewnienia aplikacji funkcjonalności. Visual C++ 2005 znacznie ułatwia pracę nad tymi aspektami tworzenia aplikacji Windows . W dalszej części tego rozdziału przekonamy się, że można stworzyć działający program dla Windowsa z GUl bez napisania nawet jednego wiersza kodu . Cały podstawowy kod może zostać wygenerowany automatycznie przez Visual C++ 2005 . Zrozumienie sposobu działania tego kodu jest jednak niezbędne, gdyż później będziemy chcieli go zmodyfikować i rozszerzyć, aby wykonywał zamierzone przez nas czyn ności. Aby tego dokonać , musimy bardzo dobrze rozumieć C++ . Z tego powodu na początku skupimy się na nauce samego języka C++ (zarówno natywnego, jak i w wersji C++/CLI), bez wdawania się w zawiłości programowania dla systemu Win dows. Gdy opanujemy już sam język C++, przejdziemy do tworzenia prawdziwych aplikacji Windows przy użyciu obu wersji języka C++. Oznacza to, że podczas nauki C++ będziemy tworzyć programy działające z poziomu wiersza poleceń. Dzięki takiemu podejściu będzie można skupić się na specyfice działania języka C++ i uniknąć w przyszłości komplikacji związanych z tworzeniem i kontrolą GUL Po opanowaniu C++ stwierdzisz, że przejście od praktycznego wykorzystania zdobytej wiedzy do tworzenia programów dla Windowsa jest naturalnym krokiem.
Nauka C++ Visual C++ 2005 standardach:
obsługuje
w
pełni
dwie wersje języka C++, zdefiniowane w dwóch
różnych
• Standard C++ ISO/ANSI służący do implementacji natywnych programów - C++ niezarządzany . Ta wersja języka obsługiwana jest przez większość platform komputerowych. • Standard C++/CLI, który został zaprojektowany specjalnie do tworzenia aplikacji działających pod kontrolą CLR i stanowi rozszerzenie C++ ISO/ANSI. Rozdziały od 2. do 10. poświęcone są nauce języka C++. Ze względu na fakt, że C++/CLI jest rozszerzeniem C++ ISO/ANSI , pierwsza część każdego rozdziału wprowadza elementy języka C++ ISO/ANSI, a druga objaśnia dodatkowe możliwości, których dostarcza C++/CLI. Pisząc programy w C++/CLI , możemy w pełni wykorzystać możliwości platformy .NET, co nie jest możliwe w programach pisanych w C++ ISO/ANSI. Mimo że C++/CLI jest rozszerze niem C++ ISO/ANSI , aby program mógł zostać wykonany całkowicie pod kontrolą CLR, musi on zostać napisany zgodnie z wymaganiami tej technologii. Oznacza to, że C++ ISO/ANSI posiada pewne właściwo ści , których nie można wykorzystywać w CLR. Jednym z przykładów, jak można się domyślić, jest brak kompatybilności mechanizmów przydzielania i zwalniania pamięci oferowanych przez C++ ISO/ANSI z CLR . Do zarządzania pamięcią musimy korzy stać z mechanizmu CLR, a to z kolei oznacza, że musimy używać klas C++/CLI, a nie na tywnych klas C++ .
32
Visual C++ 2005. Od podstaw
Standardy C++ Standard ISO/ANSI zdefiniowany jest w dokumencie ISO/lEC 14882 opublikowanym przez American National Standards Institute (ANSI). C++ ISO/ANSI istnieje od roku 1998 i ma już ugruntowaną pozycję . Obsługiwany jest przez kompilatory większości komputerowych platform sprzętowych i systemów operacyjnych. Programy napisane w C++ ISO /ANSI można dość łatwo przenosić pomiędzy różnymi systemami, chociaż prawdziwym wyznaczni kiem tego , czy dany program można łatwo przenieść, czy nie , są funkcje klas przez niego używanych w szczególności klas związanych z budową GUl. Standard C++ ISO/ANSI jest wybierany przez wielu profesjonalnych programistów ze względu na jego powszechną implementację oraz dlatego, że jest to jeden z naj potężniej szych dostępnych obecnie języków programowania.
Dokument iso.org.
opisujący
standard C++ ISO/ANSI
można zamówić
na stronie: http://www.
C++/CLI jest natomiast wersją języka C++, która rozszerza jego standardowe możliwości, czemu lepiej obsługuje specyfikację CLI zdefiniowaną w standardzie ECMA-355. Pierwsza wersja robocza tego standardu pojawiła się w 2003 roku i była rozwijana ze wstępnej specyfikacji techn icznej stworzonej przez Microsoft w celu umożliw ienia uruchamiania programów C++ na platformie .NET. Tak więc zarówno CLI, jak i C++ /CLI wywodzą się z firmy z Redmond , a ich przeznaczeniem jest współpraca z platformą .NET. Oczywiście ustandaryzowanie CLI oraz C++/CLI znacznie podwyższa prawdopodobieństwo implemen tacji w środowiskach innych niż Windows . Należy jednak pamiętać, że mimo iż C++/CLI jest rozszerzeniem C++ ISO/ANSI, to niektórych właściwości tego języka nie możemy wykorzy stywać, jeżeli chcemy, aby nasze programy działały w pełni pod kontrolą CLR . O właściwo ściach tych piszę w następnych rozdziałach.
dzięki
CLR ma pewne właściwości, które dają mu znaczną przewagę nad środowiskiem natywnym. Programy pisane dla CLR są bezpieczniejsze i mniej podatne na potencjalne błędy, które łatwo popełnić podczas wykorzystywania wszystkich możliwości C++ ISO/ANSI. CLR usu wa również wszelkie niekompatybilności związane z zastosowaniem różnych języków wyso kiego poziomu poprzez ustandaryzowanie środowiska, dla którego tworzone są programy. Dzięki temu można łączyć moduły napisane w C++ z modułami napisanym i w innych języ kach, takich jak C# lub Yisual Basic .
Aplikacje działające
wtrybie konsoli
Poza typowymi programami dla Windowsa, Yisual CH 2005 pozwala także na pisanie, kom pilowanie i testowanie programów C++ pozbawionych całego bagażu wymaganego od apli kacji okienkowych. Aplikacje te działają w oparciu o tryb tekstowy i wiersz poleceń . W Yisual CH 2005 nazywają się one aplikacjami konsolowymi (ang. eonsole applicationsy, ponieważ komunikacja z nimi odbywa się za pomocą klawiatury i ekranu w trybie tekstowym . Pisząc tego typu programy, można odnieść wrażenie, że odchodzimy nieco od tematu książki (jest to konieczne przed rozpoczęciem programowania specjalnie dla systemu Windows), ale jest to najlepszy sposób nauki C++. Nawet prosty program w Windowsie zbudowany jest
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
33
liczby wierszy kodu i ważne jest, aby zawiłości związane z systemem Windows nie naszej uwagi skupionej na mechanizmach działania języka C++. W związku z tym w początkowych rozdziałach, w których uczymy się samego języka C++, będziemy tworzyć proste aplikacje konsolowe, a dopiero później przejdziemy do bardziej skompliko wanych, złożonych z dużej liczby wierszy kodu, programów dla Windowsa. z
dużej
rozpraszały
Podczas nauki C++ będziemy mogli skoncentrować się na właściwościach języka, nie mar twiąc się o środowisko, w którym operujemy. Aplikacje konsolowe, które będziemy tworzyć, posiadają interfejs tekstowy, ale dla zrozumienia C++ to całkowicie wystarczy. Sam język z definicji nie posiada żadnych możliwości graficznych. Oczywiście programowaniu graficz nego interfejsu użytkownika poświęciłem dużo miejsca, ale w tej części książki, która została poświęcona programowaniu dla Windowsa przy użyciu biblioteki MFC w natywnym C++ oraz Windows Forrns z CLR . Istnieją dwa rodzaje aplikacji konsolowych i będziemy używać obu. Aplikacje konsolowe Win32 kompilowane są do kodu natywnego i za ich pomocą będziemy wypróbowywać moż liwości C++ ISO/ANSI. Aplikacje konsolowe CLR tworzone są dla CLR, a więc będziemy ich używać, pracując z językiem CH/CLI.
Koncepcie programowania wsystemie Windows do programowania dla systemu Windows zakłada wykorzystanie wszystkich w Visual C++ 2005. Narzędzia służące do tworzenia nowego projektu potrafią automatycznie wygenerować kod szkieletu różnego rodzaju aplikacji, włączając w to podstawowe programy dla Windowsa. Proces pisania każdego programu lub komponentu w Visual C++ 2005 rozpoczyna się od utworzenia nowego projektu. Aby sprawdzić , jak to działa, w dalszej części rozdziału utworzymy kilka przykładów włącznie ze szkieletem pro gramu dla Windowsa.
Nasze
podejście
narzędzi dostępnych
Programy w Windowsie mają inną budowę niż typowe aplikacje konsolowe wykonywane za pomocą wiersza poleceń - są bardziej skomplikowane. W aplikacji konsolowej dane można przyjmować wprost z klawiatury i wyniki działań wysyłać z powrotem do wiersza poleceń. Programy dla Windowsa natomiast pobierają i wysyłają dane wyłącznie za pomocą funkcji systemu Windows . Nie pozwalają one na dostęp do zasobów sprzętowych . Ze względu na fakt, że system Windows pozwala na uruchamianie kilku aplikacji naraz, musi on umieć określić, dla której z nich przeznaczone zostało dane zdarzenie, np. kliknięcie przyciskiem myszki lub naciśnięcie klawisza na klawiaturze, a następnie wysłać sygnał do właściwego programu. Dzięki temu system Windows sprawuje podstawową " ko n tro l ę nad całym procesem komuni kacji z użytkownikiem. A zatem natura interfejsu pomiędzy użytkownikiem a systemem Windows jest taka, że po zwala na wiele różnych operacji wejścia i wyjścia w tym samym czasie. Użytkownik może wybrać jedną z wielu opcji dostępnych w menu, kliknąć przycisk na pasku narzędzi czy przy ciskiem myszki w dowolnym miejscu okna aplikacji. Dobrze zaprojektowany program dla Windowsa musi być przygotowany na wszelkiego rodzaju dane wejściowe w dowolnym czasie, gdyż nie ma sposobu dowiedzenia się z góry, jakiego rodzaju dane wejściowe zostaną przekazane . Czynności tego typu wykonywane przez użytkownika są przechwytywane przez system jako pierwsze i nazywają się zdarzeniami. Zdarzenie wywołane przez użytkownika
34
Visual C++ 2005. Od podstaw w obrębie interfejsu powoduje zazwyczaj wykonanie określonego fragmentu kodu. A zatem sposób wykonywania całego programu zależy od zachowania użytkownika. Programy ope rujące w ten sposób nazywane są programami zdarzeniowymi i różnią się od zwykłych programów proceduralnych, które odznaczają się pojedynczą kolejnością wykonywania . Wprowadzanie danych do programu proceduralnego kontrolowane jest przez jego kod i może mieć miejsce tylko wtedy, gdy program na to zezwoli. Tak więc program Windows składa się przede wszystkim z fragmentów kodu , które reagują na zdarzenia spowodowane przez użyt kownika lub sam system. Struktura tego typu programu została przedstawiona na rysunku 1.2.
Rysunek 1.2 Każdy prostokąt na rysunku 1.2 reprezentuje fragment kodu napisany specjalnie do obsługi jednego określonego zdarzenia. Z rysunku można wywnioskować, że przedstawiony pro gram jest nieco rozbity ze względu na brak połączeń pomiędzy niektórymi blokami, ale tak nie jest, gdyż głównym spoiwem jest tutaj system operacyjny Windows. Pisząc program, można go traktować jako swego rodzaju naukę wykonywania określonych czynności w sys temie Windows.
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
35
Oczywiście wszystkie moduły obsługujące różne zdarzenia zewnętrzne, takie jak wybór menu
lub kliknięcie przyciskiem myszki , mają dostęp do wspólnej puli danych właściwych dla danej aplikacji. Dane te zawierają informacje o tym, czym jest program - np. bloki tekstu w edytorze lub rekordy przechowujące punkty gracza w programie mającym za zadanie śle dzenie wyników drużyny piłkarskiej - jak również informacje o niektórych zdarzeniach ma jących miejsce podczas wykonywania programu . Te współdzielone dane pozwalają różnym częściom programu, które wydają się niezależne, komunikować się oraz operować w skoor dynowany i zintegrowany sposób. Więcej na ten temat piszę w dalszej części książki. Nawet najbardziej podstawowy program dla Windowsa składa się z kilku wierszy kodu, a w programach wygenerowanych za pomocą kreatora takiego jak Visual C++ 2005 kilka przeistacza się w kilkaset. Aby uprościć proces nauki C++, potrzebujemy jak najprostszego kontekstu. Na szczęście Visual C++ 2005 posiada ś ro d o w i s k o w sam raz nadające się do tego celu.
Czym jest zintegrowane środowisko programistyczne Zintegrowane środowisko programistyczne (ang. Integrated Developm ent Environment IDE) , które dostarczane jest z Visual C++ 2005, jest kompletną platformą do tworzenia, kom pilowania, konsolidowania i testowania programów napisanych w C++. Tak się składa, że doskonale nadaje się również do nauki języka C++ (w szczególności w połączeniu z dobrą książką).
Visual C++ 2005 zawiera wiele w pełni zintegrowanych narzędzi zaprojektowanych w celu uproszczenia całego procesu pisania programów w C++. Część tych narzędzi poznamy już w tym rozdziale, ale zamiast przedzierać się przez nudną listę abstrakcyjnych opcji i właściwo ści, najpierw opanujmy podstawy, aby zobaczyć, jak działa IDE. Reszta przyjdzie stopniowo, w miarę postępu nauki.
Składniki
systemu
Podstawowymi składnikami Visual C++ 2005, dostarczanymi jako część IDE, są edytor, kompilator, program łączący (konsolidator) oraz biblioteki. Są to narzędzia niezbędne do napisania i wykonania programu w C++ . Ich funkcje zostały opisane poniżej .
Edylor Edytor jest interaktywnym środowiskiem do tworzenia i edycji kodu źródłowego w języku C++. Poza typowymi, znanymi każdemu funkcjami typu kopiuj i wklej, edytor posiada także funkcję kolorowania kodu . Edytor automatycznie rozpoznaje podstawowe słowa kluczowe
36
VisIlai C++ 2005. Od podstaw w języku C++ i nadaje im odpowiedni kolor, zgodnie z ich przeznaczeniem. Funkcja ta nie tylko sprawia, że kod jest o wiele bardziej czytelny, ale także pozwala natychmiast zoriento wać się, że został popełniony błąd przy wpisywaniu tych słów.
Kompilator Kompilator konwertuje kod źródłowy na kod obiektowy oraz wykrywa błędy występujące podczas procesu kompilacji i o nich raportuje. Kompilator potrafi wykryć wiele różnego rodzaju błędów związanych z nieprawidłowym lub nierozpoznanym kodem, jak również błę dów strukturalnych, np. kiedy fragment kodu nigdy nie zostanie wykonany. Kod obiektowy wygenerowany przez kompilator przechowywany jest w plikach zwanych plikami obiekto wymi. Istnieją dwa rodzaje kodu obiektowego, który może zostać wygenerowany przez kom pilator. Kody te zazwyczaj przechowywane są w plikach o rozszerzeniu .obj.
Program łącząCY Program łączący (konsolidator) dołącza różne moduły wygenerowane przez kompilator z pli ków z kodem źródłowym, dodaje wymagane moduły z kodem z bibliotek dostarczanych jako część C++ oraz łączy wszystko w jedną wykonywa1ną całość. Konsolidator może także wykrywać błędy i o nich raportować, np. gdy brakuje części programu lub gdy znajdzie odwołanie do nieistniejącego komponentu biblioteki.
Biblioteki Biblioteka to po prostu zbiór wcześniej napisanych procedur, które rozszerzają możliwości C++, dostarczając standardowych, profesjonalnie zaprojektowanych jednostek kodu, które można wykorzystać we własnych programach w celu wykonania niektórych częstych operacji. Operacje zaimplementowane przez procedury w rozmaitych bibliotekach dostar czanych przez Visual C++ 2005 znacznie zwiększają produktywność, ponieważ pozwalają zaoszczędzić czas potrzebny na napisanie i testowanie kodu dla tych operacji. Wspominałem już o bibliotece platformy .NET, ale jest ich o wiele więcej, zbyt wiele, by je wszystkie tutaj wymienić, ale najważniejsze z nich zostaną opisane.
języka
Standardowa biblioteka C++ definiuje podstawowy zestaw procedur wspólnych dJa wszyst kich kompilatorów C++ ISO/ANSI. Zawiera wiele procedur, na przykład funkcje operujące na liczbach (na przykład obliczające pierwiastek kwadratowy czy funkcje trygonometryczne), procedury przetwarzania znaków i ciągów, takie jak klasyfikacja znaków i porównywanie ciągów, a także wiele innych. W trakcie nauki języka C++ ISO/ANSI nauczymy się posługi wać wieloma z nich. Istnieją także biblioteki obsługujące rozszerzenia C++/CLI do C++ ISO/ANSI. Natywne aplikacje oparte na oknach obsługiwane są przez bibliotekę zwaną Microsoft Foun dation Classes (MFC). Biblioteka ta w znacznym stopniu ułatwia proces tworzenia graficz nego interfejsu aplikacji. Więcej na temat biblioteki MFC dowiemy się po zakończeniu nauki języka C++. Inna biblioteka zawiera zestaw narzędzi - zwanych Windows Forms - mniej
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
37
więcej o d po w i ad ający c h
bibliotece MFC dla programów opartych na oknach, które wyko nywane są za pomocą ś ro d o w is k a .NET . W dalszej częśc i ks i ążki dowi emy się, jak używać biblioteki Windows Forms do tworzenia programów.
Używanie IDE Wszystkie programy w tej książce s ą tworzone i wykonywane wewnątrz IDE. Po uruchomien iu aplikacj i Visual C++ 2005 powinni śm y zoba czyć okno podobne do tego na rysunku 1.3. 1'';
m [gJ
Start Page - Microsoft Visual Studio Edit View rocs
File
- =t
..
l'r1SON: Vlsual C+ +
Recent Proleci'
C+ + At Wark : IRegis:tr ar , Finding su bme nus . and Mor-e Fr l, 08 sep 2C06 21:49 ;4 1 GMT - Thls rnonlh : DLL problams, conta xt menus, rvu= C str j-qs to managedC++, and mors.
C+ + At Wark : Cr eat e dynamie dialogs, sat ellite Dt.Ls, and more fv'o1, 07 Aug 2006 2 1:36 :2 1 GMT - Th is mon lh Pau l rx.ssoa te ac hes
Open : Create:
Project.;
1\.';Jf"-!'"' s a..
Pro]BC t ..
[weo SIt..
rea ders the rkjl t wCtI to ( rea le dyna mk: dł a logs , explains setalute OLls and discusses lcI1guagereso.rce Oll s. C + + At Wark : Customizing Combobox and Listbox Fn, 07.1J ł 2OC620 :29 :28 GMT - This rnonlh Faul Djt esc ta codes same M ~rosoft OffK:e· styls dialog box featur es.
Net ti ng C + + : Resouc:e Cleanup
Ge lting Sta rted
No detin1t.1on e e i e e e e e
Ready
Rysunek 1.3 Okno znajduj ące się po lewej stronie na rysunku 1.3 to okno ekspl oratora rozwiązań (So/ution Exp/orer), okno znajdujące się w prawym górnym rogu, w którym obecnie wyświetlona zo st ała strona startowa, to okno edytora (Edit or window), a okno na samym dole to okno wyj ś ci a (Output window). Okno eksploratora rozwiązań umożl iwia nawigację pomiędzy plikami programu oraz wy świetlan i e ich zawarto ści w oknie edytora, a także dodawanie nowych plików do programu. Okn o Solution Explorer może m ieć do trzech dodatkowych z akładek (na rysunku 1.3 widoczne s ą tylko dwie) , które reprezentują C/ass View (widok klas), Resour ce View (widok zasobów) oraz Property Manager (menedżer właściwo ści) aplika cji . Wyboru wyśw i e t l a nyc h zakład e k można dokonać w menu View. Okno edytora służy do wprowadza nia i modyfikowania kodu źródłowego oraz innych komponentów aplikacji. Okno Output wyświetla komunikaty powstałe w wyniku kompilacji i konsolidacji programu .
38
Visual C++ 2005. Od podslaw
Opcje paska narzędzi Paski narzędzi , które mają być wyśw i etl an e , w Visual C++ można wybrać, przyci skiem myszy w polu paska n arzędzi. Pojawia się menu zawieraj ące pasków n arzędzi (rysunek lA). Aktywne paski są zaznaczone.
Rysunek 1.4
'" l:=: '"
IL......J
klikając
prawym
listę dostępnych
Bu ild Class Designer Crystal Reports - In ser t Crys ta I Reports - Ma in Data Design Database Diagr-am
El
Debug Debug Location Device Dialog Edito r Formatti ng Help HTML Source Edit ing Image Editor Layout Query Designer Report Border s Report For matti ng Sour ce Contro i
El Standar d Sty le Shee t Tab le Designer Text Editor
W menu tym można wybrać , które paski narzędzi maj ą być zawsze widoczne. Można sobie podobne ś ro d ow i s ko pracy jak na rysunku 1.3, wybi eraj ąc kolejno elementy menu: Sui/d, Class Designer, Debug , Standard oraz View Designer. Aby wybrać element z listy, na leży klikn ąć na szarym polu po jego lewej stronie . Aby sc h ować element, trzeba kliknąć znak zaznaczenia znajdujący si ę po jego lewej stronie.
stworzy ć
Nie ma potrzeby przeładowywać okna aplikacji paskami n arzędzi , które mogą s ię nam kiedyś Niektóre z nich pojawiają się automatycznie, gdy są potrzebne, a więc prawdopo dobnie w większości przyp adków najlepszym rozwiązaniem okażą się domyślne ustawienia. Podczas tworzenia programu mo żemy dojść do wniosku , że byłoby wygodniej, gdyby niektóre paski narzędzi były wyświetlone cały czas. Zestaw wyświetlanych pasków można modyfi kowa ć wedle potrzeb, klik ając praw ym przyciskiem myszy na szarym polu w obrębi e paska n arzędzi i wybieraj ąc żąd an e paski z menu kontekstowego. przydać .
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
39
Podobnie jak we wszystkich programa ch w Windowsie, w paskach narzędzi Visual C+ + 2005 dostępn e są chmurki z podpowiedziami . Aby dowiedzieć s ię. do czeg o służy dana opcja, należy umieścić nad nią kursor i odcz ekać sekundę lub dwie na wyświetlenie informacji w chmurce.
Dokowalne paski narzędzi Dokowalny pasek narzędzi to taki , który można dowolnie przemieszczać w obrębie okna za pomocą myszy . Kiedy zostanie umieszczony w pobli żu jednej z czterech krawędzi okna , jest dokowany i wyglądem przypomina paski widoczne na górze okna. Pasek znajdujący się w górnej linii, zawierający ikony dyskietek i ramkę tekstową po prawej stronie lornetki, nosi nazwę Standard. Można go przeciągnąć w inne miejsce okna , klikając go lewym przyciskiem myszy i - nie puszczając tego przycisku - umieszczając go w innym dowolnym miejscu. Po odciągnięciu od krawędzi pasek zamienia się w oddzielne okno, które można dowolnie przemieszczać.
Po odciągnięciu dokowalnego paska narzędzi wygląda on jak standardowy pasek widoczny na rysunku 1.5. Ma on postać niewielkiego okna opatrzonego odpowiednią etykietą. Pasek w takim stanie nazywa się paskiem pływającym. Wszystkie paski narzędzi widoczne na rysunku 1.3 mogą być dokowane i pływające. Spróbuj przeciągnąć niektóre z nich. Kiedy zostaną zadokowane, wracają do swojego dawnego wyglądu. Paski można dokować przy każdej z czterech krawędzi okna głównego.
Rysunek 1.5 Niektóre ikony paska zadań Visual C++ 2005 będą wyglądały podobnie do tych znanych z innych aplikacji systemu Windows, ale ich przeznaczenie w Visual C++ może być trochę inne. Z tego powodu będę wyjaśniał, do czego one służą, gdy nastąpi potrzeba ich użycia. Jako że do tworzenia nowego programu za każdym razem trzeba tworzyć nowy projekt, dobrym punktem startowym nauki obsługi Visual C++ 2005 będzie zaznajomienie się z mechanizmem definiowania projektów.
Dokumentacia Nadarzy się wiele sytuacji, w których będziemy mieli potrzebę zasięgnięcia dodatkowych informacji o Visual C++ 2005. Wyczerpującym źródłem wiedzy na ten temat jest dokumenta cja MSDN (ang. Microsoft Development Network Librarys. Zawiera ona opis wszystkich możliwości programu, a także wiele innych informacji. Podczas instalacji Visual C++ 2005 pojawia się opcja pozwalająca zainstalować pełną dokumentację MSDN. Jeżeli dysponujesz wystarczającą ilością miejsca na dysku, to zachęcam Cię do jej zainstalowania.
40
Visual C++ 2005. Od podstaw Aby uzyskać dostęp do zasobów MSDN, należy nacisnąć klawisz F l . Menu Help umożliwia przeszuk iwanie dokumentacji na różne sposoby. Poza źródłem wiedzy o możliwościach pro gramu, dokumentacja MSDN stanowi przydatne narzędzie, gdy ma się do czynienia z błędam i w kodzie , o czym przekonamy się w dalszej części rozdziału .
Projekt' i rozwiązania Projekt jest zbiorem wszystkich składników składających się na program - może to być program konsolowy, program oparty na oknach lub jeszcze inny typ programu. Zazwyczaj składa się on z jednego lub większej liczby plików z kodem źródłowym oraz prawdopodobnie innych plików zawierających dodatkowe dane . Wszystkie pliki projektu przechowywane są w folderze projektu , a szczegółowe informacje o nim przechowywane są w pliku XML o rozszerzeniu . vcproj, który również znajduje s i ę w folderze projektu. Folder projektu zawie ra równ ież inne foldery, w których zapisywane są pliki powstałe w procesach kompilacji i konsolidacji projektu. Czym jest rozwiązanie, mówi już sama jego nazwa. Jest to mechanizm łączący wszystkie pro gramy i inne zasoby składające się na rozwiązanie jednego problemu związanego z przetwa rzaniem danych . Na przykład rozproszony system zgłaszania zamówień dla operacji bizneso wych mógłby składać się z kilku różnych programów, z których każdy mógłby być rozwijany jako projekt w obrębie jednego rozwiązania. Tak więc rozwiązanie stanowi folder, w którym przechowywane są wszelkie informacje dotyczące jednego lub większej liczby projektów. Co za tym idzie, w folderze tym znajduje się co najmniej jeden podkatalog z projektem. Dane na temat projektów rozwiązania przechowywane są w dwóch plikach o rozszerzeniach .sln oraz .suo. Nowe rozwiązanie tworzone jest automatycznie, gdy tworzy się nowy projekt, chyba że dodamy go do już istniejącego rozwiązania. Kiedy podczas tworzenia projektu zostanie utworzone rozwiązanie , to istnieje możliwość póź niejszego dodawania do niego następnych projektów. Można dodawać projekty dowolnego rodzaju, ale zazwyczaj dodaje się takie, które są w jakiś sposób powiązane z już istniejącym lub istniejącymi projektami w rozwiązaniu. Z reguły, jeżeli nie istnieją żadne przeciwwska zania, każdy projekt powinien być przypisany do jakiegoś rozwiązania. Wszystkie przykłady w tej książce stanowią pojedyncze projekty z własnymi rozwiązaniami .
Definiowanie projektu Pierw szą czynnością, którą należy wykonać , aby rozpocząć pisanie programu w Yisual C++ 2005, jest stworzenie nowego projektu, kolejno wybierając opcje File/New/Project lub naci skając kombinację klawiszy Ctrl+Shift+N. Poza plikami zawierającymi cały kod i wszelkie inne dane, które składają się na program , w folderze projektu znajduje się plik XML. Są w nim zapisywane wszystkie opcje Yisual C++ 2005, których używaliśmy. Mimo że nie ma potrzeby własnoręcznego edytowania tego pliku (tym zajmuje się w całości IDE), to można go otworzyć i sprawdzić, co zawiera. Pamiętaj tylko, aby nie zmienić przez przypadek jego zawartości.
Na tym
skończymy, jeśli
chodzi o teorię. Czas
zabrać się
do pracy.
Rozdział1.
• Programowanie przy użyciu Visual C++ 2005
41
~ Tworzenie proiektu aplikacji konsolowej dla systemu Win32 Utworzymy teraz projekt aplikacj i konsolowej. Najpierw z menu Fil e n ależy w ybr a ć opcje New/Projec t. Pojawi się okno dialogowe tworzenia nowego projektu, podobne do pokazanego na rysunku 1.6. -
_-
..
-
-
-- -
-----
-
----
-
[1]rBJ
New Project Project types :
- ---,
lIisual Studio installed templates
1 - - - -.0. 0.00.. 0 -
ATL CLR
00
;~ W in32 Console Applicatron
General
t
m(@,
Templates :
;= Visual C++
Mf C Smar t Oevt e Win32
Other Languages
O\her ProJect Types
I
1'5IW k132 ProJect
I
~v Temp lat~
. Search Onlne Templ atesooo
L
IA prcject for creatrng a Wrn32 consoleapplication LQCation:
l ID :\Translations'ł>ellon\jvO'
sa"!lon Name :
,
Name :
Cwl _0l
I CW1_Ol
~'
Hortons Visual C++
.. -
I
-
L~l l
2005\Przyk łady
I
~Create di' ectDry for -soluton
I
ca ncel
I
--
I
I
BrOWS8 ...
OK
II
Rysunek 1.6 W lewym panelu okna dialogowego New Pr oject pokazane s ą dostępne typy tworzonych projektów . My wybieramy Win32. W ten sposób informujemy program , którego kreatora ma użyć do utworzen ia wstępn ych plików projektu . W prawym panelu widoczna jest lista szablo nów dostępnych dla wybranego typu projektu. Wybrany szablon zostanie wykorzystany przez kreator podczas tworzenia plików projektu. W następnym oknie, które poj awia się po klik n ięciu przycisku OK, możemy ustawić opcje dla tworzonych plików. W przypadku większo ści typów lub szablonów autom atycznie tworzony jest podstawowy zestaw modułów źródło wych programu . Możemy
teraz wpisać w polu Name wybraną nazwę dla naszego projektu, np. Cwl_Ol. Visual C++ 2005 pozwal a na stosowanie długich nazw plików, a więc mamy tu duże pole manewru. Nazwa folderu rozwiązania pojawia się w polu tekstowym na dole i domyślnie jest taka sama jak nazwa projektu. W razie potrzeb y można j ą jednak zmienić . W tym samym okn ie można także zmieni ć lok al izację folderu rozwiązani a na dysku za pomoc ą pola Location. Jeżeli wpi szemy tylko nazwę projektu, to zostanie on umieszczony w folderze o takiej samej nazwie w lokalizacji pok azanej w polu Location. Domyślnie, jeżeli fold er rozwiązania nie istnieje, zostanie on automatycznie utworzony. Jeśli chcemy, aby nasze pliki zostały zap isane w innym
42
Visual C++ 2005. Od podstaw katalogu , wystarczy zmienić ścieżkę w polu Location, wpisując ją ręcznie lub wyszukując za pomo cą opcji Browse. Kliknięcie przycisku OK spowoduje ukazanie s i ę okna dialogowego kreatora aplika cji Win32, który został zapre zentowan y na rysunku 1.7.
Rysunek 1.7 Welcom e to th e Win32 Application Wlzard
These ere the currentprO)ect settings:
Ovet"łiew AppkaŁkm
5ettirqs
• ccesce ~(~ion
ekkFinishfrem any wroow to eccept tbe current seł:Łf'J05 . Afteryou creete the project, see the pecject 'sreeone. txt f ~ e for inforrMtion ebout the projectfeaturesand flles that.ere ęenereted.
",. "
"' I
LI
Next>
II
F" j,h
II
C.ncel
l
Okno to zawi era informacj e o wybr anych opcjach. Kliknięcie przycisku Finish spowoduj e utworzenie na podstawie tych informacji wszystkich plików projektu. W oknie tym możem y także zmienić ustawienia aplikacji , klikając Application Settings po lewej stronie kreatora, jak pokazano na rysunku 1.8.
Rysunek loB Appllcation Set tings
Overvlew
AppIication SeUjng<
Add corrmcn heeder f ~es for:
ApplicatiOn tvce :
O 'f!lfldows application
o CQO'oie applicotloo
D an D~FC
O Qll
O 2lalk 10 ' 01")' AddilionalQPl;1ons:
D ~mpty projed ~f.oort~ J)l,
o Erecompded beeeer .
I < Prevlou, I "-"
F ,,~h
II
Cancel
Okno Application Settings pozwala na wybór opcji projektu. C h oć w przypadku w iększości projektów, które będziemy tworzyć podczas nauki języka C++ , wybierzemy opcję Empty project, tym razem możemy pozostawić ją bez zmian i kliknąć przycisk Finish . Kreator apli kacji utwo rzy projekt ze wszystkimi domyślnymi plikami.
Rozdział 1. • Programowanie przy użyciu VisIlai C++ 2005
43
Folder projektu ma taką samą nazwę, jaką podaliśmy dla samego projektu, oraz zawiera wszystkie pliki składające się na projekt. Folder rozwiązania ma nazwę taką samą jak folder projektu - chyba że ją zmieniliśmy - oraz zawiera pliki definiujące zawartość rozwiązania. Przeszukując zawartość folderu rozwiązania za pomocą eksploratora Windows, stwierdzisz, że zawiera on trzy pliki : • plik z rozszerzeniem .sln, w którym zapisywane
są
informacje o projektach
rozwiązania,
• plik z rozszerzeniem .suo, w którym przechowywane dotyczącego tego rozwiązania,
są
opcje
użytkownika
• plik z rozszerzeniem .ncb, który przechowuje dane Intellisense dla rozwiązania. Intellisense to narzędzie automatycznie uzupełniające i podpowiadające podczas pisania kodu w oknie edytora. Jeżeli
zajrzymy do folderu projektu za pomocą eksploratora Windows, to znajdziemy tam plików, z których jeden to ReadMe.txt, zawierający infor macj e o zawartości plików utworzonych dla projektu. Jedynym plikiem, o którym może nie być żadnych informacji w pliku ReadMe.txt,jest plik o złożonej nazwie typu Cwl _OI.vcproj.NazwaKomputera. NazwaUzytkownika.uzytkownik, w którym przechowywane są ustawienia projektu. sze ść
Utworzony projekt zostanie automatycznie otwarty przez Visual C++ 2005 w lewym panelu, jak widać na rysunku 1.9. Celowo zwiększyłem jego szerokość, aby było widać pełne nazwy na zakładkach .
Okno Solution Explorer pokazuje wszystkie projekty w bieżącym rozwiązaniu oraz zawarte w nich pliki - u nas oczywiście jest tylko jeden projekt . Zawartość każdego z plików można wyświetlić w nowej zakładce w oknie edytora, dwukrotnie klikając jego nazwę lewym przy ciskiem myszy w zakładce okna Solution Explorer. Pomiędzy poszczególnymi otwartymi pli kami w panelu edycyjnym można się szybko przemieszczać, klikając odpowiednią zakładkę. Zakładka
C/ass View pokazuje listę klas zdefiniowanych dla projektu oraz treść każdej z nich. W naszej aplikacj i nie ma żadnych klas, a więc zakładka jest pusta. Kiedy będziemy mówić o klasach, przekonasz się, że za pomocą zakładki C/ass View można łatwo i szybko poruszać się po kodzie odnoszącym się do definicji i implementacji wszystkich klas.
44
VislJal C++ 2005. Od podstaw Zakładka
Property Manager pokazuje właściwości, które zostały ustawione dla wersji testo wej i ostatecznej (ang. Debug i Release) projektu. Znaczenie tych wersji wyjaśnimy w dalszej części rozdziału. Dowolną właściwość można zmienić, klikając ją prawym przyciskiem myszy i wybierając opcję Properties z menu kontekstowego. Spowoduje to wyświetlenie okna dialo gowego, w którym można ustawić właściwość projektu . Innym sposobem wyświetlenia okna dialogowego właściwości jest naciśnięcie kombinacji klawiszy Alt+F7. Więcej na ten temat piszę przy okazji opisywaniu wersji testowej i ostatecznej programu. Zakładka Resource View pokazuje okna dialogowe, ikony, paski narzędzi i inne zasoby uży wane przez program. Jako że nasz program to aplikacja konsolowa, nie używa on żadnych zasobów. Gdy rozpoczniemy pisać programy dla Windowsa, to w tym miejscu pojawi się wiele elementów. Za pomocą tej zakładki można dodawać do projektu dostępne zasoby lub
edytować już istniejące .
Jak w przypadku większości składników IDE Visual C++ 2005, w zakładce Solution Explorer, a także w innych zakładkach dostępne są menu zależne od kontekstu, które pojawiają się po kliknięc iu prawym przyciskiem myszy jednego z elementów zakładki, a czasami także w jej pustym obszarze. Jeżeli panel Solution Explorer przeszkadza Ci podczas pisania kodu, możesz go ukryć, klikając ikonę Autohide. Aby go ponownie wyświetlić, należy kliknąć jego nazwę po lewej stronie okna IDE.
Modyfikowanie kodu źródłowego Kreator aplikacji generuje kompletny program konsolowy Win32, który można poddać kom pilacji i uruchomić . Niestety program ten w obecnym stanie nic nie robi . Aby był on bardziej interesujący, należy wnieść do niego kilka zmian . Jeżeli nie widać go jeszcze w panelu Edi tor, dwukrotnie kliknij plik Cwl_Ol.cpp w panelu Solution Explorer. Jest to główny plik z kodem źródłowym programu, który został wygenerowany przez kreator aplikacji (rysu nek 1.10).
Rysunek 1.10
twl Ol.cpp~tP~
... x
I
Sil
(Global SCope)
~; L
TO '_'""'"""' r e t.ur n
9'
ta
vi --l
~ e r qc ,
- TCHAR'
a"gv[) I
o:
II
I
11
li
rz
I I
.J.
I <
-
I
> II ~
Rozdział 1.
• Programowanie przy użYCiu Visual C++ 2005
45
Jeżeli nie widzisz u siebie numerów linijek, to z menu Tools wybierz Options. Pojawi się okno opcj i. W oknie tym poszukaj opcji d oty czący ch edyt ora tekstowego (Text Editor) i kliknij C/C++, a następnie w prawej części okna zaznacz opcję Line numbers. Najpierw tylko z grub sza objaśnię, do czego służy kod widoczny na rysunku 1.10. Więcej na ten temat dowiesz się
trochę później.
komentarzami . Wszystko, co znajduj e s i ę za znakami / /w tym samym Jeżeli chcesz za pomoc ą komentarzy opisywać, co robi program, używaj tych znaków. Dwie pierwsze linijki
są
wi erszu, jest ignorowane przez kompilator.
W czwartej linijce znajduje się dyrektywa #i nc l ude, która zastęp owana je st zawartością pliku stdafx .h. Jest to standardowy sposób dodawan ia zawartości plików z rozszerzeniem .h do plików z kodem źródłowym o rozszerzeniu .cpp w programa ch w C++. Wiersz numer 7 jest pierwszym , który zawiera wykonywalny kod oraz
rozpoczęcie funkcji
_tmai nr). Funkcja to po prostu nazwany fragment kodu, który wykonuje jakieś zadanie. program w C++
s kła d a s i ę
Każdy
z co najmniej jednej , a zazwyczaj wielu funkcji .
Wiersze 8. i 10. zawierają odpowiednio otwarcie i zamknięcie nawiasu klamrowego, w obrębie które go znajduje s i ę wykonywalny kod funkcji _tmain O. A zatem cały wykonywalny kod znajduje si ę w wierszu 10., a jego jedynym zadani em je st zakoń czenie działania programu. Możemy
teraz
dodać następujące
II Cwl _OI.cpp: definiowanie punktu II
dwa wiersze kodu w oknie edytora:
wejściowego
dla aplikacji konsolowej .
#include "st dafx.h" #include int _tma in(i nt argc. _TCHAR* argv[]) {
st d: :cout return O:
«
"Witaj
św i ec i e
vn":
Niezaciemn ione wiersze to te, które zostały wygenerowane na samym początku. Wiersze, które należy dodać , są w ramce. W celu dodania nowego wiersza umieść kursor na końcu poprzedniego i naci śnij klawisz Enter, co spo woduje utworzenie nowego, pustego wiersza, w którym można wpisać kod. Pamiętaj, że Twój kod musi być identy czny z kodem pokaza nym na listingu. W innym przypadku program może nie dać się poprawnie skompilować. Pierwszy z dwóch nowych wierszy to dyrektywa #i ncl ude dodająca do pliku źródłowego treść standardowych bibliotek C++ ISO/ANSI. Biblioteka zawiera definicje podstawo wych operacji wejścia-wyjścia. Drugi wiersz wysyła do wiersza poleceń tekst. st d: :cout jest nazwą standardowego strumienia wyjściowego i drugi dodany wier sz dokonuje zapi su do niego łańcucha "Wi t aj ś w i eci e I \n". Wszystko, co pojawi się pomiędzy podwójnymi cudzy słowam i, wypisywane jest w wierszu poleceń.
46
Visual C++ 2005. Od podstaw
Kompilacja rozwiązania Aby skompilować rozwiązani e, n al eży n a cisnąć klawi sz F7 lub wybrać z menu Build/Build Solution. Można także kliknąć odpowi edni przycisk na pasku narzędzi. Przyciski paska narzę dzi menu Build mogą nie być widoczn e, ale można to łatwo zmienić, klikając prawym przyci skiem myszy w obszarze paska narzędzi i wybi eraj ąc pasek Build z listy. Kompilacja programu powinna zakończyć się pomyślni e . J eżeli poj awi ą s i ę bł ędy, upewnij się, czy w kodzie nie ma żadnych pomyłek, a więc w s z c ze g ó l n ośc i dokł adnie sprawdź dwa nowe wiersze.
Pliki tworzone podczas kompilacji aplikacji konsolowej Po pomyślnym zakończeniu kompilacji projektu zajrzyj do folderu projektu i znajdź podkatalog o nazwie Debl/g . Katalog ten zawiera pliki powstałe w procesie wła śnie ukończonej kompilacji projektu. Zau waż, że znajduj e się w nim kilka plików. Wszystkie pliki, poza tym z rozszerzeniem .exe, który j est naszym programem w wykonywa1 nej formie , nie są dla nas i n te re s uj ąc e i nie musimy zbyt wiele o nich w i ed z i eć . Aby j ednak zaspokoić Twoj ą ci ekawość, zw ięźle wyj aśn iam, do czego służą bardziej interesujące z nich.
Rozszerzenie pliku
Opis
.exe
Program w postaci wykon ywalnej. Plik ten powstaje tylko wted y, gdy kompil acja i konsolidacja z akończą się powod zeniem .
.obj
Pliki te to pliki obiektowe i tworzone są przez kompilator. Zawierają one kod maszynowy z plik ów źródłowych programu. Razem z plikami bibliotek uż yw ane są przez konsolid ator do utworzenia pliku .exe.
.ilk
Plik ten używ any jest przez konsolidator w momencie przebudowy projektu . Pozwala on temu programowi na stopniowe dołączanie plików obiektowych utworzonych ze zmodyfikow anego kodu źró dł o we g o do istniejącego pliku .exe. Dzięki temu unika si ę ponownej kons olid acji wszystkich komponentów programu przy każdej je go zmianie.
.p ch
Jest to prekompilowany plik naglówkowy. W plikach tego typu przechowywane s ą duże ilo ś ci kodu, który nie podlega modyfikacji (w szczególności kod dostarc zany prz ez biblioteki C++). Kod ten zostaje przetworzony jeden raz, a następnie je st prz echowywany w pliku o roz szerzeniu .pch. Dzięki tym plikom ponowna komp ilacja programu zajmuje znacznie mniej czasu.
.pdb
Plik ten zaw iera informacj e debu gowania, które są wykorzystywane podczas uruchamia nia programu w trybie debugowania. W trybie tym możn a dynamicznie przegląd a ć informa cje generowane podczas działania programu .
.idb
Zawiera informacj e
u żywane
podczas przebudowy
rozwiązan i a .
Wersie 1es1owa i os1a1eczna programu Za pomocą menu Project/Cwl Ul Properties można ustawić wiele opcji dla projektu . Opcje te okre ślaj ą sposób przetwarzania kodu źródłowego podczas kompil acj i i konsol idacji . Zestaw opcji, za pomo cą którego utworzony został wykonywalny program, nazywa si ę konfigura cją (ang. conjiguration). Podczas tworzenia nowej powierzchni robocz ej dla projektu Visual
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
47
C++ 2005 automatycznie tworzy konfiguracje do utworzenia dwóch wersji aplikacji. Jedna z nich, zwana wersją testową (ang. debug version), zawiera informacje pomocne przy poszu kiwaniu błędów w programie. Jeśli coś jest nie w porządku, przy użyciu wersji testowej moż na wykonać kod krok po kroku, sprawdzając dane w programie. Druga wersja to wersja osta teczna, która nie zawiera żadnych informacji pomocnych w debugowaniu i kompilowana jest z włączoną opcją optymalizacji, dzięki czemu otrzymujemy możliwie najbardziej wydaj ny moduł wykonywalny. Te dwie konfiguracje wystarczą nam na potrzeby tego kursu, ale jeżeli chcemy dodać inne konfiguracje dla aplikacji, to można to zrobić za pomocą opcji Configuration Manager znajdującej się w menu Build. Zauważ, że element ten nie jest widoczny, jeżeli nie ma załadowanego żadnego projektu. Oczywiście, nie jest to żadnym problemem, ale może być mylące, jeśli tylko przeglądamy interfejs programu, aby zobaczyć, co zawiera. Wyboru konfiguracji, z którą ma pracować nasz program, dokonujemy, zaznaczając odpo wiednią pozycję z listy rozwijanej Active solution configuration znajdującej się w oknie dia logowym Configuration Manager, jak widać na rysunku 1.11. Configuration Ma~ager
Rysunek 1.11
..a.c tlY8 soll tJJn coofgJraoon:
:0
~
~ase
_de--'p--' kJ--' ) ·_
,;Edrt-,=-",~>
-
A _c_ tive _ '_OlJ _tioo --,-p _latfur _ m_'
~r! ~32y .,---------.J
'V
--'---' Platform
WII'132
r1JlR! __ ;u ~
-----,
--"
Buikl
o
c_
Wybierz interesującą Cię konfigurację, a następnie naciśnij przycisk Close. Podczas two rzenia aplikacji będziesz pracować z konfiguracją testową. Po przeprowadzeniu testów przy użyciu konfiguracji debugowania, kiedy program nie sprawia żadnych problemów, zazwyczaj kompiluje się go jako wersję ostateczną. W ten sposób powstaje zoptymalizowany kod bez zbędnych możliwości debugowania i śledzenia, co sprawia, że działa on szybciej i zajmuje mniej pamięci.
Uruchamianie programu skompilowaniu rozwiązania możemy uruchomić nasz program, wciskając klawiszy Ctrl+F5. Powinno ukazać się okno, które zostało zaprezentowane na rysunku 1.12.
Po
pomyślnym
kombinację
48
Visual C++ 2005. Od podstaw
RysUnek 1.12
.. .
••
\H taj ś w Le c I e Aby kun ly n u uu",ć~
Ih' c i ćn i j
d O \.łOl ny
x
_ D
k lrt uis..! .
.
II _
_ _
Jak widać na rysunku, tekst, który znajdował się pomięd zy podwójnymi cudzysłowami, został wypisany w wierszu poleceń . Znaki \ n na końcu ciągu to specjalna sekwencja, zwana znakiem zastępczym, która oznacza znak nowego wiersza. Znaki zastępcze służą do reprezentowa nia w łańcuchach znaków, których nie można bezpośrednio wpisać z klawiatury.
Rm!rImI Tworzenie pustego projektu konsolowego Poprzedni przykład zawiera pewną ilość niepotrzebnego bagażu, który nie jest nam potrzebny podczas pracy z prostymi przykładami w języku C++. Domyślnie wybrana opcja prekom pilowanych nagłówków spowodowała stworzenie w projekcie pliku stdafx .h. Mechanizm ten usprawnia proces kompilacji w przypadku programu składającego się z dużej liczby plików, ale w wielu naszych przykładach jest on niepotrzebny. W takich przypadkach rozpoczynamy od utworzenia nowego projektu , do którego możemy dodać własne pliki źródłowe. Możesz sprawdzić, jak to dzi ała, tworząc nowy projekt w nowym rozwiązaniu dla programu konso lowego Win32 o nazwie Cwl_02. Po wprowadzeniu nazwy projektu i kliknięciu przycisku OK kliknij Applieation Settings po lewej stronie okna dialogowego, które się ukaże. N astęp nie po prawej stronie odszukujemy opcję Empty projeet i zaznaczamy ją, jak pokazano na rysunku 1.13.
Rysunek 1.13
Win32 ~P lic atio n Wizard - Cw1 ~02-
-
-- - m~
-
Application Settings
Overview
App~ cation type:
o l1ijr>Jows
. Add comrr.on heeder nes for:
. ~ic.t ion
o CQ'lsoie.pphcation
eh
O Q.ll
o
~tatic library
Aó1itional Opt IOOS:
o ~mpty proj ect o
t..,rLlft ..
er"'" c'
"lot
(j,-
I <:~relllous I
.I
f01ish
II
C. ncel
I
Po kliknięciu przycisku Finish, podobnie jak poprzednio, utworzony zostanie nowy projekt, ale bez ż adnych plików źródłowych.
Rozdzial1. • Programowanie przy użyciu Visual C++ 2005
49
Następnie
dodajemy nowy plik źródłowy do projektu. Klikamy prawym przyciskiem myszy w panelu Solunon Explorer i wybieramy Add/New !tem z menu kontekstowego. Pojawia się okno dialogowe. Po lewej stronie klikamy opcję Code, a następnie po prawej wybieramy C++ File (.cpp) . Wpisz nazwę pliku Cwl_02, takjak widać na rysunku 1.14. -
-
-
-
-
Add New Item - CwC02 Categorie s :
r'
-- --
-
--
m[g]
"-1
Templates :
Visual C++ Ul
Code Data
Visual Studio installed templates
t:.l C++ File (.epp)
t!!l
Header File (.h)
1!;)Module-Definition File (.def) ~ ln stalle r Clsss
~Midl File (.idl)
'i!l Comp:>nent Class
Resource Web
Utility Proper ty Sheets
_ My T~p~~!~S
.j
Search
I
ontne Templates...
I
L.
l
Creates a file containl '19 CH socrcs eode
Name: Locatlon :
I.
..
I
ICW1.o2/ , I d :\T ransla tJOns\)1elion\) vor HortDns Vlsual CH 2005\,Pr zyklady\Cw l . 02\Cw l . 02
II
Browse., ,
II
Cancel
..
I
Add
I I
Rysunek 1.14 przycisku OK do projektu dodawany jest nowy plik, a jego zawartość zostaje oknie edytora. Plik jest oczywiście pusty, a więc widoczne jest tylko białe pole. W oknie edytora wprowadź do niego n astępującą treś ć :
Po
kliknięciu
wyświetlona w
II Cwl_02.cpp - prosty program konsolowy.
#incl ude
II Podstawowa biblioteka II wejś cia-wyjś cia.
int ma i n() (
st d: :cout st d: :cout st d: :cout ret urn O:
Zauważ, że
« « «
"To j est prosty program. który wy świetla te kst. " « st d .endl : "M oż n a wy św ietl i ć wię c ej lini j ek t ekstu . " « st d: :endl : "powt a r zaj ą c in st r uk cj ę wyj śc i a podob n ą do tej " « st d: :endl .
II Powrót do systemu operacyjnego.
w czasie pisania kodu program automatycznie robi wcięcia. Wcięcia w języku C++ stosowane w celu zwiększenia przejrzystości kodu . Edytor stosuje wcięcie dla każdego wiersza na podstawie zawartości poprzedniego. W czasie wpisywania można także zaobser wować kolorowanie składni. Niektóre elementy programu pokazane są w różnych kolorach, ponieważ edytor automatycznie koloruje elementy języka w zależności od ich przeznaczenia. są
50
Visual C++ 2005. Od podstaw Kod na p ow y ż s zym listingu jest kompletnym programem. Można zauw a ży ć kilka różnic w porównaniu z kodem automaty cznie wygenerowanym przez kreator aplikacji w poprzednim przykładzie . Brakuje dyrektywy #i ncl ude dla pliku stdafx.h. Nie dołączyli śmy tego pliku do naszego projektu , gd yż nie używamy w nim narzęd zia prekompilowanych n agłówków . Tutaj mamy funkcj ę o nazwie ma i n, a w poprzednim przykład z ie była to _ tmai n. W rzeczyw i s tośc i wszystkie programy w C++ ISO/ANSI ro zpoczynaj ą si ę od funkcji main ( ). Microsoft dodał także wersję tej funkcji o nazwie wma i n, która jest u żyw an a przy wykorzystaniu znaków Uni code . Funkcja _ tma i n z o s t ał a zdefin iowana jako ma i n lub wma i n w zależn o ś c i od tego, czy w programie będ ą u żywane znaki Unicode. W poprzednim p rzykładzie funkcja _t ma in została niejawnie zdefini owana jako ma in. Funkcj i o nazwie mai n używamy we wszystki ch przykła dach w CH ISO/ANSI. Instrukcje w yj ś c i a st d: :cout
«
są trochę
inne. Pierwsza instrukcja w main( ) to:
"To j est prosty program. który
wy św tet la
tekst . "
«
st d :endl :
W dwóch miej scach pojawia się o perator « i za każd ym razem powoduje on wysłanie wszystkiego, co po nim następuje, do st d: :cout , czyli stand ardowego strumienia w yj ścio wego. Ciąg spomiędzy podwójnych c u d zys ł o w ó w zostaje w y słany najpierw do strumienia, a następnie do st d: :endl, który zdefiniow any jest w bibliotece standardowej jako znak nowego wiersza. Wc ze śniej wewnątrz łań cu ch a jako znaku nowego wiersza użyl i śm y znaku za s tęp czego \ n umieszczonego w cudzysłowach . Poprzednie wyraże n i e moglibyśmy również zap i s ać w następując y sposób: st d cout
«
"To j est prosty program , który
wyświet l e
t ekst .\ n":
Powinienem wyjaśnić , dlaczego powy ższy wiersz jest na przy ciemnionym tle . Ot ó ż , gdy p is zę wiersz kodu, który już wcześniej wid zieli śmy , to um ieszczam go na białym tle. Po wyż szy wiersz jest nowy, a więc o z naczyłe m to poprzez umie szczenie go na ciemniejszym tle . Tak jak poprzednio, możemy teraz s kom p i l ow a ć nasz projekt. Z auważ, że wszystkie otwarte pliki źró d ł o we zo stan ą zapisane automatycznie, jeżeli nie zr obili śmy tego sami w cze śniej . Po pomyślnym skompilowaniu programu w ci śnij kombinację klawi szy Ctrl+F5 w ce lu jego uruchomienia. Pojawi si ę okno podobne do tego, które jest wido czne na rysunku 1.15.
Rysunek 1.15
"
C:\WINDOWS\system32\cmd.exe
~~ ż~:~~y~~1:~ri2~:t~~:j ~~~i~ekY~;~::~: tekst . ~b~tk~~~~~~o~:~~r~:~1 ~ n~Jj~~t: lr.~d~~:~i~~ l ej .
1I1!1E:l
a
D Błędy Jeżeli
wpiszemy kod programu niepoprawnie, to kompilator zgło s i błędy. Aby sprawdzi ć , jak celowo popełnimy jeden błąd w naszym programie. Je żel i jednak udało Ci s i ę już zro b ić j akiś błąd , to mo że sz go wykorzyst ać do wykonan ia tego ćwicze ni a. Przejdź do okna edytora i usuń średnik znaj d uj ący s i ę na ko ń cu przedostatniego wiersza pomiędzy nawiasami to
działa,
Rozdział 1.
• Programowanie przy użyciu Visual C++ 2005
51
klamrowymi (ósmy wiers z). N astępnie skompiluj program ponownie. Panel wyników na dole zawiera teraz n astępujący komunikat o błędzi e:
C2143: synt ax error : mi ssi ng ' : ' before ' retur n' Każdy
komun ikat o błędzi e podc zas kompilacji ma swój numer, które go znaczenie m ożna w dokumentacji . W tym przypadku wiadomo, co s p ow o d owało wy s tąp i e ni e pro blemu . Jednak w wielu przypadkach dokumentacja może o kazać s i ę bardzo pomocna w odna lezieniu źródła problemu . Aby otwo rzyć tę część dokumentacji, która dotyczy naszego błędu , n ale ży klikn ąć wiersz zawie rający numer błędu i nacisnąć FI . Pojawi się nowe okno z dodat kowymi inform acjami na tem at błędu. Możn a to wypróbow ać na naszym błędzie. sp rawdzić
Po poprawieniu błędu możemy ponownie spróbować skompilować projekt. Operacja ta prze biega sprawnie, gd yż definicja projektu ś l edzi status plików s kład aj ącyc h s ię na niego. Pod czas normalnego procesu komp ilacji Visual C++ 2005 ponownie kompiluje tylko te pliki, które zmieniły się od momentu poprzedniej kompilacji. Ozn acza to, że j eśli mamy projekt skł adający się z kilku plików źródłowych i od momentu ostatniej kompilacji projektu zmiany wprowadziliśmy tylko do jednego z nich, to tylko ten jeden plik zostanie ponownie skompi lowany przed pro cesem kon sol idacji mając ym na celu utworzenie nowego pliku .exe. Będ ziemy także omawiać kł ad
pro gramy konsolowe CLR, a zatem projektu konsolowego CLR .
poniżej
przedstawiam przy
~ Tworzenie projektu konsolowego elB N a ci śnij
klawisze Ctrl+Shijt+N, aby pojawiło się okno dialogowe New Project. N astępnie jako typ projektu wybieramy CLR, a j ako szablon - CLR Console Application, j ak pokazano na rysunku 1.16. W polu Name wpisz nazwę Cwl _03. Po klikn ię c iu przycisku OK zostaną utworzone pliki projektu. Dla projektu konsolowego CLR nie ma żadnych opcji , a więc przy użyciu tego szablonu zawsze rozpoczynamy pracę z tym samym zestawem plików. Jeśli chcemy stwo rzy ć pusty projekt (w tej książce nie będziem y tego robić ) , to musimy skorzysta ć z innego specjalnie do tego przygotowanego szablonu. Na rysunku 1.17 w panelu Solu/żon Explorer mamy kilka plików gdy tworzyliśmy projekt konsolowy Win32.
więcej, niż widzieliśmy,
W wirtu alnym folderze Resouree Files zn ajdują się dwa pliki . Plik o rozszerzeniu . ico prze chowuje ikonę apl ikacji, która będzie widoczna po zminim alizowaniu okna programu. Plik o rozszerzeniu .re zapisuje zasoby aplikacj i - w tym przypadku zawiera tylko ikonę. W projekcie znajduje się także plik o nazwie Assemblylnfo.cpp . Każdy progr am CLR składa się z jednej lub większej liczby asemblacji, które stanowią zbiór kodu i zasobów tworzących funkcjonalną jednostkę. Asemblacj a zawiera równ ież dużą ilo ś ć danych dla CLR - specy fikacje używanych typów danych , informacje o wersj i kodu oraz czy do tej asembl acji mogą mie ć dostęp inne asemblacje. Mówiąc krótko, asembl acja jest podst awowym tworzywem do budowy wszystkich programów CLR .
52
Visual C++ 2005. Od podstaw -
-
-
-
-
m~ mI§]
New Project Templates :
Projecl types : - v.sual C++ ATL QR
I
--"isu a! studio installed t empl ates
~, ASP. NE T Web Servk:e
~' ''.j ;; l i mfłi1 m!tiM
General
H ,SQL Server ProjecI
MFC Smart Devlce W n32 .!J Other Languages ." Other Project Types
.ill WJndows Form s Controi Laary
r n Class Librar y 3lCLR Emply Projec l ',J1 Wndows Forms Apphcallon ifll W indows Serv ice
J eżel i kod źród łowy pliku Cwl_03.cpp nie jest widoczny, to kliknij go dwukrot nie w panelu Solulian Explorer. Jego zawartość powin na wy glądać ja k na rysunku I . 18.
Plik ten zawiera tę samą dyre ktywę #i ncl ude co d omy śln y program w natywnym C++, po niew a ż programy CLR u żywają n agłówk ów prekompilowan ych w celu podnie sien ia wy d ajn oś ci . P oni żs zy wiersz jest nowy :
using namespace Syst em:
Rozdzial1. • Programowanie przy użyciu Visual C++ 2005 ... x
/ CWC03.cppt. Start I'ag
cli
(Global Seope)
113 /"- Cwl _0 3 . c p p
~ ' l # lnC IUde ~:
ue i n q
'"
" ssc
:
deź x
g ł o llJny v
="' 1
p La k p r o j ekt.n .
~I
h"
I
n eme ap e ce Sys t em ;
I
-7'8 r n t;
mę i.n
3' 1{
Canso le : :U r::: i t.eL ine ( L"tJ ita j
9' 1); 11 )
1:: 1
53
t e r r evcave c em . : Sc r i ng A> Aa r::: g S ) ś e t ec t e v j r
re turn O;
'-'- - -
..
.... .
2"; ) JI
Rysunek 1.18 Wszystkie narzędzia biblioteki .NET są zdefiniowane w obrębie jakiej ś przestrzeni nazw. Wszystkie standardowe biblioteki, których najprawdopodobniej będziemy używać , należą do przestrzeni nazw System. Ale czym właściwie je st przestrzeń nazw ? Przestrzeń
nazw jest bardzo prostym pojęciem. W kodzie programu oraz w kodzie tworzącym biblioteki .NET znajduje się bardzo dużo rzeczy , które muszą mieć swoje nazwy (typy danych, zmienne, bloki kodu zwane funkcjami). Z tym związany jest jeden problem - łatwo jest czemuś nadać nazwę, która została już użyta w innym miejscu, co prowadzi do nieporozumień . Przestrzenie nazw pomagają uniknąć tego problemu . Wszystkie nazwy w kodzie biblioteki zdefiniowanej w przestrzeni nazw System poprzedzone są przedrostkiem stanowiącym nazwę tej przestrzeni. Tak więc nazwa Str i ng w bibliotece to w rzeczywisto ści System: :String. Oznacza to, że jeżeli przez nieuwagę u żyjemy w nasz ym kodzie nazwy St ri ng, to w celu odwołania się do tej nazwy w bibliotece .NET możem y u ży ć zapisu System: :St ri ng. Dwa dwukropki ( : :) stanowią operator, który nazywany jest operatorem zasięgu. W naszym przypadku operator ten oddziela nazwę przestrzeni nazw Syst emod nazwy typu Stri ng. W przykładach dotyczących natywnego C++ używali śmy go we fragmentach std: :cout oraz std : :endl. W tym przypadku sytuacja wygląda podobnie - std jest nazwą przestrzeni nazw bibliotek natywnego C++, a cout i end l stanowią nazwy zdefiniowane w obrębie tej przestrzeni i reprezentują odpowiednio standardowy strumień wyj ściowy oraz znak nowego wiersza. Zastosowane w przykładzie wyra żenie us i ng namespace pozwala nam używać dowolnych nazw z prze strzeni nazw System, bez konie czności podawania jej nazwy jako przedrostka. Jeżeli spowodowal i śmy konflikt nazw, to problem ten możemy rozwiązać, usuwając wyraże nia usin g namespace i jawnie przyporządkowującnazwę z biblioteki do nazwy przestrzeni nazw. Wię cej na temat przestrzeni nazw dowiesz s i ę w rozdziale 2. Program możemy skompilować i widoczny jest na rysunku 1.19.
uru chomi ć , naci skając
klawisze Ctrl+F5. Rezultat tego
54
Visual C++ 2005. Od podstaw
Rysunek 1.19
Wynik d ział an i a tego kodu jest taki sam jak kodu z pierwszego tekstu odpowiada wiersz : Consol e: :Wr iteLi ne(L"Wit aj
przykładu .
Za
wyświet lenie
ś wi ecie " ) ;
W tym przyp adku w celu wydru kowania w wierszu po lec e ń za warto ś c i c u dzysłowów p o słu żyl iśmy s i ę funk cj ą na leżącą do biblioteki .NET, a więc jes t to odpowie dnik CLR wyraże nia w natyw nym C++ , którego u ży l iś my w przykła dzie Cwl_O l : st d: :cout « Wy raże n i e
"Witaj
ś w t ec ie
vn" :
CLR j est bardziej przejrzyste
niż wyraż eni e
w natywn ym C++.
Ustawianie opcji wVisual C++ 2005 I stn iej ą dwa rodzaj e opcj i, które m o ż e my ustawi ać. Może my ustawi ć opcje narzęd z i Visua l C++ 2005, które mają zastosowanie w każdy m projekcie, lub opcje dotyczące konkretnego proj ektu, o kreś lające sposó b przetwarzania kod u tego proj ektu podczas procesów komp ilacj i i konsolidacji . Opcje ustawia s ię w oknie dialogowy m Options (Tools/Op tions) . Okno to wi doczne j est na rysunku 1.20. Opti~~5
D Always show Err"," List if build f1nishes wl th errors
D Track Active !tem in so lutlon Explorer 0Show advanCedbuild configuratlons
o Always show solut ion 0 s ave new projects w hen created 0warn user when the project Iocation is no t trusted 0 Show OUtput w rnow when bulld star ts DPrompt for symbohc renaming when renamng files
t OK
Rysunek 1.20
II
cancel
Rozdział 1.
Klikając
jeden ze znaków +
• Programowanie przy użyciu Visual C++ 2005
55
znajdujących się
obok elementów w lewym panelu, spowodujemy podtematów. Na rysunku 1.20 widać opcje podtematu General elementu Projects and Solutions . W prawym panelu widoczne są opcje dostępne dla wybra nego podtematu. W tej chwili interesują nas tylko niektóre z nich , ale dobrze jest poświęcić później trochę czasu na zapoznanie się z pozostałymi . Aby wyświetlić pomoc dotyczącą bie żących opcji , należy kliknąć znak zapytania znajdujący się w prawym górnym rogu okna.
wyświetlenie listy dostępnych
Tworząc
nowy projekt, prawdopodobnie chcemy wybrać domyślny katalog, w którym mają przechowywane wszystkie pliki. Możemy tego dokonać za pomocą pierwszej opcji widocznej na rysunku 1.20. Wystarczy wybrać lokalizację, w której chcemy przechowywać wszystkie pliki naszych rozwiązań i projektów . być
Aby ustawić opcje odnoszące się do każdego projektu C++, należy kolejno wybrać Projects and Solutions/VC++ Project Settings w lewym panelu. Można także dostosować ustawienia tylko dla bieżącego projektu, wybierając pozycję Prop erties z głównego menu Project. Ten element menu zawsze zawiera na
początku nazwę bieżącego
projektu .
Tworzenie iuruchamianie programów dla Windowsa Abyśmy
mogli przekonać się, jak proste jest to zadanie, utworzymy teraz dwie działające aplikacje Windows . Najpierw stworzymy program w natywnym C++, wykorzystującym bibliotekę MFC, a następnie aplikację Windows Forms działającą pod kontrolą CLR. Kod tych programów przedyskutujemy trochę później , gdy będziemy już posiadali niezbędną do ich zrozumienia wiedzę. Jednak już teraz przekonasz się, że procesy ich tworzenia są na prawdę proste.
Tworzenie aplikacji MFC Na początek, jeżeli mamy otwarty jakiś projekt (je śli jest otwarty, to na pasku tytułu głównego okna widnieje jego nazwa), to możemy go zamknąć, wybierając z menu File CIose Solution . Można także utworzyć od razu nowy projekt, a wtedy automatycznie zostanie zamknięte bie żące rozwiązanie.
dla Windowsa, należy wybrać New/Project z menu File lub wcisnąć klawiszy Ctrl+Shijt+N, a następnie wybrać typ projektu MFC oraz szablon projektu MFC Application. Projekt nazwiemy Cwl_04, jak pokazano na rysunku 1.21. Aby
utworzyć program
kombinację
Po kliknięciu przycisku OK pojawi się okno dialogowe tworzenia programu Application Wizard. W oknie tym mamy możliwość wyboru spośród wielu właściwości, które chcemy umieścić w naszej aplikacji . Występują one jako elementy listy po prawej stronie okna, jak widać na rysunku 1.22 . Wielu z nich będziemy używać jeszcze wielokrotnie . Wszystkie te opcje możemy w tej chwili zignorować i zatwierdzić domyślne ustawienia. W tym celu klikamy przycisk Finish, aby utworzyć nowy projekt z domyślnymi ustawieniami. Panel Solutton Explorer w oknie IDE wygląda teraz tak , jak pokazano na rysunku 1.23 .
56
Visual C++ 2005. Od podstaw
Iemplates : Visu al St udio installed templa t es
f~ MFC Appl ication
Mi MFC ActiveX Contra I m\ MFC DLL
ił1
~!.-,:.,!,p lat es
S mart Oevlee Win32 Other Lar<juages
Other Pro ject Types
• ' Search Online Ternplates...
A prcject fOr' creabng an applicatoo that uses the Mk:rosoft Fwndation Class Library t:1ame : ~ocatlon :
SolutlOnNaille:
D:\T ranslabons\{lelion\Ivor Hortons Visual C++ 2005'f'rzyklady
I
Cw l _D4
0 Create 'g irectory fOr solution
OK
'l[
Cancel
Rysunek 1.21 Rysunek 1.22 W elco me t o the MFC Applic at ion Wiz ard
'l hese ere
Overvicw Application Type
Compound Oocument Support Dccument Template Str1nQS
Detebese Suppott User Interface
Peetu-es
AdvencedFeetures Genereted
the ct.r'rent prc ject seUings:
• MtJItiple document nerface • No detebese support
• Nocompound document support Cllck Finishfrorn any windowto eccect the rurrent
settoęs .
After you « ee te the crotect, see the prolect's reedrre .txt f~ e for information
ebout the project teetwes end files that ere qenereted .
dasses
Next
Za uważ, że sc howałem zakła d kę
>
II
Firlsh
II
(oneel
Property Manager, kli kając j ą pra wym przyciskiem myszy i wybie rając Hide z menu , które s ię pojawi ł o. Lista utworzonych plik ów jest dosyć dłu ga . Do pisania programów dla Windowsa trzeba mieć bardzo dużo miejsca na dysku twar dym ! Pliki z rozszerzeniem .cpp zawi erają wykonywalny kod w ję zyku C++ , a z rozszerze niem .h kod w C++ składający s ię z defin icji wykorzystywanyc h w kodzie wyko nywa lnym.
Pliki O rozszerzeniu .ico zawierają ikony. Wszystkie pliki zostały pogrupowane i umiesz czone w odpowiednich podkatalogach dla ułatwienia dostępu do nich . Nie są to jednak prawdziwe katalogi i nie zobaczymy ich w folderze projektu na dysku . Jeśli
zajrzymy do foldera rozwiązania Cwl_04 za pomocą eksploratora Windows lub jakie gokolwiek innego programu służącego do przeglądania plików na dysku , to zauważymy , że w sumie zostały wygenerowane 24 pliki. Trzy z nich znajdują się w folderze rozwiązania , 17 w folderze projektu, a pozostałe cztery w podkatalogu folderu projektu o nazwie res. Pliki w tym folderze zawierają zasoby wykorzystywane przez program - takie jak menu oraz iko ny. Wszystkie te pliki zostają utworzone po wpisaniu żąd anej nazwy projektu . Teraz już łatwo zrozumieć , dlaczego tak dobrym pomysłem j est stworzenie oddzielnego folderu dla każdego projektu. Wśród
plików w folderze projektu znajduje się plik o nazwie ReadMe.txt. W nim znajduje s ię przeznaczenia każdego pliku wygenerowanego przez kreator aplikacji MFC . Można go odczytać za pomocą programów Notepad, WordPad lub nawet edytora Visual C++ 2005. Aby obejrzeć go w oknie edytora, należy kliknąć go dwukrotnie w panelu Solutżon Explorer.
wyjaśnienie
Kompilacia iuruchamianie aplikacii MFC Przed uruchomieniem programu należy skompilować kod źródłowy , a następnie moduły pro gramu poddać procesowi konsolidacji. Robi s i ę to w identyczny sposób jak w przypadku programu konsolowego. W celu zaoszczędzen ia na czas ie można użyć kombinacji klawiszy Ctrl+F5, co spowoduje kompila cję oraz uruchomienie pro gramu .
58
Visual C++ 2005. Od podstaw Po kompilacji projektu w oknie Output widzimy, że nie wystąpiły żadne błędy, a program rozpoczyna działanie. Okno wygenerowanego przez nas programu widać na rysunku 1.24.
Rysunek 1.24
na rysunku , wygenerowane zostało w pełni funkcjonalne okno , z menu i paskiem Mimo że sam program nic określonego nie robi (dodanie funkcjonalności należy do programisty), to wszystkie menu działają. Możemy je wypróbować , a nawet utworzyć nowe okna, wybierając opcję New z menu File .
Jak
w idać
narzędzi .
Myślę, iż każdy się ze mną zgodzi , że utworzenie programu dla Windowsa za pomocą kreatora aplikacji MFC nie wymagało zaangażowania zbyt wielu szarych komórek. Więcej wysiłku będzie trzeba włożyć w zrobienie czegoś pożytecznego z wygenerowanym przed chwilą programem, ale też nie będzie to zadanie zbyt trudne. Niektórzy ludzie piszący powa żne-programy dla Windowsa za pomocą staroświeckich metod , bez użyc ia Visual C++ 2005 , przed rozpoczęciem prób musieli przynajmniej na kilka miesięcy przestawiać się na dietę rybną. Dlatego tak wielu programistów miało zwyczaj jadać sushi . Dzięki Visual C++ 2005 nie ma już takiej potrzeby. Chociaż tak naprawdę nigdy nie wiadomo, co czai się za rogiem w świecie technologii programowania. Jeżeli lubisz sushi, to lepiej nie przestawaj go je ść, tak na wszelki wypadek.
Tworzenie aplikacji Windows Forms W tym przypadku skorzystamy z innego kreatora. Tworzymy jeszcze jeden projekt, ale tym razem wybieramy opcję CLR w lewym panelu okna dialogowego New Project oraz szablon Windows Forms Application. Następnie wpisujemy nazwę projektu Cwl _05, jak widać na rysunku 1.25.
w tym przypadku nie mamy żadnych opcji do wyboru, a więc naciskamy OK w celu utworze nia nowego projektu.
Rozdział 1. -
-
--~
--
-
-
-
-
-
-
• Programowanie przy użyciu Visual C++ 2005
---
-
-
--------
New Project Pro ject types :
-
_ visual .~tudio Installed templates W' ASP.NET Web Service ,~C LR Console Applicatlon ij1SQL Server Project .i'iJ Windows Form s Controi Librar y
General MFC
smart Device Win32 iii Olher Languages
iłI Othar Projec t Types
A proJect for creathj
GlClass LIbrar y ::!J QR Empty Projsct .]:lWhc!ows Forms Application .!mWhc! ows Service
~y Temp la t es
. .: Search Online Templa tes...
an applicalion w llh a Wndows user interface
I v! I
: CWl _osl
Name :
Location: SO....tlon :
-ffi~
CJ9j
Telll'lates :
Ci Visual c++ ATL CLR
59
. iD:\ TranslatKJns\helion\Jva
Hor tons vi sus l c+ + 2005\Pr zy k ł ady
~
!creata new Soubon Solu tion Na me:
-
~
__ _:J -, -
Brows e...
I
Cancel
I
0 create drectorv filr solulion
-
---
I
OK
II
Rysunek 1.25 W panelu Solution Explorer, widocznym na rysunku 1.26, pokazano pliki, które zostały wyge nerowan e dla tego projektu.
app.rc Source F iles ej Asserrbly ln fil .cpp c.::'} c w l _OS.cpp cj stdafx.cpp ReadMe.txt
""ilsolutio...
w tym
projekcie mamy znacznie mniej plików - w sumie jest ich tylko 15 (włącznie z plika Jest to spowodowane między innymi tym, że początkowe GUl jest prostsze n iż w aplikacj i w natywnym C++ używającym biblioteki MFC . Aplikacja Windows Fonns nie
mi
rozwiązania) .
60
Visual C++ 2005. Od podslaw ma żadnych menu ani pasków narzędzi i składa s ię tylko z jednego okna . Oczywiście , łatwo te elementy dodać , ale kreator aplikacji Windows Forms nie zakłada z góry, że potrzebujemy ich od samego po czątku. Okno edytora wygląda trochę inaczej, jak
widać
na rysunku 1.27.
Rysunek 1.27 W okn ie edytora zam iast kodu widzimy okno aplikacj i. Jest to spowodowane tym, że pro jektowanie GUl dla Windows Forms zorientowane jest na tryb graficzny , a nie kodowy. Ele menty interfejsu umies zczamy w oknie, przeciągając je lub umi eszczając w odpowiednich miejscach w trybie graficznym, a Visual C++ 2005 automatycznie wstawia potrzebny kod. Naciśn ięcie klawiszy Ctrl+Alt+X lub wybranie z menu Viewopcji Toolbbx spowoduje poja wienie się dodatkowego okna zawierającego listę składników GUl , jak widać na rysunku 1.28. Okno Toolbox przedstawia listę standardowych komponentów, które można dodawać do apli kacji Windows Forms . Możemy na próbę dodać kilka przycisków do okna projektu Cwl_05. W tym celu klikamy Bulion na liście w oknie Toolbox, a następnie klikamy w obszarze klienc kim okna apl ikacji Cwl_05 wyświetlonego w oknie edytora w miejscu, w którym chcemy umieścić nasz przycisk . Można dowolnie dopasować rozmiar przycisku , przeciągając jego krawędzie, oraz przem ieszczać go w dowolne miejsce za pomocą przeciągania . Aby zm ieni ć jego etykietę, wystarczy ją wpisać - spróbuj napisać Sta rt , a następnie naciśnij Enter. Napis na przycisku zmienia się i jednocześnie pojawia się inne okno, pokazujące właściwości tego przycisku. Nie będziemy się teraz nimi zajmować . Wyjaśnię tylko, że są to opcje pozwalające na kontrolę wyglądu przycisku i że za ich pomocą możemy go dostosować do własnych po trzeb. Spróbujm y doda ć jeszcze jeden przycisk z napisem St op. Okno edytora będzie wyglą dało podobnie do tego na rysunku 1.29. W trybie grafi cznym można w dowolnym czasie dodać dowolny komponent GUl, a kod zostanie automatycznie dopasowany. Spróbuj dodać kilka innych komponentów w podobn y sposób jak pierwsze dwa, a następnie skompiluj i uruchom program, wciskając klawisze Ctrl+F5. Po wykonaniu tych czynności aplika cja prezentuje się w pełnej krasie. Mogłoby być jeszcze prościej?
Rozdzial1. • Programowanie pru u2rciu Visual C++ 2005 Rysunek 1.28
Toolbox
61
• -l=l X
',' AlI WIndows fonn.
; ! Common Cootrols II; Pointer
o Button o CheckBox l "O Checked-istBox ~ ComboBox
Podsumowanie W tym rozdziale poznaliśmy podstawy tworzenia różnego typu aplikacji za pomocą Visual CH 2005. Utworzyliśmy i uruchomiliśmy natywne programy konsolowe CLR, a za pomocą kreatorów aplikacji utworzyliśmy oparte na bibliotece MFC programy Windows oraz urucha miany pod kontrolą CLR - Windows Forms.
62
VisIlai C++ 2005. Od podstaw Z tego
rozdziału powinniśmy zapamiętać, że:
•
Common Language Runtime (CLR) to implementacja firmy Microsoft standardu CLI (ang. Common Language Infrastructurei .
•
Platforma .NET składa działające pod CLR.
•
Natywne aplikacje CH pisane
•
Programy napisane w CH/CLI uruchamia
•
Rozwiązanie jest zbiorem rozwiązanie
się
z CLR oraz bibliotek .NET, które są
wspomagają aplikacje
w języku C++ ISO/ANSI. się
za
pomocą
CLR.
jednego lub większej liczby projektów, które razem pewnego problemu związanego z przetwarzaniem danych. składają się
na
tworzą
funkcjonalnąjednostkę
•
Projekt zawiera kod i elementy, które w programie.
•
Asemblacjajest podstawowąjednostką w programach CLR . Wszystkie tego typu programy składają się z co najmniej jednej asemblacji.
Zaczynając
od drugiego rozdziału aż do połowy książki, będziemy tworzyć programy konsolowe. Wszystkie przykłady ilustrujące sposoby użycia elementów języka C++ są uruchamiane przy użyciu aplikacji konsolowych Win32 lub CLR. Do aplikacji opartych na MFC, tworzonych za pomocą kreatora, powrócimy, gdy opanujemy w wystarczającym storiU sekrety języka C++.
2 Dane, zmienne
i działania arytmetyczne
W tym rozdziale nauczysz się podstaw programowania w języku C++. Po jego lekturze na uczysz się pisać proste programy w C++ w tradycyjnej formie: wejście-przetwarzanie-wyjście. Jak już informowałem w poprzednim rozdziale, najpierw opiszę właściwości języka C++ ISO/ANSI, a następnie przejdę do omówienia dodatkowych cech lub różnic w języku C++/CLI. przy użyciu działających przykładów będziemy mieli okazję zdobyć w poruszaniu się po środowisku Visual C++. Dla każdego przykładu w książce, przed przejściem do kompilacji i uruchomienia, należy utworzyć oddzielny projekt. Pamiętaj, że projekty definiowane w tym rozdziale i we wszystkich następnych aż do dziesią tego są aplikacjami konsolowymi .
W trakcie nauki
języka
więcej doświadczenia
W rozdziale tym dowiesz
się :
• Jakajest struktura programu w C++. • Czym są przestrzenie nazw . • Czym są zmienne w C++ . • Jak
definiować
zmienne i stałe w C++.
• O podstawowych operacjach wprowadzania danych z klawiatury oraz danych na ekran. • Jak
wykonywać
obliczenia arytmetyczne.
• Co to jest rzutowanie typów. • Co to jest zasięg zmiennej .
wysyłania
64
Visual C++ 2005. Od podslaw
Struktura programu wC++
Programy działające jako aplikacje konsolowe w Yisual C++ 2005 pobierają dane z wiersza poleceń i do niego też wysyłają wyniki. Aby uniknąć niepotrzebnego grzebania w skompli kowanych mechani zmach tworzenia i zarządzania aplikacją Windows, bez posiadania wiedzy pozwalającej je zrozumieć, wszystkie przykłady mające ułatwić zrozumienie języka C++ będą aplikacjami konsolowymi Win32 lub .NET. Dzięki temu będziem y mogli całkowicie skupić się na języku C++. Po j ego opanowaniu będziemy w stanie poradzić sobie z aplika cjami Windows . Najpierw zobaczymy, jaka jest struktura programu konsolowego. Każdy
program napisany w języku C++ składa się z co najmniej jednej funkcji. W rozdziale l . widzieli śmy przykład programu konsolowego Win3 2, który zawierał tylko jedną funkcję ma t ru ), gdzie main to nazwa tej funkcji . Każdy program w języku C++ ISO/ANSI zawiera funkcję main() i wszystkie programy w C++, bez względu na rozmiar, składają się z kilku funkcji main() , oznaczającej początek programu, oraz pewnej liczby innych funkcji. Funk cja to po prostu blok kodu o unikalnej nazwie, który jest wywoływany w celu wykonania za pomocą jej nazwy. Jak widzieliśmy w rozdziale 1., aplikacja konsolowa Win32 wygenero wana przez kreator aplikacji ma podstawową funkcję o nazwie _tmai n. Nazwa ta może ozna czać zarówno main,jak i wmai n, w zależności od tego , czy program używa znaków Unicode, czy nie. Nazwy wmain i _tmain zostały stworzone przez firmę Microsoft. Nazwa tej funkcji w standardzie CH ISO/ANSI to mai n. Będę jej używał we wszystkich przykładach w języku CH ISO /ANSI. Typowy program komunikujący się za pomocą wiersza taką, jaka została zaprezentowana na rysunku 2.1.
poleceń może posiadać strukturę
Wykonywanie programu przedstawionego na rysunku 2.1 zaczyna się w miejscu rozpoczęcia funkcji main() . Następnie kontrolę przejmuje funkcja input_names (), która zwraca wynik w miejscu znajdującym się bezpośrednio za tym, w którym została wywołana w nat ru ). Następnie w funkcji main() wywoływana jest funkcja sort_names(), a po powrocie kontroli do main() wywoływana jest ostatnia funkcja output_names () . Po zakończeniu wysyłania wy ników na wyjście kontrola wraca z powrotem do funkcji mai n() i program kończy działanie . Oczywiście , różne
programy mogą mieć całkiem inną strukturę, ale ich wykonywanie zawsze rozpoczyna się od funkcji main(). Główną zaletą podziału programu na funkcje jest fakt, że poszczególne jego części można pisać i testować oddzielnie. Drugą zaletą jest możliwość wykorzystania raz napisanych funkcji w innych programach. W bibliotekach C++ znajduje się wiele standardowych funkcji, których można używać we własnych programach. Dzięki nim można zaoszczędzić bardzo dużo pracy. Więcej
na temat tworzenia i
~
używaniafunkcji
dowiesz
się w
rozdziale 5.
Prosly prOgram
Prosty przykład nowy projekt -
pomoże
nam lepiej zrozumieć poszczególne elementy programu. Utwórzmy to szybko zrobić za pomocą kombinacji klawiszy Ctrl+Shift+N.
możemy
Rozdział2.
• Dane, zmienne i działania arytmetyczne
65
Po wywołaniu funkcja zaczyna wykonywanie kodu od pierwszego wiersza void lnpurnamest) [
Wykonywanie programu rozpoczy na się od funkcji mainO
II ... return ;
int mainO )
(
inputjrarnesl):
sort _namesO; void sort jrarnest) output_names();
[ +-
'--
II ...
return O; return ; )
l
Wynik działania funkcji mainO zostaje zwrócony do systemu operacyjnego Wynik działania funkcji zostaje zwrócony za miejscem, w którym została wywołana
void outpurnarnest) [
II ... return ;
l
Rysunek 2.1 Kiedy pojawi się okno dialogowe New Project, widoczne na rysunku 2.2, wybierz typ pro jektu Win32 i szablon (ang. templatey Win32 Console Application . Projekt nazwij Cw2_01. Po kliknięciu przycisku OK ukaże się nowe okno dialogowe, podobne do tego, które widać na rysunku 2.3. Przedstawione w nim są podstawowe informacje na temat tego , co zostanie wygenerowane przez kreator. Kliknięcie Application Settings po lewej stronie okna spowoduje wyświetlenie dodatkowych opcji dla aplikacji Win32 , jak widać na rysunku 2.4. Domyślnie utworzona zostanie aplikacja konsolowa zawierająca plik z funkcją main () , ale my zaczniemy pracę od najbardziej podstawowej struktury projektu. W tym celu zaznaczamy opcję Empty project i klikamy przycisk Finish. Utworzony został nowy projekt, ale nie zawiera on żadnych plików. Jego zawartość można sprawdzić w panelu Solution Explorer, jak widać na rysunku 2.5.
66
Visual C++ 2005. Od podstaw --
-
-
-
-
--
-
(1]1&1
New Project
lm @]
Ta rrolates :
Project types:
r
8 vis ual C++
ATl
Visua! _~tudio
installed templates
~ Win32 Console Applicatlon
, CLR General
31 Wln32 Project
My Templates
MFC Smart Oevice
1 .'
Wln32
Search Onllne TemplalEs..,
I
Othsr Languages
- Oltl er Project ryp ss
I I I
II ..J!
A project for creating a Win32 console application
Name:
"
Location:
-- - - §T rans la t lor~ 'I1e lion \I ,a- HorlOns
SOlution Name:
I c w2_01 _ _
---- - - -
I
,
[ Cw2_01
Visual CH
2005l/'r~ła dy
dl
Browse..,
I
Cancel
I
I o CrealE directDry for solutbn
- - ---- --
l
OK
II
Rysunek 2.2 Rysunek 2.3 Welcome to th e Win32 Applicatlon Wlzard
o vervtew Application Settings
These ere the rurrent oroject s e trinęs : •
Consołe
applicalion
dek Flnish łrom any windOlł\l to ecceot the CLU"ren\':settinQs; , After yOU creete the project, see the prcjecrs readme.t xt filefor inforTl"latiOn aboutthe proe ct feetwes andfi1es thet M e eenereted .
" H.
Pracę
N_xl >
li
FI",sh
I[
Concel
rozpoczniemy od dodania nowego pliku źródłowego do projektu. W tym celu klikam y prawym przyciskiem myszy Source Fi/es w panelu Solution Explorer i wybieramy kolejno Add/New ltem. W tym momencie powinno pojawić się okno dialogowe Add New Item , podob ne do widocznego na rysunku 2.6.
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
67
Rysunek 2.4 Appllcation Settings
Applic~tłon
Addcommon beeder files for:
Apphcation tvpe:
Overview
Settings
o \'1.ind_ appkcalioo o C<,;'1so1e applicatlon
,.
O l
o ~atic hbrary
Ad
o ~""ty
prc ject
~.oPO"t.~
f',.
aho.Jll"
I < Previous I
'j : ~~ ,
Finish
II
Cancel
Rvslmek 2.5 CI Solution 'Cw2_01 ' (l ,]ll [lEli[l
project)
Header Files
Resource Files Source Files
Klikając,
wybieramy szablon C++ File ( cpp) , a następnie wpisujemy nazwę pliku, tak jak to pokazane na rysunku 2.6. Rozszerzenie .cpp zostanie nadane plikowi automatycz nie i nie trzeba go podawać . Nie ma żadnych przeszkód, aby plikowi nadać tę samą nazwę, którą ma projekt. Plik projektu ma rozszerzenie . vcproj i to go będzie odróżniało od pliku
zostało
źródłowego .
Kliknij przycisk Add w celu utworzenia nowego pliku. w panelu edytora w oknie IDE: II Cw2_0/. cpp
~ Modu le-D efinilion File (.def) -!!.ll nstaller Class
.gM dl File (.idl) -!!.l Component Class
Utili tl Proper ty Sheets
My lemplates U Search Online Templates...
I
I I !
t Creates a file contain,'9 CH source code
§~r .J1a me >
Name: Locatlon:
----
Id ;\Tr anslationslj;elon\lvor Herm s Vlsuai CH -
-
I
-
200S\PrzykladY\Cw2_01\CW2_01
-
-
I
I I Browse...
I
II
I
Add
Cancel
Rysunek 2.6 i nt mai n() {
i nt apples. oranges: i nt f ruit: apples = 5: oranges = 6: fr ul t = appl es + oranges: cout cout cout
« « « «
endl : "Mamy me tylko pomar ań c ze .. . v, w sumie mamy " « f ruit « end l :
ret urn O:
II Deklaracja dwóch zmiennych typu ca łko wi t ego II ... i jeszcze jedna zmienna typu całkowitego. II Ustawi anie wartośc i początko wych. II Obli czanie sumy wszystkich owocó w. II Rozpoczęc ie « end l
wysyłania
od nowego wiersza.
owoców." : II
Wysłan ie
znaku nowego wiers za.
II Konie c progr amu .
Powyższy przykład
ma na celu pokazanie kilku sposobów pisania instrukcji w języku C+ +, ale nie stanowi wzoru dobrego stylu programowania. że
nasz plik ma rozszerzenie .cpp, to zostanie on zidentyfikowany jako plik z kodem C++, zaś znajdujące się w nim słowa kluczowe rozpoznane przez edytor będą prz edstawione w odpowiednich kolorach . Można to sprawdzić, wpisując I nt zamiast in t . Int nie zostanie nadany żaden kolor stosowany dla słów kluczowych.
Jako
źródłowym
W panelu Solution Explorer naszego projektu widzimy nowo utworzony plik. W panelu tym zawsze pokazywane są wszystkie pliki projektu . Kliknięcie zakładki Class View w dolnej czę ści panelu Solulion Explorer spowoduje pojawienie się panelu Class View. Jest on podzielony
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
69
na dwie części - górną, która zawiera funkcje globalne i makra projektu (oraz klasy, kiedy dojdziemy do zagadnień związanych z ich tworzeniem), oraz dolną, w tej chwili jeszcze pustą. Funkcja ma i n() pojawi się w dolnym panelu, jeśli w górnym wybierzemy Global Functions and Variables. Widać to na rysunku 2.7. Więcej na ten temat powiem trochę później, a na razie zapamiętajmy, że globalne mogą być funkcje i (lub) zmienne, zaś ich właściwościąjest to, że dostęp do nich można uzyskać z dowolnego miejsca w programie.
Rysunek 2.7
Class Vlew
.
;-
~
-
... li- x
.
~
~ O ~
<Search>
""
.]1
Cw2 01
"MSm'.i@"Mj,\·Im'm Macrosand Constants
'-
' ,
,
"
• ..
_ .-
._
..
.~ mai1(void)
--
~ So lutJon E
1 ę] <;_,?_s ~ _ ."",-~~~\~~J~IRe source .,
Program można skompilować i poddać konsolidacji na trzy sposoby. Można wybrać z menu Build polecenie Build Cw2_01, nacisnąć klawisz F7lub wybrać odpowiedni przycisk z paska narzędzi (aby sprawdzić, do czego służy dany przycisk na pasku narzędzi, wystarczy najechać na niego kursorem i chwilę odczekać). Jeżeli operacja kompilacji' zakończyła się powodze niem, to program możemy uruchomić, wciskając klawisze Ctrl+F5 lub wybierając polecenie Start Without Debugging z menu Debug. W oknie wiersza poleceń powinniśmy zobaczyć następujący rezultat:
Mamy nie tylko pomarańcze
- w sumie mamy 11 owoców.
Aby kontynuować. naciśnij dowolny klawisz.
Pierwsze dwa wiersze zostały utworzone przez program, a ostatni informuje, w jaki sposób można zakończyć jego działanie i zamknąć wiersz poleceń.
Komentarze wprogramie Pierwsze dwa wiersze w kodzie to komentarze. Są one ważną częścią każdego programu, ale nie stanowią wykonywalnego kodu - służą tylko jako pomoc podczas jego analizowania. Wszystkie komentarze są ignorowane przez kompilator. Dwa ukośniki występujące jeden po drugim, nieznajdujące się wewnątrz łańcucha znaków (o łańcuchach będziemy jeszcze mó wić), oznaczają, że wszystko, co się za nimi znajduje aż do końca wiersza, jest komentarzem.
l
W języku polskim operacje kompilacji i konsolidacji często określa się jednym słowem "kompilacja" - przyp. tlum.
70
Visual C++ 2005. Od podstaw Niektóre wiersze w programie zawierają zarówno koment arze, jak i instrukcje. Można także używać innego typu komentarzy, których granice określane są przez znaki /* oraz */. Na przy kład pierwszy wiersz programu można było napisać następująco: /*
Cw2 Ol.cpp */
Komentarze rozpoczynające się od znaków / / obejmują tylko to, co znajduje się za nimi w tym samym wierszu . Natomiast komentarze w stylu / * . . . */ obejmują wszystko, co znajdzie się pomiędzy nimi, i mogą obejmować kilka wierszy. Moglibyśmy na przykład napisać : / *
Cw2_01.cpp
Prosty przykład owy program.
*/
Wszystkie cztery wiersze są komentarzem. to możemy ozdobi ć je ramką:
Jeśli
chcemy
wyróżnić
kilka wierszy komentarza,
/ *****************************
* Cw2_01. cpp *
* Prosty
przykład owy
prog ram. *
********** ************** ***** /
Zasada jest taka, że w naszym kodzie zawsze powinniśmy stosować zrozumiałe komentarze. Powinny one być na tyle jasne, żeby inny programista (lub my sami) mógł po jakimś czasie zrozumieć przeznaczenie i sposób działania danego fragmentu kodu .
Dyrektywa #include - pliki naglówkowe Po komentarzach znajduje się dyrektywa #i nel ude:
#i nclude Dyrektywy służą do wydawani a kompilatorowi poleceń - w tym przypadku nakazujemy mu, by przed kompilacją do programu dodał zawartość pliku < iostream > . Plik ten nazywany jest plikiem nagłówkowym, gdyż j est on zazwyczaj dołączany na samym początku programu. W rzeczywistości do bardziej pasuje nazwa nagłówek, ponieważ w standardzie ANSI C++ nagłówek nie musi zawierać się w pliku. Ja jednak będę posługiwał się terminem plik nagłówkowy, ponieważ w Visual C++ 2005 wszystkie nagłówki przechowywane są w plikach . Nagłówek zawiera definicje niezbędne, gdy chcemy używać instrukcji wejścia-wyjścia C++. Gdybyśmy nie umieścili zawartości nagłówka w programie, nie moglibyśmy go skompilować , ponieważ używamy w nim wyrażeń, które są zależne od definicji zawartych w tym pliku. W Visual C++ dostępnych jest wiele plików nagłówkowych , które znacznie rozszerzają nasze możliwości. Będziemy je stopniowo poznawać, w miarę zdo bywania coraz większych umiejętności posługiwania się językiem. Wyrażenie #i nel ude jest jedną z kilku dyrektyw preprocesora. Visual C++ rozpoznaje je i oznacza w oknie edytora kolorem niebieskim . Dyrektywy preprocesora są poleceniami wykonywanymi przez proces wstępnego przetwarzania kompilatora, który ma miejsce przed
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
71
kompilacją
kodu do post aci obiektowej . Dyrektywy preprocesora zazwyczaj w jaki ś sposób na kod, zanim zostanie on skompilowany . Zaczynają się zawsze od znaku #' Kolejne dyrektywy preprocesora będziemy poznawać, gdy zajdzie potrzeba ich u życia. oddziałują
Przestrzenie nazw ideklaracia usino Jak dowiedzieliśmy się w rozdziale l., biblioteka standardowa to obszerny zbiór procedur napisanych w celu wykonywania wielu często spotykanych zadań, a przykładem mogą być obsługa strumienia wejścia-wyjścia czy wykonywanie podstawowych obliczeń arytmetycz nych . Ze względu na fakt, że liczba zarówno tych procedur, jak i innych elementów mających swoje nazwy jest bardzo duża, istnieje ryzyko, że przez przypadek użyjemy do własnych celów nazwy, która została już wcześniej użyta w bibliotece standardowej . Przestrzeń nazw to me chanizm, który pozwala na uniknięcie problemów związanych z duplikacją nazw. Dokonywane jest to poprzez skojarzenie pewnego zbioru nazw, takiego jak na przykład z biblioteki standar dowej , z czymś w rodzaju rodziny nazw, która jest nazwą przestrzeni nazw. Z każdą nazwą zdefiniowaną w kodzie, należącą do jakiejś przestrzeni nazw, skojarzona jest ta przestrzeń nazw. Wszystko, co znajduje się w bibliotece standardowej C++ ISO/ANSI, zde finiowane jest w przestrzeni nazw o nazwie std. A zatem każdy element z tej biblioteki, do którego możemy uzyskać dostęp w naszym programie, ma swoją nazwę, a jako kwalifikator pojawi a się nazwa przestrzeni nazw std. Nazwy eout oraz endl zdefiniowane są w bibliotece standardowej, a więc ich pełne nazwy to st d : :eout oraz st d: :endl - przykład ich działania widzieliśmy w rozdziale l. Dwa dwukropki oddzielające nazwę przestrzeni od nazwy encji stanowią tak zwany operator zasięgu. Ma on także inne zastosowania, które będą jeszcze omawiane. Używanie pełnych nazw w programie zaciemniłoby trochę kod. Dobrze by było, gdybyśmy mogli używać prostych nazw bez kwalifikatora w postaci nazwy przestrzeni nazw st d. Poniższe dwa wiersze naszego programu, które następują po dyrektywie #i nel ude dołączającej plik nagłówkowy , nam to umożliwiają:
using st d: :cout : usi ng std : :endl: Są
to deklaracje using, które informują kompilator, że nazw eout oraz endl z przestrzeni std w programie bez określania nazwy tej przestrzeni. Od tej pory kompilator będzie wiedział, że wszystkie przypadki wystąpienia nazwy eout po pierwszej deklaracji usi ng należą do przestrzeni nazw biblioteki standardowej . Nazwa eout reprezentuje standardowy strumień wyjścia, który domyślnie odpowiada wierszowi poleceń, a endl reprezentuje znak nowego wiersza. będziemy używali
Więcej części
o przestrzeniach nazw oraz o sposobie definiowania tego rozdziału .
własnych
powiemy sobie w dalszej
Funkcja mainO Funkcja mai n() w przykładzie składa się z nagłówka funkcj i definiującego jąjako mai n() oraz wszystkiego, co znajduje się pomiędzy nawiasami klamrowymi ({}). Nawiasy klamrowe zawierają wykonywalne instrukcje funkcji, które łącznie nazywa się jej ciałem.
72
Visual C++ 2005. Od podstaw Jak się przekonamy, wszystkie funkcje składają się z nagłówka ( m i ę dzy innymi) definiujące go nazwę funkcji, po którym następuje ciało funkcji składające się z pewnej liczby instrukcji programu zamkniętych pomiędzy nawiasami klamrowymi. Ciało funkcji może nie zaw ierać żad nyc h instrukcji , wtedy po prostu funkcja nic nie robi. Funkcj a, która nic nie robi, może wyd ać
się
zbyteczna, ale kiedy piszemy duży program, jego na funkcjach , pomijając kod wielu z nich i pozostawiając je z minimalną lub zerową zaw artością. Takie działanie oznacza, że możemy skompilować i uruchomić kompletny program ze wszy stkimi jego funkcjami w do wolnym czasie, a ich kod dodawać stopniowo. kompletną strukturę możemy początkowo odwzorować
Instrukcje programu Instrukcje składające się na ciało funkcj i ma i n() zakończone są znakiem średnika . To właśnie wyznacza koniec instrukcji, a nie koniec wiersza . W związku z tym instruk cję można rozbić na wiele wierszy, je śli dzięki temu kod będzie bardziej przejrzysty. Można także umie ścić kilka instrukcji w jednym wierszu . Instrukcja je st podstawową jednostką identyfikującą czynno ści wykonywane przez program. Mo żna ją porównać ze zdaniem w akapicie, gdzie każde zdanie opisuje j akąś odrębn ą koncepcję , ale zarazem stanowi fragment bardziej ogólnej całości, mając z nią liczne powiązania. Instrukcja jest samodzielnądefinicjąjakiejśczynności, którą ma wykon ać komputer, ale można ją łączyć z innymi instrukcjami w celu zdefiniowania bardziej złożonej czynnoś ci lub operacji liczenia. ś red n i k
Działanie
funkcji jest zawsze określone przez kilka instrukcji, z których każda zakończona Spójrzmy na podany przed chwilą przykładowy kod, aby ogólnie zorientować s i ę w sposobie działania instrukcji i funkcji . Bardziej szczegółowo opisuję wszystkie rodzaje instrukcji w dalszej części tego rozdziału .
jest
średnikiem.
Pierwsza instrukcja w ciele funkcji main () to:
int apples . oranges:
II Deklaracja dwóch zmiennych typu
całkowitego .
Instrukcja ta definiuje dwie zmienne - apples oraz oranges. Zmienna jest nazwanym frag mentem pamięc i komputera, którego możemy używać do przechowywania danych , a instruk cja nadaj ąca nazwę jednej lub większej liczbie zmiennych nazywana jest deklaracją zmien nej. Słowo kluczowe i nt oznacza, że zmienne apples i oranges mogą przechowywać liczby całkowite . Za każdym razem, gdy deklarujemy zmienną w programie, musimy określi ć, jakie go typu dane będzie ona przechowywać , czyli wskazać typ zmiennej. N astępna
instrukcja deklaruje jeszcze jedną zm ienną typu
int fruit:
11...ijeszczejedna zmienna typu
c ałko w i te g o
-
frui t :
ca łkowitego.
Mimo że za pomocą jednej instrukcj i można zadeklarować kilka zmiennych, jak zrobiliśmy w naszym przykładzie ze zmiennymi appl es i oranges, to jednak dobrze jest umieszczać wszystkie deklaracje w osobnych wierszach. Pozwala to na dodanie dla każdej z nicb od dzielnego komentarza, w którym możemy wyjaśnić ich przeznaczenie.
Rozdział2.•
Następny
appl es
wiersz kodu przedstawia =
5: oranges
=
6:
Dane. zmienne i działania arytmetyczne
73
się następująco :
II Ustawianie wartości początko wych .
Wiersz ten zawiera dwie instrukcje , każda z nich zakończona jest ś re d n i k i e m . Napisałem tutaj tak , a by pok azać, że kilka instrukcji może wystąpi ć w jednym wierszu . Mimo że n ie jest to obowiązkowe , to do zasad dobrego stylu programowania n ależy umieszczanie tylko jednej instrukcji w wierszu, ponieważ zwiększa to czytelno ś ć kodu. Dobrą praktyką programistyczną jest pisanie kodu w taki sposób, aby był on łatwy do odczytania i s twa rzał jak najmniej okazj i do popełnien i a błędu . Dwie instrukcje w poprzednim wierszu przechowują wartoś ci 5 i 6 w zmiennych app l es i ora nges . Instrukcje te nazywamy instrukcjami przypisania , poniew a ż przypisują one nowe wartości do zmiennych. Zn ak = jest operatorem przypisania. Następna instrukcja
f ruit = apples
+
to :
oranges:
II Obliczanie sumy wszys tkich owoców.
Ta instrukcja jest także instrukcją przypisania, ale trochę inną niż poprzednie, gdyż po prawej stronie operatora przypi sania zawiera dod atkowo wyrażenie arytmetyczne. W yrażenie to dodaje wartości przechowywane przez zm ienne appl es i or anges , zaś wynik zapisuje do zmiennej fru i t . Następne
trzy
wyrażeni a
to:
cout « end l : cout « "Mamy ni e t yl ko « " . w sumi e mamy cout « endl :
II Rozpoczę cie pom a r a ń cze.
« f ruit « "
«
wysyłania
w nowym wierszu.
endl
OWOCÓ~I . " :
II WysIanie znaku nowego wiersza.
Wszystkie one są instrukcjami wyjściowymi. Pierwsza instrukcja znajduje się tutaj w pierw szym wierszu i wys yła na ekran znak nowego wiersza za pomocą słowa kluczowego endl . W j ę zyk u C++ źró dło wprowadzanych dan ych lub cel wysyłanych danych nazywane są stru mieniem. Nazwa cout określa standardowy strumień wyjściowy, a operator « informuje, że wszystko, co znajduje si ę po jego prawej stronie, ma zostać wysłane do strumienia wyjścio wego - cout . Operator « pokazuje kierunek, w którym mają zostać skierowane dane - od zmiennej lub łańcucha po prawej stronie operatora do wyjścia po lewej. Tak więc w pierwszej instrukcji wartość reprezentowana przez na zwę endl . która oznacza znak nowego wiersza, wysł ana zostanie do strumienia zidentyfikowanego przez cout , zaś dane wysłane do cout są drukowane w wierszu poleceń. Znaczenie nazwy cout i operatora « zdefiniowane jest w pliku nagłówkowym biblioteki stan dardowej < iostream > , który dodaliśmy na sam ym poc zątku do programu za pomo cą dyrek tywy #i nc l ude. cout w bibliotece standardowej jest nazwą, a wię c należy do przestrzeni nazw std. Gdyby nie deklaracja usi ng, to nazwa ta nie zostałaby rozpoznana , chyba że użyl ibyśmy pełnej nazwy z kwalifikatorem - std : :cout, jak już w s p o m i n ałe m wcześn iej . Ze względu na fakt , że cout jest nazwą zarezerwowanądo reprezentowania standardowego strumienia wyj ściowego, nie powinno się jej używać do innych celów, a więc na przykł ad nie można jej stosować jako nazwy zmiennej w programie. Oczywiście używanie tej samej nazwy w odnie sieniu do różn ych rzeczy zwiększa prawdopodobieństwo powstania nieporozumień.
74
Visual C++ 2005. Od podstaw Druga instrukcja
wyjścia
podzielona została na dwa wiersze:
cout « "Mamy nie t yl ko p om a rańcze .. . « "- w sumie mamy " « fruit «
"
« endl
owoców. ":
już mówiłem wcześniej, każdą instrukcję można podzielić
na dowolną liczbę wierszy, czemu kod stanie się bardziej przejrzysty. Koniec instrukcji zawsze oznaczony jest średnikiem, a nie końcem wiersza. Następujące po sobie wiersze są odczytywane i łączone przez kompilator w jedną instrukcję do momentu napotkania średnika oznaczającego koniec instrukcji. Oznacza to, że jeżeli zapomnimy na końcu instrukcji postawić średnik, to kompila tor potraktuje następny wiersz jeszcze jako część tej instrukcji. W rezultacie zazwyczaj po wstaje coś , czego kompilator nie potrafi zrozumieć, więc zgłasza błąd .
Jak
dzięki
Instrukcja ta wysyła łańcuch znaków: ,,Mamy nie tylko pomarańcze ... " do wiersza poleceń , po którym następuje znak nowego wiersza (endl), łańcuch" - w sumie mamy", wartość zmien nej fruit oraz łańcuch "owoców". Nie ma żadnych przeciwwskazań co do łączenia w taki sposób różnych wysyłanych elementów . Instrukcja wykonywana jest od lewej do prawej, a każdy element wysyłany jest do strumienia eout. Należy zauważyć, że każdy element, który ma zostać wysłany do eout , poprzedzony jest operatorem « . Trzecia i zarazem ostatnia instrukcja wyjścia wysyła już tylko znak nowego wiersza. Rezultat wszystkich tych trzech instrukcji oglądamy na ekranie. Ostatnia instrukcja w programie to:
retur n
o:
// Koniec programu.
ona wykonywanie funkcji maint ), która zatrzymuje program. Kontrola powraca do systemu operacyjnego . Bardziej szczegółowo na temat tych instrukcji będziemy jeszcze mówili.
Kończy
Instrukcje w programie wykonywane są w takiej kolejności, w jakiej zostały wpisane , chyba że któraś z nich celowo ten stan zmienia. Instrukcje zmieniające normalną kolejność wykony wania opisane zostały w rozdziale 3.
Riale znaki Terminem białe znaki w C++ określa się wszystkie znaki tabulacji, nowego wiersza , przesu nięcia strony oraz komentarze. Białe znaki rozdzielają poszczególne części instrukcji oraz pozwalają kompilatorowi zorientować się, gdzie kończy się jej jeden element (np. i nt ), a gdzie zaczyna następny. W innych przypadkach białe znaki są ignorowane i nie wywołują żadnego efektu. Spójrzmy na i nt
fruit:
Pomiędzy
poniższą przykładową instrukcję : // ...i jeszcze j edna zmienna typu ca łko wi t ego .
i nt a fr uit musi być co najmniej jeden biały znak, aby kompilator mógł rozróż te dwa elementy. Można oczywiście dodać więcej białych znaków, ale zostaną one zigno rowane . Wszystko, co znajduje się za średnikiem, to białe znaki, a więc są one ignorowane. nić
Rozdział 2.•
Spójrzmy teraz na inny
fr uit
~
apples
+
Dane. zmienne i działania arytmelyczne
75
przykład:
oranges :
II Obliczani e sumy wszystkich owoców.
Pomiędzy
frui t i = oraz = i app l es nie muszą występować żadne białe znaki, choć można je chcemy. Bierze się to stąd, że znak = nie jest znakiem należącym do alfabetu ani cyfrą, a więc kompilator potrafi oddzielić go od otaczających znaków . Podobnie wygląda sytuacja ze znakiem + - nie ma konieczności wstawiania białych znaków w jego sąsiedztwie, ale można to zrobić w celu zwiększenia czytelności kodu.
zastosować,jeżeli
Jak już powiedziałem , białe znaki używane są do rozdzielania elementów instrukcji, w in nych przypadkach są one ignorowane przez kompilator (z wyjątkiem oczywiście łańcuchów znaków w cudzysłowach) . Dzięki temu, aby zwiększyć przejrzystość kodu w swoich progra mach, możemy wstawiać dowolną liczbę białych znaków, tak jak zrobiliśmy wcześniej, roz bijając instrukcję wysyłającą dane na kilka wierszy . Pamiętaj, że w C++ koniec instrukcji oznaczany jest średnikiem.
Bloki instrukcji Kilka instrukcji
można umieścić pomiędzy
nawiasami klamrowymi . To, co powstaje, nazywa blokiem jest ciało funkcji. Taką złożoną instrukcję można traktować jako pojedyncze wyrażenie (o czym przekonamy się przy omawianiu podejmowania decyzji w C++ w rozdziale 3.) W rzeczywistości wszędzie tam , gdzie można wstawić pojedynczą instrukcję, można również wstawić cały blok instrukcji otoczony nawiasami klamrowymi. W konsekwencji wewnątrz bloków mogą znaleźć się inne bloki, które można zagnieżdżać w dowolnej liczbie. się
blokiem instrukcji lub
instrukcją złożoną . Przykładowym
Bloki instrukcji wywierają także poważny wpływ na zmienne, ale więcej na ten temat powiem w dalszej części rozdziału przy okazj omawiania zasięgu zmiennych.
Programy konsolowe generowane automatycznie W poprzednim przykładzie utworzyliśmy pusty projekt bez żadnych plików źródłowych, które później dodaliśmy samodzielnie. Jeśli pozwolimy kreatorowi na wygenerowanie projektu tak jak w rozdziale l., to utworzy w nim kilka plików, których zawartości powinniśmy dokładnie się przyjrzeć. Utwórzmy nowy projekt konsolowy Win32 o nazwie Cw2_01A i tym razem pozwólmy kreatorowi działać, nie zmieniając żadnych ustawień. Projekt zawiera trzy pliki z kodem: pliki źródłowe Cw2_0IA.cpp i stdafx.cpp oraz plik nagłówkowy stdafx.h, Pliki te dostarczają podstawowych narzędzi, których możemy potrzebować w programie konsolowym, i składają się na działający program, który w obecnym stanie nic nie robi . Jeżeli mamy otwarty jakiś projekt, to możemy go zamknąć, wybierając z menu File polecenie C/os e Solution. Możemy także utworzyć nowy projekt, a wtedy bieżący zostanie automatycznie zamknięty, chyba że zechcemy go dodać do tego samego rozwiązania. Najpierw przyjrzymy
się zawartości pliku
Cw2_0IA:
76
Visual C++ 2005. Od podstaw #include "stdaf x.h" i nt _tmai n(i nt argc . _TCHAR* argv[] )
{
retu rn O;
Zawartoś ć
tego pliku znacznie
#i ncl ude dla pliku
różni się
nagłówkowego
od poprzedniego przykładu. Mamy tutaj dyrektywę stdafx.h, której wcześniej nie było. Program zaczyna się od
funkcji _t ma i nt ), a nie mai nt ). Kreator aplikacji wygenerował plik nagłówkowy stdafx.h jako część projektu. Po otworzen iu tego pliku zobaczymy, że zawiera on dwie dyrektywy #i ncl ude dla plików nagłówkowych biblioteki standardowej stdio.h oraz tehar.h. Plik stdio.h jest starym nagłówkiem obsługują cym stand ardowe operacje wejścia-wyjścia, które były używane przed pojawieniem się obec nego standardu C++ ISO/ANSI. Jego funkcjonalność pokrywa się z nagłówkiem . tehar.h jest nagłówkiem stworzonym przez firmę Microsoft i definiuje funkcje tekstowe. W zasadzie plik stdafx.h służy jako zbiór standardowych, dołączanych do projektu plików systemowych, które dodalibyśmy do projektu za pomocą dyrektywy #i ncl ude. Podczas nauki CH ISO/ANSI nie będziemy potrzebowali żadnego z nagłówków pojawiających si ę w pliku nagłówkowym stdafx.h, co jest jednym z powodów n ieużywania domyślnych ustaw ień gene rowania aplikacji przez kreator. Jak już wspominałem , Visual C++ 2005 wykorzystuje funkcję wmai n( ) w zamian za ma i n() wtedy, gdy tworzony program używa znaków Uni code. Funk cja wmain ( ) została stworzona przez firmę Microsoft i nie należy do standardu C++ ISO/ANSI. W związku z tym n agłówek tchar.h definiuje nazwę _tmain, dzięki czemu jest ona zastępowana przez main, ale jeżeli sym bol_UNICODE został zdefiniowany, to zostanie zamieniona na wma in. Aby więc poinformować , że program korzysta ze znaków UNICODE, należy dodać następującą instrukcję na początku pliku nagłówkowego stdafx.h :
#define UNI COOE Po tych wszystkich
wyjaśnieniach będziemy
main O we wszystkich
przykładach w
konsekwentnie trzymali C++ ISO/ANSI.
się
stosowania funkcji
Deliniowanie zmiennych Podstawowym celem ws zystkich programów komputerowych jest przetwarzanie danych i zwracanie wyników . Podstawowym elementem tego procesu j est posiadanie fragmentu pamięci, któremu możemy nadać wybraną nazwę, do którego możemy się odwoływać przy u życiu tej nazwy oraz w którym możemy przechowywać dane . Każdy fragment pamięci o takich wła ś ciwościach nazywa się zmienną. Jak już wiemy, każda zmienna przechowuje dane określonego typu , który ustalamy podczas jej definiowania w programie. Zmienna może zostać zdefiniowana do przechowywania tylko liczb całkow itych (to znaczy danych typu i nt eger) i nie można jej używ ać do liczb ułam
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
77
kowych. Wartość przechowywana przez zmienną w danym momencie zależna jest od instruk cji w programie i zazwyczaj zmienia się wielokrotnie w czasie wykonywania przez program obliczeń. Poniżej opisuję
zasady nadawania nazw zmiennym wprowadzanym do programu.
Zasady nadawania nazw zmiennym Nazwa nadana zmiennej to jej identyfikator lub po prostu nazwa zmiennej . W nazwach zmiennych można używać wielkich i małych liter alfabetu, cyfr oraz znaku podkreślenia. Żadne inne znaki nie są dozwolone i w przypadku niezastosowania się do tych reguł podczas kompilacji zazwyczaj otrzymamy komunikat o błędzie. Nazwy zmiennych muszą dodatkowo zaczynać się od litery lub znaku podkreślenia. Najlepiej , aby nazwa odzwierciedlała rodzaj przechowywanych informacji. Ze względu na fakt , że w Visual C++ 2005 nazwy zmiennych mogą mieć długość do 2048 znaków, mamy dość duże pole manewru. Poza zmiennymi, w C++ nazwy można nadawać także innym elementom, które podlegają takim samym zasadom nazewniczym co zmienne. Używanie nazwo maksymalnej długości może znacznie utrudni ć odczytywanie kodu progra mu, a ponadto ich wpisywanie zabiera dużo czasu (chyba że wyjątkowo sprawnie posługuje my się klawiaturą) . Ważniejszym powodem do niestosowania tak długich nazw jest fakt, że nie wszystkie kompilatory potrafiąje obsłużyć. Jeśli przewidujemy, że nasz kod będzie kom pilowany także w innych środowiskach, to dobrze jest ograniczyć długość nazw do 31 znaków . Liczba ta, będąc wystarczającą do stworzenia znaczących nazw, jest także bezpieczna, jeśli chodzi o ograniczenia większości kompilatorów. nazwy zmiennych mogą rozpoczynać się od znaku podkreślenia , to lepiej tak nie w ten sposób unikamy ryzyka wystąpienia potencjalnych konfliktów ze standar dowymi zmiennymi systemowymi, które mają taką właśnie formę . Z tego samego powodu powinniśmy także unikać nazw zmiennych rozpoczynających się od dwóch znaków pod Mimo
że
robić , gdyż
kreślenia . Poniżej
zamieszczam
•
cena ,
•
rabat,
•
pKszta1t,
•
wartosc_,
•
LICZNIK.
przykłady dobrych
nazw zmiennych:
Nazwy takie jak 8_Ba11 , 7Up czy 6_pack są niedozwolone. Podobnie jak Cisza ! czy Anna-Maria , chociaż Ann a_Mar i a jest już w porządku. Oczywiście Anna Mari a jest nieprawidłowąnazwą, gdyż spacje w nazwach zmiennych są niedozwolone. Zauważ, że nazwy zmiennych repub l i can i Republ i can są różne , ponieważ wielk ie i małe litery są rozróżniane. Oczywiście w nazwach nie mogą się pojawiać białe znaki, a ich przypadkowe tam umieszczenie spowoduje powstanie dwóch lub więcej nazw zamiast jednej, co z kolei zazwyczaj prowadzi do zgłoszenia błędu przez kompilator.
78
Visual C++ 2005. Od podstaw Często stosowanąkonwencjąjest m ałej .
od
O klasach
piszę
rozpo czynanie nazw klas od wielkiej litery, a nazw zmiennych w rozdziale 8.
Slowa kluczowe wC++ W C++ istnieje pewna liczba zarezerwowanych słów, tzw . słów kluczowych, które mają spe cjalne znaczenie w języku. Edytor Visual C++ 2005 oznacza je specjalnym kolorem - u mnie domyślnym jest niebieski. Jeżeli słowo kluczowe nie zost ało wyróżnione kolorem, to zna czy, że wpisaliśmy je niepoprawnie. Należy pamięta ć , że
w przypadku słów kluczowych , tak jak i wszystkich innych elementów wielkie i małe lite ry. Na przykład program, który omawialiśmy wcześniej , zawierał słowa kluczowe int i ret urn. Gdybyśm y napisali Int lub Return, to nie byłyby to już słow a kluczowe i nie zostałyby one jako takie rozpoznane. W trakcie poznawa nia język a C++ poznamy jeszcze wiele słów klu czowych . Należy zawsze się upewnić , czy nazwy nadawane różnym elementom programu, takim jak np. zmienne, nie są identyczne ze słowami klu czowymi w C++. Pełna lista słów kluczowych używanych w Visual C++ 2005 znajduje s i ę w dodatku A.
języka
C+ +,
rozróżn i ane są
Deklarowanie zmiennych Jak już widzieli śmy , deklaracja zmiennej jest typu. Na przykład :
instrukcją określającą nazwę
zmiennej danego
int val ue: fragment kodu deklaruje zmienną o nazwie val ue, która może przechowywać liczby T yp dan ych , które może przechowywać zmienna va l ue, określony został przez słowo kluczowe i nt (który wła śnie oznacza liczby cał kow i te) . Ze względu na fakt, że i nt jest słowem klu czowym , nie mo żemy go użyć jako nazwy jednej ze swoich zmiennych. Powyższy
całkowite .
Zauważ, że
deklaracja zmiennej zawsze zakonczona j est ś redn ikiem.
jednej dekl aracji można nadać nazwy wielu zmiennym, ale jak już wcześniej jest je deklarować za pomocą pojedynczych instrukcji - każda w od dzielnym wierszu. Czasami będę od tej zasady odstępował, ale tylko w przypadkach, gdy kod zajmowałby zbyt wiele stron.
Za
pomocą
wspominałem, lepiej
dane (na przykład wartość liczby c a łko wi tej ), nie wystarczy tylko nazwy . Mus imy także skojarzyć z nimi fragment pamięc i komputera. Proces ten nazywa się definicją zmiennej . W C++ deklaracja zmiennej stanowi zarazem jej definicję (z wyjątkiem kilku przyp adków, o który ch będziemy jeszcze mówi ć) . W przypadku pojedyn czej instrukcji podajemy nazwę zmiennej i jednocze śnie przypisujemy ją do obszaru pamięci o odpowiednim rozmiarze.
Aby móc
przechowywać
zdefiniować
A zatem instrukcja :
int value:
Rozdział2.•
Dane. zmienne i działania arytmetyczne
79
jest zarówno deklaracją, jak i definicją. Za pomo cą nazwy zadekl arow anej zmiennej va l ue uzyskujemy dostęp do zdefiniowanego fragmentu pamięci komputera, w którym można prze chowywać pojedynczą wartość typu i nt.
Terminu deklaracja używa się, kiedy wprowadzanaj est nazwa do programu wraz z infor o j ej przeznaczeniu. Termin " defini cja " odnosi się natomiast do procesu przydzie lenia tej nazwie obszaru pamięci komputera. Zmienne można deklarowa ć i definiować w jednej instrukcji - jak w powyższym przykładzie. Powodem takiego dokladn ego roz dzielenia terminów " deklaracja " i " definicja" j est fakt, że można spo tkać instrukcje będąc e deklaracjami. ale nie definicjami. macją
Zmienne w programie mu szą być deklarowane w miejscu znajdującym się wcześniej niż ich pierwsze użycie. Dobrą praktyką w C++ jest deklarowanie zmienn ych w pobliżu miejsca ich pierwszego użycia.
Wartość początkowa zmiennei Deklarując zmienną, możn a pod ać
jej wartość początkową. Deklaracja zmiennej z przypisa niem do niej wartości po czątkowej nazyw a się inicjalizacją . Aby zainicjalizować zmienną podczas deklaracji, wystarczy po jej nazwie postawić znak równości, a po nim podać żądaną wartość. Każda z poniższych instrukcji nadaje zmiennym wartość początkową: int valu e = o:
int count ~ 10:
int number ~ 5 :
W tym przypadku wartość początkowa zmiennej va l ue wynosić 10, a zmiennej number - 5.
będzie
O, zmiennej count
W C++ istnieje jeszcze inny spos ób wpisywania początkowej wartości dla zmiennej, zwany notacją funkcjonalną. Zami ast podawać wartość po znaku równości, można ją umieścić w nawiasach okrągłych po nazwie zmiennej . Wszystkie powyższe przykłady można zapisać następująco :
i nt val ue (O) :
i nt count t l u) :
in t number ( 5) :
Jeśli
nie podamy wartoś ci początkowej zmiennej, to zazwyczaj będzie ona przechowywała to, co z os tało po poprzednim programie w przydzielonym jej obszarze pamięci (jest od tej reguły wyjątek, o którym będziemy mówi ć w dalszej części rozdziału). W miarę możliwości powin no się ini cj al izować wszystkie zmienne. Jeśli zmienne przy rozpoczęciu pracy programu mają znane wartości, to ł atwiej jest znaleź ć źródło problemu, gdy wystąpią błędy. Jednej rzeczy możemy by ć pewni - błęd y na pewno wystąpią.
80
Visual C++ 2005. Od podstaw
Podstawowe typy danych
Rodzaj informacji przechowywanych przez zmienną określony jest przez typ danych. Wszyst kie dane i zmienne w programie muszą być określonego typu. Standard C++ ISO/ANSI definiuje pewną liczbę fundamentalnych typów danych określanych za pomocą słów klu czowych. Nazwa "typy fundamentalne" bierze się stąd, że służą one do przechowywania pod stawowych danych komputera - przede wszystkim danych numery cznych , do których wlicza się także litery, ponieważ są one reprezentowane za pomocą kodu numerycznego. Do tej pory widzieliśmy już słowo kluczowe i nt używane do definiowania zmiennych przechowujących liczby całkowite. W C++/CLI dostępne są także podstawowe typy danych, które nie należą do standardu ISO/ANSI - o nich będzie mowa w dalszej części rozdziału. Jedną
z cech języka zorientowanego obiektowo jest możliwość definiowania własnych ty pów danych, o czym przekonamy się później, a ponadto w bibliotekach, które mamy do dys pozycji w Visual C++ 2005, zdefiniowanych jest wiele dodatkowych typów danych. W tej chwili poprzestaniemy na typach danych dostępnych w C++ ISO/ANSI. Typy fundamen talne dzielą się na trzy kategorie: typy przechowujące liczby całkowite , typy przechowujące liczby niecałkowite , zwane liczbami zmiennopozycyjnymi. oraz typ void, który określa pusty zbiór wartości lub brak typu .
Zmienne calkowite Jak już mówiłem, zmienne typu i nteger (całkowite) mogą przechowywać tylko liczby cał kowite . Liczba graczy w meczu jest liczbą całkowitą (przynajmniej na początku gry) . Wiemy już, że zmienne typu całkowitego deklarujemy za pomocą słowa kluczowego i nt. Zmienne tego typu zajmują cztery bajty pamięci komputera i mogą przechowywać zarówno dodatnie, jak i ujemne wartości. Dolny i górny limit wartości typu całkowitego odpowiada minimal nej i maksymalnej liczbie binarnej ze znakiem, która może być rerrezentowana za pomocą 32 bitów. Górna granica dla zmiennej typu całkowitego wynosi 2 3 -l, czyli 2 147483647, a dolna to --{2 31) , czyli -2 147 483 648. Poniżej znajduje się przykładowa definicja zmiennej całkowitej :
int t oeCount = 10; W Visual C++ 2005 istnieje też słowo kluczowe s hort , które również służy do definiowania zmiennych całkowitych, ale zajmujących tylko dwa bajty pamięci. Słowo kluczowe short jest równoznaczne z short in t . Zmienne typu short (całkowite krótkie) można deklarować następująco:
short feetPerPerson = 2; short int feetPerYard = 3; zmienne są tego samego typu, gdyż s hort i s hor t i nt oznaczają to samo. tutaj obu form , aby pokazać sposób ich użycia, ale w praktyce należy zdecydować się na jedną metodę i konsekwentnie ją stosować. Najczęściej wybierana jest krótsza - short.
Obie
powyższe
Użyłem
Rozdział 2.•
W C++
Dane. zmienne i działania aryimetyczne
81
dost ępny
r ównie ż zapisa ć
jest jeszcze jeden typ całk ow ity - long (całkowity długi), który można jako l ong i nt. Poniżej znajduj ą s i ę przykłady deklarowania zmiennych typu
l ong: long bigNumber = lOOOO OOL: long largeValue = OL; Powyższe
instrukcje dekl aruj ą zmienn e bigNumber i l argeValue o wa rtości ac h początko wych 1000000 i O. Litera L znajdująca się na końcu k ażdego literału informuje, że są to liczby cał kowite typu l ong. Można w tym celu również użyć małej litery l, ale jest ona często mylona z cyfrą l. Literały całkowite bez dołączonego L są typu i nt .
W kodzie nie m ożna s toso wać spacji w dużych liczbach. W tekście ale w programie musi to już być 12345.
m ożna napisać
12 345,
Zmienne całkowite zadeklarowane jako l ong w Visual C++ 2005 zajmuj ą cztery bajty pamięci i mogą mieć wartość od - 2 147483 648 do 2 147483 647, czyli mają taki sam zakres wartości jak zmienne typu i nt.
W innych kompilatorach zmienne typ u 70ng (który jest równoznac zny z 70ng i nt) m ogą nie być identyczne z i nt. Należy o tym pamiętać, planując kompilację swoich pro gramów w innych środowiskach. Aby kod był w pełni przenośny, nie m ożna nawet zakładać, że typ i nt zajmuje cztery bajty (np. w starszych 16-bitowych wersjach Visual C++ zmienna typu int z ajm owała tylko dwa bajty).
Znakowe Iypy danych Typ danych char służy do dwóch celów . Określa on jednobitow ą zmienną, której można użyć do przechowywania liczb całko witych z określonego zbioru lub pojedynczego znaku ASCII (ang . American Standard Code for 1nformation Exc hange). Kody znaków ze zbioru ASCII znajduj ą się w dodatku B. Zmienną typu char definiujemy w na stępujący sposób:
char lett er Powyższa
~
'A' ;
nazwie letter i inicjalizuje ją wartością początkową tylko z jednej litery podajemy w pojedynczych cudzysłowach, a łańcuchy znaków w podwójnych. Łańcuch znakó w to szereg wartości typu char zgrupowanych w jeden c i ąg , zwany tablicą. Do tablic i ł ańcuchów znaków wrócimy jeszcze w rozdziale 4. 'A' .
instrukcja deklaruje
zmienną o
Zauważmy, że wartości składające się
Ze wzgl ędu na fakt , że litera " A" w ASCII jest reprezentowan a przez dobrze mogliśmy zapisać n aszą instrukcję następująco :
char let t er = 65 ;
li czbę
65 , to równie
II Równoznaczne z A.
Instrukcja ta spowoduje taki sam rezultat jak poprzednia. Liczby w zmiennych znakowych mogą mieć rozmiar od - 128 do 127.
całkowite
przechowywane
82
Visual C++ 2005. Od podslaw Pamiętaj, ż e
standard C++ ISO/ANSI nie wymaga, aby typ char reprezentował j edno bajtowe liczby całkowite ze znakiem. Czy typ char ma reprezen to wać liczby całkowite ze znakiem, należące do zbioru od -128 do + 127, czy liczby calkowite bez znaku od Odo 255, zależy wyłącznie od twórcy kompilatora. Należy o tym pamiętać podczas przenoszenia kodu C+ + do innego ś ro dowis ka. Typ wchar_t oznacza typ wide character. Zmienne tego typu przechowują dwubajtowe kody znakowe o wartościach z przedziału 0- 65 535. Poniżej znajduje się przykład definicji zmien nej typu wchar_t:
wchar t letter = L' Z':
II Zmienna przechowują c a 16-bit owy kod znak owy.
Powyższa
instrukcja definiuje zmienną 1et t er, która jest zainicjalizowana 16-bitowym kodem litery "Z". Znajduj ąc a s i ę z przodu wielka litera L informuje kompilator, że jest to 16-bitowy kod znakowy.
Do inicjalizacji zmiennych znakowych Ci innyc h typów całkowitych) możem y także używać wartości szesnastkowych, które oczywiśc ie są prostsze w u życ iu, gdy kod y znaków mam y podane w systemie szesnastkowym (heksadecymalnym). Liczba szesnastkowa zapisywana jest przy użyc iu standardowego systemu szesnastkowego - za pomocą cyfr od O do 9 oraz liter A - F (lub a - f) reprezentujących liczby od 10 do 15. Z przodu zaw sze dodaje s i ę znaki Ox (lub OX) w celu odróżnienia od wartości dziesiętnych . A zatem aby u zyskać wynik identyczny z poprzednim, ostatnią instrukcję możemy zapisać następująco:
cha r let t er = Dx4 l :
II Równoznaczne z A.
Nie zapisuj liczb całkowitych z wiodącym zerem. Kompilator potraktuje je jako ósemkowe (base 8) - wartość 065 w systemie dz i esi ętnym b ędzie wynosiła 53.
wartości
Warto także pamiętać , że w systemie Windows XP dostępne j est narzędzie Tablica znaków, które pozwala zlokalizować znaki dowolnego fontu dostępnego na komputerze. Pokazuje kod szesnastkowy danego znaku i podpowiada, jak go wprowadzi ć z klawiatury. Narzędzie to dostępne jest w menu Start/Akcesoria/Narzędz ia systemowe.
Modyfikatory typU integer Zmienne jednego z typów i nt eger - char, i nt , short lub Ionq - domyślnie przechowują warto ś ci całkow ite ze znakiem , a więc można ich używać do przechowywania zarówno warto ści dodatnich, jak i ujemnych. Jest to możliwe , ponieważ zakłada się, że mają one domyślny modyfikator typu si gned. W związku z tym w każdym miejscu, gdzie napisaliśmy i nt lub l onq, równie dobrze mogliśmy napisać si gned i nt lub s i gned l ang. Możemy również określić
przypadku
będzie
signed va lue
~
to
-5:
typ zmiennej za pomocą samego s i gned i nt . Na przykład:
oznaczać
II Równ oznaczne z signed in/o
słowa
kluczowego s i gned. W takim
Rozdzial2. • Dane, zmienne i działania aryimetyczne Zapis taki nie jes t często spotykany i ja osob iście wo lę jest czytelniejszy.
używać
83
i nt, który sprawia , że program
Wartości, które można przechowywać w zmiennej typu cha r, zawierają się w zakre sie od - 128 do + 127, czyli takim samym jak dla zm iennej typu signed char. Poza tym typy char i signed char są różn ymi typami i nie należy zakłada ć , że to to samo. Jeśli masz pewność, że nie będzie konieczności przechowywania wartości ujemnych w danej zmiennej (je śli np. zapisujemy liczbę przejechanych w c i ągu tygodnia kilometrów), to jej typ m ożemy określi ć jako unsigned:
unsi gned lang mileage = OUL: W tym przypadku minimalną wartością zmiennej mi l eage jest zero, a m aksymalną 32_ liczba 4 294 967 295 (czyli 2 1). Porównaj tę li czbę z maksym alną warto ś cią typu signed long - - 2 147483648. Bit, który był użyty do określenia znaku liczby, w typie un si gned jest wykorzystany jako część wartości. W konsekwencji zmienna typu unsigned ma większy zasięg warto ś ci dodatnich, ale nie może przechowywać wartości ujemnych. Zauważ , że do stałych bez znaku dołączana je st litera U(lub u). W powyższym przykładzie dołączyłem także L, aby zaznaczyć, że stała jest typu l onq. Zarówno L, jak i U mogą być pisane wielką lub małą lite rą. Kolejno ś ć nie gra tutaj roli, choć dobrze jest konsekwentnie stosować jeden sposób określania taki ch wartości. Słowa kluczowego unsigned W takim przypadku zmienna Pamiętaj, ż e
można także uży ć będzie typu
samodzielnie do
okre ślenia
typu zm iennej .
unsi gned i nt.
si gned oraz unsi gned to sło wa kluczow e i nie
można używać
ich jako nazw
zmi ennych.
Typ logiczny Zmienne logiczne mogą mieć tylko jedną z dwóch wartości : true lub false. Typem logicznej zmiennej jest bool - nazwany od twórcy algebry boolowskiej, George'a Boole'a. Typ bool zaliczany jest do typów całkowitych. Zmienne logiczne nazywane są także czasami zmiennymi boolowskimi. Wykorzystywane są one do przechowywania wyników testów , które mogą być albo true albo fal se - np . czy jedna wartość jest równa drugiej, czy nie. Nazwę
zmiennej logicznej
można zadeklarować za pomocą następującej
instrukcji:
baal testResult : Oczyw i ście ,
podczas deklaracji
możemy także inicjalizować
zmienne logiczne:
baal calarIsRed = tr ue : Wartości TRUf i FAL5f są powszechnie używane ze zmiennymi o typa ch num erycznych. a w szczególn ośc i int. Jest to pozostałość po czasach, gdy nie było j eszcze implem entacji zmiennych logicznych w C++ i do reprezentacji wartości logicznych wykorzystywane były
84
Vilillal C++ 2005. Od podstaw zmienne typu in t . Wartos ć Ow takim przypadku traktowana jest jako f a lse, a każda inna jako true. Symbole TRUf i FA L5f są wciąż używane w bibliotece MFC. gdzie reprezentują odp owiednio wartos ć niezerową oraz O. Zauważ, że TRUf i FAL5f (pisane wielkimi literami) nie są słowami kluczowymi w C++ - są one tylko symbolami zdefiniowany mi w obrębie biblioleki MFC Należy równieżpamiętać, że TRUf i FAL5f nie są prawidłowymi warto ś ci am i logicznymi. Ni e należy zolem mylić t r ue z TRUf.
Typy zmiennopoZYCyjne Wartości, które nie są liczbami całkowitymi , przechowywane są jako liczby zmiennopozy cyjne. Mogą być one reprezentowane jako wartości dziesiętne (np . 112,5) lub z wykładnikiem (np. l, l25E2), gdzie 'część dziesiętna jest mnożona przez liczbę dziesięć podniesioną do p otęgi podanej po literze "E ". Zatem nasza przykładowa liczba 112,5 to: 1,125'10 2 = 112,5. Stała zmiennopozycyjna musi zawierać kropkę o d d z i e l aj ąc ą ułamek dziesiętny lub wykład nik (albo j edno i drugie). Jeżeli napiszemy li czbę bez żadn ego z tych znaków. otrzymamy wartość całkowitą .
Zmienną zmiennopozycyjną można określić
szym
za
pomocą słowa
kluczowego doub l e, jak w
poniż
przykładzi e:
double in to mm = 25 .4: Zmienna typu doubl e zajmuje osiem bajtów pamięci i przechowuje wartości z dokładno ś c ią do około 15 miejsc Fao przecinku. Zbiór przechowywanych w artoś ci jest znacznie w iększy i wynosi od l,7 xlO- os do 1,7 xl0 30s. Jeżeli
nie planujemy wykonywać obliczeń z dokładnością do 15 miejsc po przecinku i nie potrzebujemy używać tak olbrzymich liczb, to możemy użyć słowa kluczowego f l oat w celu zadeklarowania zmienn ych zmiennopozycyjnych zajmujących tylko cztery bajty. Na przykład :
fl oat pi
=
3.14159f:
Powyższa
instrukcja deklaruje zmienną pi o wartości po czątkowej 3 . 14159f. Litera f na koń cu oznacza, że j est to stała typu fl oat . Gdyby śmy nie wpisali tej litery, to nasza stała byłaby typu doub l e. Zmienne deklarowane jako f loat mają dokładnoś ć do około siedmiu miejsc po prze cinku i przechowują wartości ze zbioru od 3,4x 10-3s do 3,4 x 103s. W standardzie C++ ISO/ANSI został zdefiniowany jeszcze typ zmiennoprzecinkowy l ong double, który w Visual C++ 2005 ma zaimplementowany taki sam zbiór wartości i dokład noś ćjak typ doubl e. W niektórych kompilatorach typ l ong double odpowiada 16-bitowej wartości zm iennopozycyjnej o znacznie większej dokładności i szerszym zakres ie możliwych wartości w porównaniu z typem doubl e.
Rozdzial2. • Dane. zmienne i działania arytmetyczne
85
Podstawowe typy danych wC++ 180/AN81 Poniższ a
tabela zawiera pod sum owanie wszystk ich pod stawowych typów danych C++ ISO/ANSI oraz zakres obsługiwanych przez nie wartości w Visual C++ 2005:
Rozmiar wbajtach
Typ
Zakres wartości
bool
t r ue lub f al se
char
Dom yślnie możn a
taki sam jak dla si gned char : -128 do + 127. Opcjo nalnie char n ad a ć taki sam zakres j ak uns ign ed char.
s i gned char
-128do+127
uns i gned char
Odo 255
wc har t
2
Odo 65 535
short
2
-32 768 do 32 767
unsigned short
2
Odo 65535
i nt
4
-2 147483648 do 2 147483647
unsi gned i nt
4
Odo 4 294 967 295
l ong
4
-2 147 483 648 do 2 147 483 647
unsi gned l ong
4
Odo 4 294 967 295
fl oat
4
±3,4 ' 1O±38 z dokladno ści ą do okola 7 miej sc po przecinku
doubl e
8
± I,7' 1O±308 z
dokł adności ą do około
15 miej sc po prze cinku
l ong doubl e
8
± 1,7'1 O±30 8 z
dokładnoś c ią
15 miejsc po przecinku
do
około
Literał, Do tej pory wielokrotnie już i n icja li z ow ał e m zmienne konkretnym i warto ściami . W C++ ustalone wartoś ci jakiegokolwiek typu nazywają się Iiterałami. Literał to wartość określonego typu, a więc wartoś ci 23,3 .14159,9 .5f oraz true są przykładami odpowiednio literałów typów i nt , doubl e, flo at oraz boa l. Literał "Samuel Beckett " jest przykładem literału będącego łań cuchem znaków (łańcuchom bliżej przyjrzymy się dopiero w rozdziale 4.). W tabeli na następ nej stronie znajduje s ię podsumowanie literałów różny ch typów. Choć
dla
literału
nie
można określi ć
początkowe wartoś ci ,
typu sho rt lub unsi gned short , to kompilator zaakceptuje typu i nt, dla zmiennych tych typów , pod warunkiem się w zakresie typu zmiennej.
które
że wartość literału mieści
Literałów często używa się
są literałami
w program ach do wykonywania obliczeń. Mogą to być na przykład konwer sji, takie jak 12 przy przeliczaniu stóp na cale czy 25,4 przy przeliczaniu cali na milimetry, albo łańcuch stanowiący komunikat o błędzie . Powinno s ię jednak unikać uży wan ia literałów liczbowych w programach, kiedy ich znaczenie nie jest oczywiste. Nie każdy musi wiedzieć, że kiedy używamy liczby 2,54, to odpowiada ona liczbie centymetrów w calu .
wartoś ci
86
Visual C++ 2005. Od podstaw
Typ
Przykład
char, signed char lub unsigned cher
"A", "Z", "8",
wchar t
LilA", L"Z" , L Il8 ", L"*"
int
- 77, 65, 12345, Ox9FE
unsi gned int
IOU,64000U
long
- 77L, 65L, 12345L
unsigned long
5UL, 999999999UL
flo at
3.14f, 34.506f
double
1.414, 2.71828
long double
1.414L,2.71828L
bool
true, false
Iileralu "*"
Lepiej jest zadeklarować zmienną o odpowiedniej stałej wartości - można ją na przykład nazwać InchesToCentimeter s. Za każdym razem, gdy używamy tej zmiennej w kodzie , jej prze znaczenie jest oczywiste. Jak nadać zmiennej stałą wartość, dowiemy się w dalszej części rozdziału.
Deliniowanie synonimów typÓW danych Słowo kluczowe typedef pozwala na zdefiniowanie własnej nazwy dla istniejącego już typu danych. Za jego pomocą standardowy typ l ong i nt możemy nazwać na przykład Bi gOne s:
typedef long i nt Bi gOnes; II Definiowanie nazwy BigOn es jako nazwy typu. Powyższa
temu
instrukcja definiuje Bi gOnes jako dodatkowy określnik dla typu l ong i nt . Dzięk i jako long i nt za pomocą następującej instrukcj i:
zmienną mynum możemy zdefiniować
BigOnes mynum
=
OL : II Definiowanie zmiennej typu long int.
Pomiędzy powyższą deklaracją
Równie dobrze
a tą używającą wbudowanej nazwy typu nie ma
żadnej różnicy .
moglibyśmy napisać:
long i nt mynum = OL; II Defini owan ie zmiennej typu fong int,
Rezultat będzie identyczny. W ten sposób, przy użyciu dwóch okre ślników typu w obrębie jednego programu, można zadeklarować różne zmienne, które będą miały ten sam typ. na fakt, że słowo kluczowe typedef służy do tworzenia synonimów typów, może jest ono niepotrzebne - ale tak nie jest. Później dowiesz się, że odgrywa ono bardzo ważną rolę w upraszczaniu skomplikowanych deklaracji , gdyż umożliwia zdefiniowanie pojedynczej nazwy do reprezentowania zawiłych specyfikacji typu. Sprawia to, że kod staje się o wiele bardziej czytelny.
Ze
względu
się wydawać, że
Rozdział
2.• Dane. zmienne i działania arylmelyczne
87
Zmienne ookreślonych zbiorach wartości Czasami będziesz potrzebować zmiennych mogących przyjmować wartości z okre ślonego zbioru, do których można się odwoływać za pomocą etykiet - na przykład nazwy dni tygo dnia czy miesięcy. W C++ dostępne jest specjalne narzędzie do tego celu, zwane wylicze niem (lub enumeracją) . Weźmy jeden z wymienionych przykładów - zmienna, która potrafi przyjmować odpowiednie wartości w zależności od dnia tygodnia. Jej definicja może wyglą dać następująco:
enumTydzien{pon . wt. sr .
CZW.
pia. sa . nd} tenTydzien:
Powyższa
instrukcja deklaruje typ wyliczeniowy o nazwie Tydzien oraz zmienną t enTydzi en egzemplarzem typu wyliczeniowego Tydz i en, który może przybierać tylko stałe war tości ze zbioru podanego w nawiasach klamrowych . Próba przypisania do tenTydzi en czego kolwiek spoza podanego zbioru spowoduje błąd. Symboliczne nazwy pomiędzy nawiasami nazywają się argumentami typu wyliczeniowego. W rze czywistości każda nazwa dnia zosta nie automatycznie zdefiniowana jako reprezentacja stałej wartości w postaci liczby całkowitej . P ierwsza nazwa na liście , pan, ma wartość O, wt - l itd. będącą
Jedną
ze stałych wyliczeniowych sposób:
można przypisać jako wartość zmiennej
tenTydzi en w
nastę
pujący
t enTydzien = czw: Zauważ, że stałej
wyliczeniowej nie trzeba kwalifikować za pomocą nazwy enumeracji . War zmiennej ten Tydzi en będzie wynosiła 3, ponieważ stałym symbolicznym, zdefiniowanym przez wyliczenie, domyślnie przyp isywane są kolejne wartości typu i nt, rozpoczynając od zera.
tość
Standardowo każdy następny argument jest większy od poprzedniego o jeden . chcemy rozpocząć wyliczanie od wybranej wartości, to możemy napisać:
enum Tydz ien {pan = l . wt . sr .
CZW.
Jeśli
jednak
pia. sa. nd} tenTydzien:
St ałe
wyliczeniowe w powyższym przykładzie będą miały wartości od l do 7. Argumenty nie nawet mieć unikalnych wartości. Możemy na przykład pan i v/t przypisać wartoś ć l za pomocą instrukcji: muszą
enumTydzien {pan ~ l . wt
=
l . sr . czw. pia. sa . nd} t enTydzien:
Jako że zmienna t enTydzi en jest typu i nt , zajmuje cztery bajty kie inne zmienne typu wyliczeniowego. Kiedy mamy już
zdefiniowanąformę
wyliczenia, to
pamięci,
podobnie jak wszyst
możemy zdefiniować inną zmienną:
enum Tydzien nastepnyTydzien : Powyższa
instrukcja definiuje zmienną nastepnyTydzien jako wyl iczenie, które może przy z poprzedniego wyliczenia. Możemy także pominąć słowo kluczowe enum
bierać wartości
i
powyższe wyrażenie zapisać następująco :
Tydzien nastepny t ydzi en:
88
Visual C++ 2005. Od podstaw Jeśli
chcemy, możemy przypisać określone wartości do wszystkich argumentów wyliczenia. na przykł ad zdefiniować takie wyliczenie:
Możemy
enum Interpunk cja {przecinek = ',', wykrzyknik =
'I',
pytajnik = '?'} rzeczy:
W powyższym wyrażeniu dla zmiennej rzeczy zdefiniowaliśmy wartości jako liczbowe ekwi walenty odpowiednich symboli. Z tablicy znaków ASCII w dodatku B wynika, że podane symbole w zapisie dziesiętnym mają odpowiednio kody 44,33 i 63. Jak widać, wartości nie muszą występować w porządku rosnącym. Jeżeli któremuś elementowi nie nadamy żadnej wartości, to zostanie mu nadana o jeden większa od poprzedniej, tak jak w naszym drugim przykładzie z dniami tygodnia. Typ wyliczenia możemy pominąć, jeżeli nie będziemy zmiennych tego typu . Na przykład :
później
potrzebowali
definiować
innych
enum {pon, wt , sr , czw , pia. so. nd} t enTydzien. na st epnyTydzien. zeszlyTydzien; W powyższej instrukcji mamy trzy zmienne, które mogą przyjmować wartości od pan do nd. Jako że typ wyliczenia nie został podany, nie możemy się do niego odwoływać. Zauważ, że dla tego wyliczenia nie możemy już zdefiniować żadnych innych zmiennych, ponieważ nie mogli byśmy powtórzyć definicji. Próba zrobienia tego potraktowana by była jako próba ponownego zdefiniowania wartości od pan do nd, ajest to niedozwolone.
Określanie typU stałych
wyliczeniowych
Stałe wyliczeniowe domyślnie mają typ int, ale możemy określić go w sposób jawny, dodając dwukropek oraz nazwę typu po nazwie wyliczenia w deklaracji. Stałym wyliczeniowym można nadać dowolny typ całkowity ze znakiem lub bez: short, int, 10n9 lub char albo typ logiczny , Dzięki temu wyliczenie sposób:
reprezentujące
dni tygodnia
moglibyśmy zdefiniować
w
następujący
enum Tydzien : char{ poni edzialek, wtorek, sroda , czwa rtek . piate k, sobota, niedzie la}: W tym wyliczeniu stałe będą typu c ha r , a pierwsza z nich będzie miała wartość O. Jednak nadając stałym typ char , prawdopodobnie wolelibyśmy je inicjalizowa ć jawnie w następujący sposób:
enum Tydzi en
char{ poniedzialek=' p' , v~ o r e k ~ · w' . sroda='s ', czwa rtek='c' . pi at e k~ 'p' , sobota='s ' , niedziel a= ' n'} :
W obecnej postaci wartości stałych trochę lepiej odzwierciedlają to, co przechowują, chociaż nie rozróżniają one takich dni jak s roda i sobota czy poniedzialek i piatek. Takie same war tości dla różnych stałych nie stanowią problemu, ale wszystkie nazwy muszą być oczywiście unikalne, Poniżej
mamy
enum Sta te
przykład
wyliczenia ze
bool { On
~
stałymi
true, Off=fal se} :
typu bool :
Rozdział 2.•
Ze
Dane. zmienne i działania arytmetyczne
89
względu
byśmy
na fakt, że On ma wartość początkową true, Off przybierze wartość fa l se. Gdy podali więcej stałych wyliczeniowych, to ich wartości zmieniałyby się na przem ian.
Podstawowe operacje wejścia-wyjścia Na temat operacji wejścia-wyjścia powiemy sobie tylko w odniesieniu do natywnego C++ . Nie jest to takie trudne - wręcz przeciwnie - ale do programowan ia dla Windowsa nie będziemy tej wiedzy potrzebować. Operacje wejścia-wyjścia w C++ skupiają się wokół koncepcji strumienia danych , które możemy wprowadzać do strumienia wyjściowego i pobie ra ć ze strumienia wejściowego. Dowiedzieliśmy się już, że do standardowego strumienia wyj ściowego C++ ISO/ANSI wysyłającego dane do wiersza poleceń na ekranie odwołujemy się za pomocą słowa kluczowego cout . Odpowiadający mu strumień wejściowy przyjmujący dane z klawiatury to ci n.
Wprowadzanie danych zklawiatury Dane z klawiatury przyjmujemy za pomocą strumienia wejściowego ci n przy użyciu operatora pobierania » . Aby pobrać dwie wartości typu i nteqer - numl i num2 - możemy posłużyć się następującą instrukcją:
cin
»
numl
»
num2:
Operator pobierania » wskazuje kierunek, w którym płyną dane - w tym przypadku ze strumienia ci n do każdej z podanych dwóch zmiennych . Wszelkie wiodące białe znaki są pomijane, a pierwsza wartość typu i nt, którą wprowadzimy z klawiatury, zostanie przypisana do zmiennej numl. Dzieje się tak, gdyż instrukcja wejściowa wykonywana jest od lewej do prawej. Wszelkie białe znaki występujące po zmiennej numl zostaną zignorowane, a druga wartość typu całkowitego, którą wprowadzimy z klawiatury, zostanie przypisana do zmiennej num2. Należy jednak pamiętać, że pomiędzy poszczególnymi wartościami musi wystąpić jakaś przerwa, aby można je było odróżnić . Operacja wprowadzania strumienia danych kończy się w momencie naciśnięcia klawisza Enter - program kontynuuje wówczas działanie, przecho dząc do następnego wyrażenia. Oczywiście, jeśli wprowadzimy nieprawidłowe dane, spowo dujemy błąd, ale zakładam, że zawsze wprowadzasz je prawidłowo! Liczby zmiennopozycyjne są wczytywane z klawiatury w taki sam sposób jak liczby cał kowite i można je oczywiście mieszać . Dane ze strumienia wejściowego i operacje automa tycznie radzą sobie ze zmiennymi i danymi jednego z typów podstawowych. Na przykład w poniższych instrukcjach:
int numl = O. num2 ~ o:
double fact or = 0.0:
cin » numl » ulamek » num2 :
ostatni wiersz wczyta liczbę całkowitą do zmiennej numl, następnie liczbę zmiennopozycyjną do zmiennej ul amek oraz liczbę całkowitą do zmiennej num2.
90
Visual C++ 2005. Od podstaw
Wysyłanie
danych do wiersza poleceń
Widzieliśmy już, jak wysyła s ię dane do wiersza poleceń, ale chciałbym do tego zagadnienia jeszcze na chwilę powrócić. Wypisywanie danych na ekranie działa w sposób uzupełniający do wprowadzonych danych . Jak wiemy, strumień wyjściowy nazywa się cout i do przesyłania do niego danych używamy operatora wstawiania «. Operator ten wskazuje również kierunek przepływu danych. Użyliśmy go już do wysłania danych spomiędzy cudzysłowów. Proces wy syłania wartości zmiennej zademonstruję na przykładzie prostego programu.
~ Wysyłanie danych do wiersza poleceń Zakładam, że
potrafisz już utworzyć nowy pusty projekt, dodać do niego nowy plik źródłowy oraz skompilować go do wykonywalnej postaci . Poniżej znajduje się kod, który należy wpisać do pliku źródłowego po utworzeniu projektu o nazwie Cw2_02: IICw2_02.cpp
II Ćwicze n ie wysyłania dany ch na wyjście.
#i ncl ude using std: :cout: using std' :endl: int main() (
5678 : II Rozpocznij w nowym wierszu. II Wyślij dwie wartości. II Zakończ na nowym wierszu . II Wyjdź z programu.
Jak to działa Pierwsza instrukcja w ciele funkcji main () deklaruje i inicjalizuje dwie zmienne: numl i num2. mamy dwie instrukcje wyjścia, z których pierwsza przenosi kursor do nowego wiersza. Jako że instrukcje wyjścia są wykonywane od lewej do prawej, to druga z nich wyświetla wartość zmiennej numl przed wartością zmiennej num2. Następnie
Po skompilowaniu i uruchomieniu tego programu otrzymamy
następujący
wynik:
12345678 Wynik jest poprawny, ale niezbyt pomocny. Pomiędzy poszczególnymi wartościami potrzebna jest co najmniej jedna spacja . Domyślnie strumień wyjściowy tylko wysyła cyfry, nie dodając żadnych spacji oddzielających poszczególne wartości, co umożliwiłoby ich rozróżnienie. W tej postaci nie mamy możliwości określenia, gdzie kończy się jedna liczba, a gdzie zaczyna druga.
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
91
Formatowanie wysylanych danych Problem braku spacji w wynikach można bardzo łatwo rozwiązać, wstawiając dwiema wartościami. Aby tego dokona ć, należy poniższy wiersz:
cout
«
num1
«
num2;
II
Wyślij
dwie
spację pomiędzy
war/oś ci ,
zastąpić następującym:
cout
«
num1
«
' , «
nu m2 :
II
Wyślij
dwie
war/ości.
Oczywiście gdybyśmy
mieli kilka wierszy wyników, które chcemy wyrównać w kolumnach, nie wiemy, z ilu cyfr będzie składać się każda wartość. W takiej sytuacji możemy posłużyć się tak zwanym manipulato rem. Służy on do modyfikowania sposobu obsługi danych wysyłanych do (lub otrzymywa nych ze) strumienia.
to
potrzebowalibyśmy trochę większych możliwości, ponieważ
Manipulatory zdefiniowane są w pliku nagłówkowym , aby więc z niego skorzy stać , musimy dodać odpowiednią dyrektywę #i ncl ude. Teraz skorzystamy z manipulatora set wrn), który spowoduje wyrównanie wysłanych danych do prawej w polu o szerokości n spacji . set w(6) spowoduje zatem, że następna wysłana wartość zostanie zaprezentowana w polu o szerokości sześciu spacji. Sprawdźmy, jak to wygląda w praktyce.
~ Uzywanie manipulatorów Aby
uzyskać pożądany
wynik, możemy
zmienić
nasz program w
następujący
sposób:
II Cw2_03 .cpp
II Ćwiczenie wysyłania danych na wyjście.
#include #include using std: :cout: usi ng std : :endl : usi ng st d: :setw: i nt mainO (
int num1 ~ 1234 , num2 ; 5678: cout « endl : cout « setw(6) « num1 « setw(6J « num2 : cout « end l , return O:
II Wyślij dwie war/ości. II Rozpo cznij w nowym wierszu. II Wyjdź z programu.
Jak to działa Zmiany w stosunku do poprzedniej wersji programu to dodanie dyrektywy #i ncl ude dla pliku nagłówkowego , deklaracja usi ng dla nazwy set w z przestrzeni nazw st d oraz wstawienie manipulatora set w( ) do strumienia wyjściowego przed każdą wartością, dzięki
92
Visual C++ 2005. Od podstaw czemu są one prezentowane w polach o szerokości rezultat z oddzielonymi od siebie dwiema liczbami:
sześciu
spacji. W wyniku otrzymujemy
1234 5678 Zauważmy, że
manipulator setw() działa tylko na pojedyncze wartości wyjściowe występujące po nim w strumieniu. Manipulator musi bezpośrednio poprzedzać każdą wartość, którą chcemy zaprezentować w polu o określonej szerokości w strumieniu wyjściowym. Jeśli użyjemy go tylko raz, to będzie miał zastosowanie tylko do tej wartości, która występuje po nim. Wszystkie pozostałe wartości zostaną wysłane w tradycyjny sposób. Możemy to spraw dzić, usuwając z kodu drugi przykład zastosowania setw(6) oraz jego operator wstawienia. bezpośrednio
KOIlowanie znaków specjalnych Pisząc łańcuch
znaków umieszczony w podwójnych cudzysłowach, możemy do niego wstawić specjalne znaki, zwane symbolami zastępczymi. Ich nazwa wzięła się stąd, że pozwalają one na wstawianie do łańcuchów znaków, których normalnie wstawiać tam nie można, zastępując je odpowiednimi symbolami. Kod znaku rozpoczyna się od znaku lewego ukośnika \, który informuje kompilator, że następny znak należy potraktować w specjalny sposób. Na przykład znak tabulacji ma postać \ t - kompilator rozumie, że litera t reprezentuje tabulację w łańcu chu, a nie prawdziwą literę "t". Spójrzmy na dwie poniższe instrukcje wyjściowe:
cout cout
endl endl
« «
« «
"To są dane wyjściowe."; "\tTo są dane wyjściowe po znaku tabulacji.
Rezultat ich działania
To
są
To
będzie następujący:
dane wyjściowe.
są dane wyjściowe po znaku tabulacji.
Sekwencja \ t w drugim pierwszego tabulatora.
wyrażeniu spowodowała wcięcie wysyłanego
tekstu do pozycji
W rzeczywistości, zamiast używać słowa kluczowego endl, jako znaku nowego wiersza można kodu xn. Tak więc powyższe instrukcje moglibyśmy równie dobrze zapisać w następujący sposób:
użyć
cout cout
« «
"\nTo są dane wyjściowe."; "\n\tTo są dane wyjściowe po znaku tabulacji.
Tabela na następnej stronie zawiera spis najbardziej przydatnych kodów znaków specjalnych. Oczywiście, jeśli
chcemy umieścić w łańcuchu znak lewego ukośnika lub podwójny cudzy jako znak, który ma być jego częścią, to musimy użyć odpowiedniego kodu (symbolu zastępczego). W przeciwnym przypadku lewy ukośnik zostanie zinterpretowany jako początek innego kodu, a podwójny cudzysłów jako wyznacznik końca łańcucha. słów
Kodów znaków specjalnych
można także używać
do inicjalizacji zmiennych typu char. Na
przykład:
char Tab
~
'\t'; II Inicjalizacja za
pomocą
znaku tabulacji.
Rozdział 2.
Kod
Przeznaczenie
\a
odtwar zanie
\n
nowy wiersz
\'
pojedynczy c u d z ys łów
\\
lewy uk o śnik
\b
backspace
\t
znak tabulacji
\"
podwójny c u dzys łów
\7
znak zapytania
Ze
• Dane. zmienne i działania arytmetyczne
93
dźw ię k u
względu
rału
na fakt, że lit er ały są otaczane pojedynczymi c udzysłow am i, w celu podania lite znakowego będ ąc e go pojedynczym cud zy sł owem musimy u ży ć kodu - ' \ ' , .
~ Używanie kodów znaków specjalnych Poni żej
znajduj e się pro gram, w któ rym wyk orzystan o niektóre kody znaków specjalnych z powyższej tabeli: II Cw2_04.cpp
II Używan ie symboli zastępczyc h.
#inc l ude #include usi ng std: :cout ; int ma i n()
{ char newl i ne = " vn " : II Sy mbol zas tępczy znaku nowego wiersza.
cout « newl i ne: II Rozpocznij w nowym wiersz u.
cout « "\ "We\ ' 11 make aur escapes i n sequence\". he sai d . " :
cout « "\ n\ tThe pr ogr am\ ' s over . i t\'s t i me t o make a beep beep. \ a\a ";
cout « newl i ne: II Rozpocznij w nowym wierszu . II Wyjdź z programu. re turn O;
Po skompilowaniu i uruchomien iu tego programu otrzy mamy
następuj ący
rezult at:
-We' l l make aur escapes i n sequence- , he sai d . The program 's over. i t ' s t i me tak e ma ke a beep beep.
Jak lo działa Pierwszy wiers z w funkcji mai n( ) definiuje zm i enn ą newl i ne i inicjal izuje ją za p om ocą zako dowanego znaku nowego wiersza. Dzi ęki temu zamiast s łowa kluczoweg o endl możem y uży wać
newl ine.
94
Visual C++ 2005. Od podstaw Po wysłaniu newl i ne do strumienia cout wysyłamy łańcuch , w którym użyte zostały zako dowane znaki podwójnego i pojedynczego cudzysłowu . Dla pojedynczych cudzysłowów nie musimy tutaj używać kodowania, gdyż ł ańcu ch otoczony je st c ud zysłowa m i podwójnymi, dzięki czemu kompilator rozpozna je j ako zwykłe znaki pojedynczego cudzysłowu, a nie znacznik końca łańcucha. Natomiast dla podwójnych cudzysłowów w tym łańcuchu musimy zastosować kodow anie . Łańcu ch rozpoczyna się od zakodowanego znaku nowego wiersza, po którym następuje zakodowany znak tabulacji , dzięki czemu wy słany wiersz zostanie wcięty . Na końcu łańcucha znajdują się jeszcze dwa zakodowane znaki powodujące odtworzenie dźwięku, a więc po uruchomieniu programu powinniśmy usł yszeć podwójny odgłos z głośnika komputerowego.
Wykonywanie obliczeń wC++ W tej chwili zaczynamy rzeczywiście coś robić z wprowadzanymi danymi . Wiemy już , jak proste operacje wejścia-wyj ś cia. Teraz przejdziemy do częś ci związanej z przetwa rzaniem danych w programie w C++. Prawie ws zystkie zagadnienia związane z liczeniem w C++ są całkowicie intuicyjne, a więc będzie to dla nas bułk a z masłem .
wykonać
Instrukcia przypisania Do tej pory
widzieliśmy już
instrukcje przypisania kilka razy. Taka typowa instrukcja
wygląda
następuj ąco:
calosc = czescl
+
czesc2
+
czesc3:
Instrukcja przypisania pozwala na oblic zenie wartości wyrażenia znajdującego si ę po pra wej stronie znaku równości - w tym przypadku sumy wartości zmiennych czescl, czesc2 i czesc3- i zapisanie wyniku w zmiennej , której nazwę podajemy po lewej stronie - w tym przypadku jest to ca l osc. W tej instrukcji ca l osc je st sumą swoich części i niczym więcej. Zauważ , że
instrukcja jak zawsze zako ńczon a j est ś redn ikiem .
Możemy również tworzyć powtarzające się
a
~
b
=
przypi sania, jak na
przykład :
2:
Powyższa
a, tak
że
instrukcja oznacza przypisanie zmiennej b wartości 2, a następnie wartości b zmiennej na końcu obie zmienne mają tę samą warto ść 2.
Typy Ivalue i rvalue Lvaluc to coś, co odnosi się do adresu w pamięci i nazywa s i ę tak, ponieważ każde wyrażenie , którego wynikiem jest l val ue, może pojawić się po lewej stronie znaku równoś ci (ang. lefi) w instrukcji przypisania. Większość zmiennych j est typu l val ue, ponieważ okre ślają one
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
95
miejsce w pam i ęc i . Jak się jednak później przekonamy, istnieją też zmienne, które nie są tego typu i nie mogą p oj aw i ć się po lewej stronie instrukcji przypisania, ponieważ ich wartości zo stały zdefiniowane j ako stałe . Zm ienne a i b p oj awiaj ące s i ę w poprzedn im przykładz ie są typu l va l ue. Natomiast wynik wyra żen i aa - b nie, po n ieważ nie okre ś la on adres u w p am i ęc i , gdzie warto ść ta mogłaby być przechowywana. Wy nik wyraże nia, który nie jest typu l val ue, nazywany jest r valu e.
Wiedza na temat typu 7va 7ue będzie nam s ię czasami przydawała w różnych częśc iach - często najmniej spodziewanych - a więc warto ją sobie przyswoić.
książki
Działania
arytmetyczne
Podstawowe operatory arytmetyczne , którymi dysponujemy, to operatory dodawan ia, odejmo wania , mnożenia i dzielenia. Reprezentowane są one odpow iednio przez symbole +, - , * oraz l . Sposó b ich działa nia jest dokładn ie taki, jakiego moglibyśmy s ię spodziewać, z wyjątkiem operatora dzielenia, który ma pewne odchylenia od normy , kiedy ma do czynienia ze zm ien nym i całkowitymi lub s tałymi, o czym s ię niebawem przeko namy . Przy użyc iu tych operato rów możemy pisać instrukcje pod obne do po niższej:
placaNet t o
=
godziny * stawk a - potracenia :
Wyrażenie to najpierw p omnoży wartości zmiennych godzi ny i stawka , a n a s tęp n i e od ilo czynu odejmie wartość zmiennej potra ceni a. Operacje mnożenia i dzielenia, jak można się spodziewać, wykonywa ne są przed dodawa niem i odejmowaniem. Na temat kolejności wyko nywania poszczególnych operatorów w wyrażeni ach będziemy jeszcze mówić w dalszej części tego rozdziału . Wyn ik całego działania godzi ny * st awka - potraceni a zostanie zapisany w zmiennej pl acaNetto.
Znak minusa, u żyty w ostat niej instrukcji , ma dwa operandy - odejm uje on wartość swo jego prawego operan du od wartości lewego. Nazywa się to operacją dwój kową, gdyż w grę wchodzą dwie wartości . Znak min usa może być także używany tylko z jednym operandem w celu zmiany znak u wartości, do której został zastosowany. W takim przypadku nazywa s i ę on jednoargumentowym znakiem minus. Po niższe p rzykła dy i l ustrują u życ i e operatora -:
int a =
o:
i nt b = -5: a = - b; II Zmienia znak operandu.
W powyższym przykładzie zmiennej a zostanie przypisana mentowy znak minus zmienia znak wartości operandu b. Zauważ, że
wartość
+5,
p o ni eważ
jednoargu
przypisywanie nie jest tym samym co równania znane nam z lekcj i algebry w szkole Jego celem jest określenie czynności do wyko nania, a nie proste stwierdzenie faktu. Wyraże nie po prawej stron ie operatora przypisania j est obliczane i wynik zostaje zapisany w l va l ue - zazwyczaj jest to zmienna , która znajduje się po lewej stronie . śred n i ej .
Spójrzmy na następuj ąc e
wyrażenie :
96
Visual C++ 2005. Od podstaw liczba = liczba
+
1;
Oznacza ono "dodaj l do bieżącej wartości przechowywanej w zmiennej l i czba, a następnie zapisz rezultat w tej samej zmiennej l i ezba". Jako normalne wyrażenie algebraiczne pozba wione byłoby ono sensu.
~ Ćwiczenie podstawowych działań arytmetycznych Podstawowe operacje arytmetyczne prze ćwiczymy sobie , obliczając , ile standardowych rolek tapety potrzeba nam do wytapetowania pokoju. Program wykonujący takie obliczenia przed stawiony jest na poni ższym listingu: II Cw2_05.cpp
II Oblicza nie liczby rolek po trzebnyc h do wytapetowa nia p okoju.
#include using std; ;cout;
using std : .ci n:
using st d: :endl;
int main() {
double height = 0.0. width double peri met er = O.O;
~
0.0. length
~
0.0;
II Wym iary pokoju.
II Obwód po koju.
const double rol lwidth ~ 53.0 ; const do uble rolllength ~ 10 .0*100.0:
II Szerokość standardowej rolki. II Długoś ć standardow ej rolki.
int st rips_per_roll = O; i nt strips_reqd = O: i nt nro11 s = O:
II Liczba pasków w rołce. II Liczba po trzebnych pasków. II Całko wita liczba rolek.
cout « endl « " Wpro wa d ź cin » height ;
II Przejście do nowego wiersza. wys o kość
pokoju w centymet rach: ".
cout «endl « " W p r owa d ź dł u g o ś ć i ci n » length » width:
II Przejś cie do nowego wiersza . s z e ro k o ś ć
st ri ps_per_roll = rolllength / height : perimeter = 2.0*(lengt h + widt h) ; strips_reqd = perimeter / roll wi dth :
w centymet rach:
u •
II Sprawdzanie liczby pas ków w rolce. II Obliczanie obwodu po koju. II Obliczanie ca łkowit ej liczby II potrzebnych pas ków tap ety. II Oblicza nie liczby rolek.
cout « endl « "Do wyt apetowani a tego pokoju potrzebujesz " « nrol l s « " rolek tapety . " « endl : return O:
Rozdział2.
• Dane. zmienne i działania arytmetyczne
97
Jeśli
nie je ste ś mistrzem klawiatury, to prawdopodobnie podczas pierwszej kompilacji otrzy masz kilka komunikatów o błędach. Po zlikwidowaniu literówek wszystko będzie działało bez zarzutów. Kompilator zgłosi także kilka o strzeżeń , ale nie przejmuj się nimi - upewnia się tylko, że wiemy, co robimy. Powód pojawiania się komunikatu o błędzie za chw il ę wyjaśnię .
Jak to działa Jedną
z rzeczy wyj aśn ij my sobie już na wstępie - nie b iorę odpowiedzialności za ewentualne braki tapety po wyliczeniach za pomocą tego programu! Jak si ę przekonasz, wszystkie błędy w obliczaniu liczby rolek spowodowane są sposobem działania C++ oraz odpadami , których nie da się uniknąć podczas przyklejania tapety - zazwyczaj 50 procent! Instrukcje z powyższego listingu przejrzymy sobie wiersz po wiersz u, zatrzymując się na chwi lę przy bardziej inte resuj ącyc h , nowych lub nawet ekscytujących właściwościach . Instruk cje pojawiające się przed rozpoczęciem funkcji ma in( ) są nam już bardzo dobrze znane, a więc ich tłumaczenie pomijam. Na początek warto powiedzie ć kilka słów na temat układu programu. Po pierwsze, instrukcje zawarte w ciele funkcji mai n() są wcięte , dzięki czemu łatw iej jest ocenić jego rozmiar. Po drugie, różne grupy instrukcji są od siebie oddzie lane pustym wierszem w celu zaznaczenia, że stanow ią grupy spełniające jakieś okre ślone funkcje. Wcinanie instrukcji jest podstawową techniką tworzenia układu kodu programów w C++. Jak się przekonasz, technika ta jest stoso wana w celu wizualnego wyróżnien ia logicznych bloków programu.
Modyfikator const Na samym początku ciała funkcji mai n() znajduje się blok deklaracji zmiennych używanych w programie . Instrukcje te s ą nam j uż bardzo dobrze znane, z wyjątkiem dwóch, które zawie rają pewne nowe właściwo ści : cons t daubl e ro11 width = 53.O: II Szero koś ć s tandardowej rolki. const daubl e ro11 1engt h = 10.0*100. O: II Długoś ć standa rdowej rolki). Każ da z nich rozpoczyna się od nowego s łowa kluczowego canst . Jest to modyfikator typu , który informuje, że zmienne nie tylko s ą typu daubl e, ale również są stałymi. Ze względu na fakt, że poi nformowali śmy komp ilator, że są to stałe, będzie on spraw dzał, czy któraś z in strukcji nie próbuje zmienić ich wartości . Je ś li tak , zgło si kom unikat o błędz ie . Zmienna zadeklarowana jako canst nie je st typu l val ue, a więc nie może występować po lewej stronie operatora przypisania. Możn a
to sp rawdz i ć , dodając w dowolnym miejscu po deklaracj i zmiennej ral l widt h wyrażenie podobne do tego p o n i żej : ra11wi dth
=
O:
Po dodani u tego kodu prog ram u nie b ę d z i e ' e r r o r C3 8 92 : ' r o l l wi d t h '
można skompilować,
a kompi lator
: you ca n n ot a ssig n to a v ariable t ha t
zgłosi błąd
i s c on s t " .
98
ViSUiII C++ 2005. Od podstaw w programie za pomocą sło wa kluczowego const je st bardzo gdy mamy zamiar używ ać ich wielokrotnie. Jednym z powodów, dla których lepiej je st deklarować stał e , niż u żywać c ałej masy literałów , j est fakt, że ich przezna czenie m o ż e nie b yć wystarczająco oczywi ste. Na przykład w a rtość 42 może o d no s ić się do dłu go ści ży c ia, do wsze ch świat a lub do c ze g oś jeszc ze innego, ale użycie st ał ej zmi ennej o nazw ie moj Wi ek o warto ś ci 42 wszystk o wyj aśnia . Drugim pow odem jest fakt , że j eśli zajdzie potrz eba zmienienia wartośc i takiej zmi enn ej, to wystarczy z m ie nić j ej defin i cj ę w pliku źró dłowym , a dok onane zmi any będ ą automatycznie widoczne w całym programie . Techniki tej
Defini owanie
stały ch używanych
u żyte czn e , z właszcza
b ęd ziem y używ a ć doś ć c zęsto.
Wyrażenia
wdeliniciach stałych
S tała
zm ienna rol l ength jest inicjalizow ana za p omoc ą wyrażenia arytmetyczn ego (lO. 0* 100. O). D zi ęki m ożl iwości użyci a wyrażeń aryt metycznych do inicjalizowa nia sta ły ch zmien nych nie musim y samod zielnie o b l i czać ich warto ści . Ponadto mog ą one by ć o wiele łatw iej sze do zrozumienia, tak jak w tym przypadku - wyrażeni e 10m razy 1OOcm jest o wiele j aśni ej sze , niż gdy byś my napi sal i po prostu 1000. Kompilator dokładn i e obl iczy w art o ści stałych . Natom iast gdyby śmy zro bili to sami, to - w za leżnośc i od naszych możliwo ści matema tycznych - istni eje pewien s topień ryzyk a wy stąpien i a pom yłki . Do inicjalizacji zmiennej można użyć dowolnego wyrażenia, włącznie z obiektami const , które Tak wi ęc je żeli j est to przydatne w naszym programi e, powierzchni ę stan dardo wej ro lki tapety m o żem y z adeklarować na stępuj ąc o:
już definiowali śmy.
~s t dou ble rolla rea = rol lwldth*rolllength ;
---~-------------------
Instrukcj a ta musi zostać umieszczona po deklaracji dwóch użytych w niej zmi ennych, ponie wszystkie zmienne pojawi aj ąc e się w wyrażen i u ini cj alizuj ącym stałą zmi enn ą muszą być wcześ niej przedstawione kompilatorowi. w aż
Wprowadzanie danych IJO programu Po deklaracji kilku zmi ennych typu i nt eqer następne cztery instrukcje danych wprowadzan ych za pom ocą klawi atury :
cout « end l « "W p rowadź ci n » helght;
cin
»
pokoju w centymetrach : ". II Przejście do nowego wiersza.
" Wpr owadź d ł ugo ś ć
l engt h
przyjm owani e
II Przejśc ie do nowego wiersza . wysoko ś ć
cout « end l «
ob sługuj ą
»
i
s z ero k ość
w centymetrac h. ".
widt h;
Najpierw wysłali śmy tekst na wyjście, w którym poprosiliśmy o podanie wym aganych danych , a następn i e wczyt ali śm y te dane za p om o cą ci n, który j est standardowy m strum ien iem wej śc iowy m. Jako pierwsz ą pobrali śmy wys oko ś ć pok oju , a następnie jego długo ść i s zerokoś ć. W prog ramie skierowanym do użytku dodaliby śmy j eszcze kontrolę poprawn o ś ci danych oraz sprawdze nie, czy maj ą one se ns, ale do tego mam y j eszcze zbyt mało wiedzy!
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
99
Obliczanie wyniku Liczbę
rolek tapety potrzebną do wytapetowania pokoju o podanych wymiarach obliczamy za instrukcji:
pomocą czterech
str ips_pe r_roll ~ roll lengt h / height ; perimete r ~ 2.0* (lengt h + width ): stri ps_reqd = perimeter / rol lwidth: nrol l s = st rips reqd / stri ps per rol l :
II Sprawdzanie liczby p ask ów w rolce. II Obliczanie obwodu p okoju. II Obliczani e ca łkowi tej liczby II potrzebnych p ask ów tapety . II Obliczanie liczby rolek.
Pierwsza instrukcja oblicza liczbę pasków tapety o długości odpowiadaj ącej wysoko ści pokoju , ze standardowej rolki, dzieląc długość rolki przez wysokość pokoju. A zatem jeżeli pokój ma 2,44 m wysoko ści , to 1000 dzielimy przez 244 , co daje nam wynik w przybli żeniu 4,09 . Jest tutaj jednak pewna rzecz, o której należy pamiętać . Zmienna prze chowująca wynik - str i ps-per -roll - została zadeklarowana jako typ i nt , a wi ęc może przechowywać tylko liczby całkowite. W wyniku tego wszystkie liczby z ułamkiem zostaną zaokrąglone do najbliższej liczby całkowitej , w tym przypadku do 4, i to właśnie ta wartość jest przechowywana w zmiennej . Jest to tak naprawdę taki wynik , jakiego byśmy tutaj chcieli, gdyż mimo że kawałki tapety można powklejać pod oknem lub nad drzwiami, to jednak przy obli czaniu się je pomija.
jaką możemy otrzymać
Konwersj a jednego typu na inny nazywa się rzutowaniem. W naszym przypadku jest to rzutowanie niejawne, gdyż w kodzie nie jest wyraźnie zaznaczone, ż e operacja taka jest wymagana - kompilator musi taką decyzję podjąć samodzielnie. Dwa ostrzeżenia , które otrzymaliśmy podczas kompilacji, informują nas o tym , że podczas konwersji typów mogą zostać utracone pewne dane. Podczas u żywania mechanizmu niejawnego rzutowania trzeba zawsze uważać. Kompilator nie zawsze zgłasza ostrzeżenie w takich przypadkach, a przypisując wartość jednego typu do zmiennej , której typ ma mniejszy zbiór możliwych do przechowywania danych, zawsze tra cimy jakąś ich część . Jeśli z mechanizmu rzutowania niejawnego skorzystamy w programie przypadkowo , to może to być trudnym do odnalezienia błędem. Jeśli nie mo żna takiego przypisania uniknąć, to można poinformow ać o tym kompilator w sposób jawny w celu pokazania, że nie jest to przypadek. Dokonujemy tego za pomocą rzutowania jawnego wartości znajdującej się po prawej stronie przypisania na typ i nt . Dzięki temu nasze wyrażenie wyglądałoby następująco:
stri psper ron
=
st atic_cast(ro l llengt h / height) ; II Oh licz licz b ę paskó w II w rolce.
stat i C_ cast z
wyrażeniem
w nawiasie po prawej stronie jawnie informuje kompilator, chcemy przekonwertować na typ i nt. Mimo że nadal tracimy jej część ułamkową, to kompilator zakłada, że wiemy, co robimy, i nie zgłosi ostrzeżenia. Więcej na temat stati c_cast<>() i innych typów rzutowania jawnego dowiemy się z dalszej części że wartość wyrażenia
rozdziału.
Zwróć uwagę
na sposób obliczania obwodu pokoju w następnej instrukcji. W celu pomnożenia sumy długości i szerokości przez dwa operację dodawania umieściliśmy w nawiasie. Zapew niamy w ten sposób, że dodawanie zostanie wykonane na początku , a następnie wynik zostanie
100
Visual C++ 2005. Od podstaw pomnożony
przez 2.0, dz ięki czemu otrzymamy właściwą wartość obwodu. Nawiasy są potrzebne, gdyż wyrażenia w nich zawarte obliczane są zawsze na początku. Jeśli w nawiasach znajdują się zagnieżdżone nawiasy, to obliczane są one w kolejności od najbardziej wewnętrz nego do najbardziej zewnętrznego . Trzecia instrukcja, obliczająca w ymaganą do pokrycia pokoju liczbę pasków tapety, jest podobna w działaniu do pierwszej. Wynik je st zaokrąglony do najbliższej liczby całkowitej, ponieważ ma być przechowywany w zmiennej całkowitej - st ri psJ eqd. Nie jest to d okładnie to, czego byśmy chcieli . Najlepiej byłoby , aby wartoś ć tę z aokrąglić, ale tego nie potrafimy jeszcze robić . Możesz do tego wróci ć i poprawić to po przeczytaniu następnego rozdziału . Ostatnie wyrażenie arytmetyczne oblic za liczbę potrzebnych rolek, dzieląc liczbę potrzeb nych pasków (i nteger) przez liczbę pasków w rolce (także i nt eger). Jako że dzielimy przez siebie dwie liczby całkowite, wynik także musi być całkowity , a reszta z dzielenia odrzucona . Musielibyśmy tak zrobić, nawet gdyby zmienna nro11 s była typu zmiennopozycyjnego. War tość całkowita będąca rezultatem wyraż enia zostałaby przekonwertowana na typ zmienno pozycyjny przed zapisaniem jej do zmiennej nro11s. Otrzymany wynik jest dokładnie taki sam, jak gdybyśmy zaokrąglili liczbę zmiennopozycyjnądo najbliższej liczby całkowitej . A le to nie jest to, czego my chcemy, awięc jeśli chcemy tego użyć , to musimy to poprawić .
Wyświetlanie Wyniki
wyniku obliczeń są wyświetlane
za
pomocą następującej
instrukcji :
cout « endl « "Do wyt apet owani a tego pokoj u pot rzebujesz " «
«
nrol ls
«
"
rolek tapety . "
endl :
Jest to prosta instrukcja wyjściowa, którą podzieliliśmy na trzy wiersze. Najpierw wysyła znak nowego wiersza, następnie łańcuch znaków : "Do wytapetowania tego pokoju potrzebujes z", potem wstawiona zostaje wartość zmiennej nro11s, a na koniec łańcuch "rolek tapety". Jak wi dać , instrukcje wyjścia w C++ są bardzo proste. Program ret urn
kończy się
po wykonaniu
poniższej
instrukcji:
o:
Wartość zero jest wartością zwracaną i w tym przypadku zostaje ona zwrócona do systemu operacyjnego. Więcej na temat wartości zwracanych dowiesz się w rozdziale 5.
Obliczanie reszty W poprzednim przykładzie zauważyliśmy, że w wyniku dzielenia dwóch liczb całkowitych otrzymuje się wynik z usuniętą resztą z dzielenia. W ten sposób, dzieląc 11 przez 4, otrzy mamy wynik 2. Jako że reszta z dzielenia może być czasami bardzo ważna (kiedy na przykład dzielimy ciastka pomiędzy dzieci), w C++ dostępny jest specjalny operator %. Przy jego użyciu możemy napisać następujące instrukcje, które rozwiążą nasz problem z ciasteczkami:
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
101
int reszta = O. ci ast ka = 19. dZleci = 5: reszt a = ciast ka %dzieci ; Zmienna reszta będzie miała wartość 4 - tyle wynosi reszta z dzielenia 19 przez 5. Aby obli czyć , ile ciastek dostanie każde dziecko, należy posłużyć się zwy kł ym operatorem dzielenia , jak poni żej :
each = ciastka I dzieci:
Modyfikowanie zmiennei Często
zachodzi konieczność zmodyfikowania istniejącej już w artośc i zmiennej (np. trzeba ją lub podwoić) . Aby zwiększyć zmienną o nazwie cou nt, możemy użyć n astępuj ącej instrukcji :
zw i ększyć
count = count + 5; Dodaje ona po prostu 5 do bieżącej wartości przechowy wanej w zmiennej count i wynik zapi suje z powrotem w tej zmiennej . Jeśli więc zmienna count po c z ątkowo miała w arto ść 10, to teraz ma już 15. To samo
wyrażenie możemy także zapisać
w krótszy sposób:
count += 5: Instrukcja ta mówi: .P obierz warto ś ć zmiennej count, dodaj do niej 5 i wynik zapisz z powro tem do tej zmiennej ". W podobny sposób możemy u żywać również innych oper atorów. Na przykł ad :
count *= 5: Instrukcja ta pomnoży bieżącą wartość zmiennej count przez 5 i wynik zapisze z powrotem do tej zmiennej . Ogólnie rzecz bi orąc , instrukcje można pisać według następującego schematu:
1s op= ps; W schemacie tym op oznacza jeden z poniżs zy ch operatorów:
*
+
«
»
&
>
Pi ęć
│
pierwszych z powyższych operatorów już znamy , a pozo stałe (które są operatorami prze i logicznymi) poznamy jeszcze w tym rozdziale. l s oznacza wszystko to, co mo że pojawi ć s ię po lewej stronie instrukcji, i zazwyczaj (chociaż nie zawsze) jest nazwą zmiennej . ps oznacza wszystko , co mo że pojawi ć się po prawej stron ie instrukcji. s unięc ia
Ogólna forma instrukcji jest równoznaczna z l s = l s op (ps);
poniższą:
102
Visual C++ 2005. Od podstaw Dzięki będzie
wstawieniu ps do nawiasu prawym operandem op.
Oznacza to, ż e
wyrażenie
to zostanie obliczone jako pierwsze, a wynik
możemy napi sać taką in strukcję :
a/ = b +c : i jest ona równoznaczna z:
a = a / (b +c) :
A zatem
wartość
a zostanie podzielona przez
sumę
b i c, a wynik z powrotem zapisany do a.
Operatory inkrementacji idekrementacji Wprowadzimy teraz dwa niezwykłe operatory arytmetyczne, zwane operatorami inkremen tacji i dekrementacji. Kiedy zaczniemy na poważne posługiwać się językiem C++, stwier dzimy, że operatory te są niezwykle przydatne. Są to operatory jednoargumentowe, których używamy do zwiększania lub zmniejszania wartości zmiennych przechowujących liczby cał kowite. Zakładając na przykład, że zmienna count je st typu i nt, poni ższe trzy instrukcje są jednoznaczne: count = count + l : count
+~
l ; ++count ;
Każda z nich zwiększa wartość zmiennej caunt o jeden. Ostatnia instrukcja, w której wyko rzystany został operator inkrementacji, jest najbardziej zwięzła.
Operator inkrementacji nie tylko zmienia wartość zmiennej, do której został zastosowany, ale w wyniku jego działania także powstaje wartość. A zatem użycie operatora inkrementacji w celu zwiększenia warto ści zmiennej o jeden może wystąpić jako część bardziej skompli kowanego wyrażenia. Je żeli zwiększamy wartość zmiennej za pomocą operatora ++, jak w ++count , wewnątrz innego wyrażenia, to jej wartość zostanie najpierw zwiększona o jeden, a następnie tak zwięks zona wartość zostanie użyta w dalszych oblic zeniach. Przypuśćmy na przykład, że zmienna caunt ma wartość 5 i że zdefiniowaliśmy zmienną typu i nt o nazwie ta ta l . Napiszmy następującą instrukcję: t otal = ++count + 6; Wynikiem jej
zmiennej cou nt do 6, a więc wartość całego 12 i liczba ta zostanie zapisana jako wartość zmiennej ta t a l .
działania będzie zwiększenie wartości
wyrażenia będzie wynosiła
Do tej pory wstawialiśmy operator inkrementacji przed nazwą zmiennej . Jest to tak zwana forma przedrostkowa tego operatora. Może on również mieć formę przyrostkową, czyli znajdować s i ę po nazwie zmiennej , do której został zastosowany. W zależności od użytej formy efekt jest nieco inny . Wartość zmiennej , do której stosujemy operator inkrementacji, zostanie zwiększona tylko wtedy, gdy została ona użyta w jakimś kontekście . Cofnijmy na przykład wartość zmiennej count z powrotem do 5 i przepiszmy naszą instrukcję w następujący sposób: tot al = count ++ + 6;
Rozdział 2.•
Dane. zmieniłe i działania arytmetyczne
103
Zmiennej t ata l zosta nie przyp isana wartość 11, ponieważ wartoś ć poc zątkowa zmiennej caunt jest tutaj użyta do obliczenia warto ści wyra żenia prze d zwięks zeniem j ej o jeden . Instrukcja tajest równoznaczna z dwiema poniższymi: t ota l = count + 6; ++count : Takie nagromadzenie znaków + jak w naszym przykładzie może prowadzić do ni ep oroz u m i e ń . Og ólnie rzecz biorąc , taki spos ób stosowania operatora inkrementacji jak w poprzednim przykładz ie nie jest zbyt dobry. O wie le lep iej byłoby napi sać : total = 6 + count++: M ając inst rukcję taką jak
a++ + b lub a+++b, moż na się łatwo pogubi ć , co ma zos tać wyko nane lub co zrob i kompi lator. Obie te instrukcje oz naczają to samo, ale w tej drugiej możliwe, że mie liśmy na myśl i a + ++b, co da j uż inny wyn ik - o jeden większy n i ż poprzednie dwie . Dokładnie
takie same zasady jak do operatora inkrementacj i maj ą zastosowanie do operatora Jeżel i na przykład zmienna ca unt ma warto ść początkową 5, to po wyko dekreme ntacj i: naniu instru kcji: -- o
t otal = --count + 6: zmienna t ata l t otal
=
będzie m iał a w artość
10, nato miast instrukcja :
6 + count - -:
na 11. Oba operatory stoso wane są zazwyczaj do liczb całkow ityc h, w szcze w pętlach , o czym przekonamy się j uż w nas tępny m rozdziale. P óźni ej dowiemy s i ę, że opera tory te mogą być stosowane także do innych typów danyc h, a zwłaszcza do zmien nych przec howującyc h adresy.
ustawi
tę wartość
gó l ności
~
Operator przecinkowy
Operator przecinkowy pozwala na podanie kilku wyrażeń w miej scu , gdzie normal nie ty lko j edn o. Najł atwiej jest to zrozumieć na przy kładzie :
p oj aw ić się
II Cw2_06.cpp
II Ćw iczen ie zast osowania opera tora prz ecinka.
#incl ude usi ng st d: .cout : usi ng st d . rendl : int mai n( )
« "j est wa r t o ś ć osta t niego wy raż em a po prawej: "
« num4 :
może
104
Visual C++ 2005. Od podstaw cout
«
end l ;
ret urn O;
Jak to dziala Po skompilowaniu i uruchomieniu tego programu otrzymamy War t o ś c i ą
szeregu
wy r a ż e ń
jest
wa r t o ś ć
następujący rezultat:
osta t niego wyrazeni a po prawej ; 30
Kod ten jest bardzo prosty. Zmiennej num4 została przypisana wartość ostatniego z trzech przy Nawiasy w tym przypisaniu są konieczne. Gdybyśmy je pominęli , to pierwsze wyrażenie oddzielone przecinkiem od pozostałych miałoby postać :
pisań .
num4 = numl = l a w wyniku czego
wartość
Oczywi ś cie wyrażenia
zmiennej num4 wyniosłaby 10.
oddzielane przecinkami nie instrukcje:
mus zą b yć
przypisaniami. Równie dobrze
mogliby śmy napisać na stępujące
long num l = l , num2 = l a, num3 = 100 , num4 num4 = (++numl, ++num2, ++num3 ) ;
~
O:
Rezultatem tego przypisania będzie zwięks zen ie wartości zmiennych numl, num2 i num3 o jeden, a następnie ustawienie wartości zmiennej num4 na wartoś ć ostatniego wyrażenia, czyli l al. Przykład ten ma na celu zaprezentowanie sposobu działania operatora przecinka , ale nie jest wzorem dobrego stylu programowania.
Koleiność wykonywania obliczeń Do tej pory nie mówiłem nic na temat kolejności obliczeń wykonywanych podczas wyzna czania w artości wyrażenia. Ogólnie jest ona podobna do tej, której uczymy się w szkole na lekcjach matematyki, ale w C++ istniej e trochę więcej operatorów. Aby zrozumieć , co się z nimi dzieje, musimy przyjrzeć się dokładniej mechanizmowi języka C++ stosowanemu do określan ia tej kolejności . Mechanizm ten nazywa si ę priorytetem operatorów.
Priorytety operatorów Jest to mechanizm , który ustawia w zadanej kolejności priorytety operatorów . W wyrażeniach operatory o najwyższym priorytecie wykonywane są na początku , następnie wykonywane są operatory o najwyższym po nich priorytecie i tak dalej aż do operatorów o prioryteci e najniż szym . P oni żej znajduje się tabela priorytetów operatorów C++:
reinterp ret _cas t sizeof new delete . . .typeid .* (jednoargumentowy) ->*
lewa
* / %
lewa
+ -
lewa
«
»
lewa
< <= > >= I ~
lewa lewa
&
lewa lewa
>
lewa
&&
lewa
»
lewa
?
(operator warunkowy)
prawa prawa lewa
Tabela zawiera wiele operatorów, których jeszcze nie znamy, ale do końca książki będziemy znać je wszystkie. Umieściłem je wszystkie w tabeli , dzięki czemu w razie potrzeby zawsze będzie można do niej wrócić i sprawdzić priorytet jednego operatora w stosunku do innego . Operatory o najwyższym priorytecie znajdują się na samej górze tabeli. Operatory znajdujące się w tej samej komórce mają taki sam priorytet. Jeżeli w wyrażeniu nie ma nawiasów, to operatory o takim samym priorytecie wykonywane są w kolejności określonej przez łączność . A zatem jeżeli łączność jest lewa, to najpierw wykonywany jest operator znajdujący się po lewej stronie, przechodząc coraz dalej w prawą. Oznacza to, że wyrażenie takie jak a + b + C + d zostanie obliczone, tak jakby było zapisane (( (a +b) + c) + d ), ponieważ binarny + ma łączność lewą. Zauważ, że w przypadku operatorów posiadających zarówno formę jednoargumentową (działa z jednym operandem), jak i dwuargumentową (działa z dwoma operandami), ta pierwsza ma zawsze wyższy priorytet i dzięki temu wykonywana jest zawsze wcześniej .
106
Visual C++ 2005. Od podstaw Kolejność
wykonywania operatorów można zawsze zm ienić za pomocą nawiasów. Ze na fakt, że w C++ dostępnych jest tak wiele operatorów, czasami trudno się połapać, co ma pierwszeństwo przed czym. Dobrym pomysłem jest w takim przypadku zastosowanie nawiasów. Dodatkowym plusem takiego podejścia jest fakt, że nawiasy zwiększają czytelność kodu.
względu
Typy zmiennych irzutowanie Obliczenia w C++ mo gą być wykonywane przy użyciu w artości tego samego typu. Kiedy napiszemy wyrażenie zaw i e raj ące zmienne lub stałe różnych typów, to kompil ator dla każdej z takich operacji musi dokonać konwersji jednego z operandów, aby pasował do drugiego. Proces ten nazywa się rzutowaniem. Je żeli na przykład chcemy dodać liczbę typu doubl e do liczby typu i nteger, to liczba i ntege r najpierw zostanie przekonwertowana na typ doubl e, a dopiero potem zostanie wykonane działanie dodawania. Oczywiście sama zmienna, która zawiera rzutowaną wartość, nie jest zmieniana. Kompilator przechowa przekonwertowaną war tość w pamięci tymcza sowej i zostanie ona usunięta po zako ńc ze n i u wykonywania obliczeń . Wybór operandu, który zostanie przekonwertowany, jest uzależniony od pewnych reguł. Każde rozbija się na kilka operacji pomiędzy dwoma operandami. Na przykład wyrażenie 2*3-4+5 zostanie podzielone na następujący szereg działań: 2*3 da w rezultacie 6, 6-4 wynosi 2 i na koniec 2+5 da nam wynik 7. A zatem zasady dotyczące rzutowania operandów, tam gdzie to konieczne, muszą być zdefiniowane tylko w kwestiach dotyczących par operandów. Dla każdej pary operandów różnego typu stosowane są poniższe zasady w podanej kolejno ści. Gdy kt óraś z zasad odnosi się do określonej sytuacji to jest ona stosowana. wyrażenie
Zasady rzutowania operandów 1.
Jeśli
2.
Jeśli
3.
Jeżeli
jeden z operandów jest typu long doubl e, to drugi przekonwertowany na ten typ. jeden z operandów jest typu doubl e, to drugi na ten typ.
też
też
zostanie
zostanie przekonwertowany
jeden z operandów jest typu fl oat , to dru gi zostanie prze konwertowany na ten typ .
4. Typy char, s ig ned cha r , unsi gned cha r, short i unsig ned s hort konwertowane są
na typ i nt.
5. Typ wyliczeniowy konwertowany jest na pierwszy z typów i nt, uns i gned i nt , l ong lub uns i gned l ong, który
6.
Jeśli
7.
Jeśli
elementów wyliczenia.
jeden z operandów jest typu uns i gned l ong, to drugi konwertowany jest na ten typ .
są
8.
mo że pomieści ć liczbę
jeden operand jest typu Io nq, a drugi typu uns i gned i nt , to oba operandy konwertowane na typ unsi gned lo ng.
Jeśli
jeden z operandów jest typu l onq, to drugi konwertowany jest na ten sam typ.
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
107
Na pierwszy rzut oka zasady te wydają s ię bardzo skomplikowa ne, ale ogó lna zasada j est taka, że typ o mniejszym zakresie wartości konwert uje s ię na typ o większy m zakres ie. Zwiększa to prawdopodob ieństwo , że b ędzi emy w stanie prze ch ować wynik. Dz i ałan ie tych zasad m ożem y wypróbować na hipotetyczn ym wyrażeni u . P rzypuśćmy , że mamy kilka deklaracj i zmiennych:
dou ble val ue = 31 .0;
i nt count = 16 :
flo at ma ny = 2.0f:
char num = 4:
Przypuś ćmy t akże , że
dysponujemy
p on i ż s zym
val ue = (value - count) *(count - num)!many Możemy
z niego instrukcj i.
wywnioskować ,
jakich
+
przypadkowym
wyrażenie m
arytmetycznym:
num!many:
rzu to wań
dokona komp ilator podczas wykonywania
Pierwsza operacja polega na obliczeniu wartości wy raże n ia (va l ue - count ), Zast osowanie ma tutaj re guła 2., w myśl której wartość zmiennej count zosta nie przekonwertowana na typ doubl e, a zwrócony wynik b ęd z i e tego właś n ie typu, czy li 15. O. N as tępnie
przechodzimy do obliczania wartości wyraże nia (count - num). W tym przypadku pierwsza zasada, która ma zastosowanie , to zasa da num er 4. A zatem zmie nna numzostanie prze konw ertowana z typu char na typ i nt i w wyniku otrzymamy li c zb ę całkowi tą 12.
Nas tępne
obliczanie dotyczy p owyż s zych dwóch wyników - liczby typu doubl e 15 oraz liczby 12. Tutaj zastos owanie ma reguła numer 2 - liczba 12 zostanie przekonwert owana na 12 . O, a n a stępni e zwrócony zosta nie wynik typu doubl e 180. O.
całkowi tej
Wynik ten musimy teraz pod z i eli ć przez wartość zmiennej many. Tutaj ponownie zastosowa nie ma r e guł a num er 2. W art o ś ć zmiennej many zos ta nie przekon wert owana na typ doubl e przed wygenerowaniem wyniku tego samego typu o w a rto ś c i 90. N as tępni e
ob licza na jest w arto ś ć wyraż en i a num/ many, gdzie zastosowanie ma reguł a num er 3, która powoduje powstanie wartości 2 . Of typu fl oat, będącej wynikiem konwersji zmiennej numz typu char na typ fl oa t.
Na zakończe nie wartość typu doubl e 90. Ojes t dodawana do w a rto ś c i typu fl oat 2 . Of . W tym przypadku stosujemy reguł ę 2. Po przekonwertowan iu 2 . Of na 2 .Odo zmiennej val ue zostaje zapisany wyni k 92 .O. Po tych wszystkich
wyjaś nie niac h powinn iś my mie ć już
ogó lne rozeznanie .
Rzutowanie winstrukcjach przypisania Jak j uż przekona liśmy się w p rzykład zi e Cw2_05.cpp, rzutowanie niejawne można spowodo w ać poprzez napisanie po prawej stronie przy pisa nia wyrażenia o innym typ ie n iż zmienna po lewej . Może to spowo dować zmianę wartości i utrat ę danych. Je śl i przypiszemy na przy kład w artoś ć typu f l oat lub doubl e do zmie nnej ty pu i nt lub l ong, to utracimy część uł am kow ą tych wartości (można utraci ć nawet więcej , j eż el i zmien na typu zmien nopozycyj nego przekracza zasięg wa rtoś c i prze widzianych dla typu i nteger).
108
Visual C++ 2005. Od podstaw Na
przykład
po wykonaniu
poniższego
fragmentu kodu:
int number = o;
float decimal = 2.5f ;
number = decimal ;
wartość zmiennej number będzie wynosiła 2. Zauważ literę f na końcu stałej wartości 2. 5f. Informuje ona kompilator, że jest to liczba zmiennopo zycyjna typu si ngle. Bez litery f do myślnie byłaby to liczba typu doubl e. Każda s t a ł a zawierająca czę ść dziesiętną jest typu zmiennopozycyjnego. Jeśli nie chcesz, aby była ona typu doubl e, to musisz dodać literę f. Takie samo znaczenie m iałaby tutaj wielka litera F.
Rzutowanie jawne W przypadku wyrażeń zawierających wartości różnych typów podstawowych kompilator automatycznie dokona rzutowania tam, gdzie jest ono potrzebne . Ale jeśli chcemy, to możemy także taką operację wymusić za pomocą rzutowania jawnego. W celu rzutowania wartości wyrażenia na okre ślony typ posługujemy się następującą instrukcją: static_cas t ( wyra żen i e )
Słowo kluczowe st at i c_cast oznacza, że rzutowanie je st sprawdzane w sposób statyczny, a więc podczas kompilacji programu. Po uruchomieniu programu nie będzie już sprawdzane, czy zasto sowanie rzutowania jest bezpieczne. Później , kiedy będziemy zajmować się kla sami, spotkamy się ze słowem kluczowym dynami c_cast, które powoduje, że konwersja jest sprawdzana dynamicznie, czyli podczas wykonywania programu . Istniejąjeszcze dwa rodzaje rzutowania: const _cast do usuwania stanu stało ści wyrażenia oraz re i nt erpr et _cast , które jest rzutowaniem bezwarunkowym, ale na razie nie będziemy s i ę nimi zajmować.
Efektem powyższego rzutowania statycznego jest konwersja wartości wyrażenia exp ressi on do typu podanego w nawiasach ostrych. Jako express i on mo żna wstawić cokolwiek - od pojedynczej zmiennej po złożone wyrażenie zawierające wiele zag n i eżd żo n y c h nawiasów . Poniżej
znajduje
s ię
szczególny przypadek
użycia
double value1 = 10.5: dou ble value2 = 15.5: i nt whole number = stat ic cast (val uel )
+
sta t i c_cast<>( ):
sta tl c cast(va l ue2);
Wartością inicjalizującą zmiennej whole_number jest suma całkowitych czę ści warto ści zmien nych val ue l i val ue2, a więc są one przekonwertowane w sposób jawny na typ i nt . Dzięki temu zmienna ta przyjmie wartość 25. Rzutowanie nie wpływa na warto ści przechowywane w zmiennych val ue1 i va l ue2, które nadal będą wynosić odpowiednio 10.5 i 15.5. Wartości 10 i 15 są tylko tymczasowo przechowywane do użycia w obliczeniach , a po ich zakończeniu usuwane z pamięci . Mimo że każde z tych rzutowań powoduje utratę danych, kompilator zakła da, że wiemy, co robimy, skoro stosujemy rzutowanie jawne.
Poza tym, jak już pi sał em w przykładzie Cw2_05.cpp odno szącym się do przypisań z różnymi typami, zawsze można poinformować, że wiemy, iż rzutowanie jest konieczne, poprzez zrobie nie go jawnym:
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
109
st r tpsperj-ol l = stat ic_castCro111ength I hei qht ) : IISprawdź liczbę paskó w II w rolce.
Rzutowanie jawne można stosować z dowolnych wartości numerycznych na dowolne inne wartości numeryczne, ale trzeba zawsze być świadomym możliwości utraty informacji . Jeśli na p rzykład dokonamy rzutowania wartości typu fl oat lub doub l e na typ l onq, to w wyniku konwersji utracimy część ułamkową liczby, a co za tym idzie, jeżeli liczba była mniejsza od l , to otrzymamy w wyniku O. Przy rzutowaniu z typu doubl e na typ f lo at strac imy na precyzji , ponieważ zmienna typu fl oat ma precyzję tylko siedrniocyfrową, a doubl e aż piętnastocyfrową. Nawet rzutowanie pomiędzy typami i nteger niesie ze sobą ryzyko utraty danych, w zależności od zastosowanych wartości. Na przykład wartość typu l ong może być zbyt duża, aby można ją było przechowywać w zmiennej typu short , a więc rzutowanie z typu l ong na short może prowadzić do utraty danych. Ogólnie rzecz biorąc, w miarę możliwości powinno się unikać rzutowania. Jeżeli okaże się , w swoim programie potrzebujesz wielu rzutowań, to prawdopodobnie został on źle zapro jektowany. Należy w takim przypadku przejrzeć jeszcze raz jego strukturę oraz sposoby wybierania typów danych i w miarę możliwości zlikwidować lub przynajmniej zredukować że
liczbę rzutowań.
Rzutowanie wstarym stylu Przed wprowadzeniem do C++ rzutowania stat i c_cast <>() (i pozostałych typów rzutowania: const_cast-c-r ), dynami c_cast <>( ) oraz rei nterpret _cast<>( ), o których będziemy jeszcze mówić) rzutowanie
jawne wyniku
wyrażenia na
inny typ
było
zapisywane
następująco:
Ctyp_do_kt órego_ma_nastąplć_ko n wersJaJwyrażen ie
Wynik liczbę
wy raż e n i a jest rzutowany na typ podany w nawiasie. Na pasków w rolce moglibyśmy zapisać następująco :
str i ps per roll = Ci nt )( ro11 1ength I heiqht ) :
II Oblicz
przykład instrukcję obliczającą
li czb ę pasków w
rolce.
Istnieją cztery typy rzutowania i każdego z nich można dokonać za pomocą starej składni. Ze względu na to kod, w którym wykorzystane jest rzutowanie starego typu, jest bardziej podatny na błędy - nie zawsze jest jasne, co mieli śmy na myśli, i możemy otrzymać inny wynik, niż się spodziewaliśmy. Mimo że rzutowanie w starym stylu jest jeszcze dość często spotykane (nadal jest częściąjęzyka i z powodów historycznych można j e spotkać w bibliotece MFC), to gorąco zachęcam do stosowania rzutowania tylko w nowym stylu .
Operatory bitowe Operatory bitowe traktują operandy jako szer eg bitów, a nie wartości numeryczne. Można ich używać tylko z typami całkowitymi, a więc : sho rt, i nt, l ong, si gned char i char oraz wer sjami tych typów bez znaku. Operatory bitowe są użyteczne w programowaniu sprzętu, gdzie status urządzenia reprezentowany jest przez szereg indywidualnych znaczników (to znaczy, że każdy bit w bajcie może określać status innego aspektu urządzenia), lub w każdej innej
110
Visual C++ 2005. Od podstaw sytu acji, w której zachodzi potrzeba upakowania zestawu znaczników typu włączon y- wyłą czony w jednej zmiennej. Sposób ich d z iałania pozn amy przy okazji s zczegółow ego om awia nia operacji wejścia-wyjścia, gdzie pojedyncze bity używ ane są do kontrolowania ró żn ych opcj i ob sługi dan ych . Istni eje
s z eść
operatorów bitowych: bitow y Ok
& bitowy AND
>
- bitow y NOT
» prze sun i ęc ie w prawo
Poniżej w yj aśniam
spos ób
A
bitowy
wyłączny
aR
« przesuni ęc i e w lewo
działani a każdego
z nich.
Opera10r bilowy AND Bito wy operator AND (&) jest operatorem binarnym, który łączy odpowiadające sobie bity w jego ope randach w okre ślony sposó b. Gdy oba bity maj ą w artość I, to zwracana jest warto ś ć l, w przeciwnym przypadku zwracana w arto ś ć to O. Efekt działania operatora binarnego cz ęs to pokazywany jest za pomoc ą tak zw anej tabeli prawdy. Pokazuje ona, jaki byłby wynik przy użyciu różnych kombinacji operandów. Tab ela prawdy dla operatora &przedstawia s ię następująco: Bitowy AND
O
O
o
1
O
O
Wynikiem łączeni a każdej kombinacji wiersza i kolumny za p omocą operatora &je st pod ana w punkcie ich przecięcia . Prze śl ed źmy na przykładzie , jak to d ziała:
char let t erl ~ 'A' , lett er2 = ' Z' . resul t result ~ let t erl &let t er2: Aby
~
wartość
O:
zobaczyć ,
co si ę dzieje, musimy spoj rzeć na wzory bitowe. Literom "A" i ,;Z" odpowia szesnastkowe Ox4] i Ox5 A (Kody ASCII pod ane zo stały w dod atku B). Sposób operatora bitowego AND na te dwie wartoś c i poka zany zo stał na rysunku 2.8.
daj ą wartości działania Mo żem y
to potwierdzić , sprawdzając , jak odpowiadaj ące sobie bity łączą s ię za pom ocą & w tabel i prawdy. Po przypisaniu otr zymamy wyn ik Ox40, co odpowiada znakowi @. na fakt , że operator & daje w wyniku zero, gd y ob a bit y m aj ą wartość zerow ą, go używać do ustawiania w zmi ennej niechcianych bitów na zero. Dokonujemy tego, tw orząc tak zwaną maskę i łącząc ją z oryginalną zmi enn ą za pomo cą operatora &. M askę tworzym y poprzez o kreś l e n i e wartości jeden, tam gd zie chcemy iachować bit , i zero tam , gdzie chcemy bit u stawi ć na zero . W wyniku łączenia maski z inną warto ści ą całkowitą otrzy mamy bity zerow e w miejscach , gdzie u stawili śmy bity zerowe w masce, oraz takie same war to ś ci jak oryginalny bit w zmiennej , tam gdz ie w masce jest bit o wartości jeden. Przypuśćmy,
Ze
w zględu
możn a
~
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
111
Rysunek 2.8
letter1 : Ox41
letter2 : ox5A
result: ox40
o
1
o
o
o o o
1
1
1
i& l& i& i& &i i& &i &i !o ! !o ! !!o ! !o i i i i i ii l J l JT rr J o o o o o 1
01
o
1
r
że
mamy z m ie nn ą l etter typu char , z której chcemy u sun ąć cztery bity wysokie i zach ować cztery bity niskie. M ożn a tego łatwo d okona ć , tworz ąc m a s k ę OxOF i łącz ąc j ą z wart o śc i ą zmiennej l etter przy uży ciu operatora &:
letter
=
l et t er &OxDF :
Lub bardziej zw i ęź le :
letter &= OxOF : Je żeli w arto ść
zmiennej char wyno s iła Ox4l , to po wykonaniu p owyż szy ch instrukcji otrzy mamy wynik OxO l. Operacja ta zo st ał a przedstawi ona na rysunku 2.9. Bity zerowe w masce powoduj ą, ż e odp owi adające im bity w zmiennej l etter zostają usta wione na zero, a bity o wartośc i jeden w masce powoduj ą, że od p owiad aj ące im bity w zmien nej zos tają zac howane w swojej oryg ina lnej postaci.
W podobny sposób za pom ocą maski OxfD możemy za c hować cztery bity wysokie i wyzerować cztery bity niskie. A zatem w wyniku poni żs zej instrukcji w arto ś ć zmiennej l etter zmieni się z Ox4l na Ox40:
letter &= OxFO:
Bitowy operator sumy logicznej Bitowy operator sumy logicznej OR ( I) , czasami nazyw any a lternatyw ą bitow ą, wiąże odpo wi ad aj ąc e sobie bity w tak i spos ób, że w wyniku otrzymuj emy w arto ść j eden, j e ż eli jeden z operandów ma warto ś ć je den, i zero, je żel i oba operandy m aj ą w artość zero. Tabela prawdy dla tego operatora przedstawia się następująco: Bitowy OR
O
O
o
l
112
Visual C++ 2005. Od podstaw
Rysunek 2.9
letter: ox41
ma sk:OxOf
result: oxoi
o
1
o
o
i& &ff& l& '!o !o Jo !o ffff - - - -
Jo J'l r o o o
o
00
ff f
1
t
& & & &
! !! ! ff ff J! ! 1
1
1
1
.o
o
o
1
Przećwic zmy na pr zykładzie , jak można ustawić poszczególne znaczniki upakowane w zmien nej typu c ałk owitego . Przypuśćmy , że mamy zmienną o nazwie styl e typu sho rt , która za wiera 16 jednobitowych znaczników. Załóżmy, że chcemy u stawi ć poszczeg ólne znaczniki w tej zmienn ej . Jednym sposobem jest zdefiniowanie warto ś ci , które można następnie powią zać z operatorem OR w celu włączenia określonych bitów . Aby u stawi ć ostatni bit po prawej, m ożn a zd e fi n i ow ać:
sho rt vredraw Aby
u stawi ć
~
OxO l;
drugi bit od prawej , zmienną hredraw można zd e fin i o wać w
sho rt hred raw
=
następujący
sposób :
Ox02;
A zatem aby w zmiennej sty le u stawić dwa ostatnie bity po prawej na l,
można posłużyć się
następuj ąc ą i nst ru kcją:
style
~
hredraw I vredraw;
Wynik tej instruk cji pokazany jest na rysunku 2.10 . Jako
że
operacj a przy
u życiu
wartość jeden, łączenie
bity
operatora OR daje wynik jeden, jeżel i któryś z dwóch bitów ma dwóch zmiennych za pomocą tego operatora daje wynik , w którym oba
są wł ąc zone .
C zęs to
zostać
zachodzi potrzeba ustawienia znaczników w zmiennej bez zmiany innych, które mogły ustawione już gdzie ś indziej. Można tego łatwo dokonać za pomoc ą poniższej instrukcji;
style
I~
hred raw I vredraw ;
Instrukcja ta ustawi dwa ostatnie (po prawej stronie) bity zmiennej sty l e na l , a pozo stałe pozostawi bez zmian.
Rozdział 2.
hredraw:oxoz
• Dane. zmienne i działania arytmetyczne
o o o o
00 o o ·
o o o o
on on on on
on on onon
on on on on
rrlr
on on aR on
l l l l i i i i = : ;: = =
l l l l i i i i = = = ::;:
lo lo lo lo i i i
lo lo lo l1 i i i i = = ::;: =
IIi l l l l l
., o o o o
vredraw:OxOl
ł
style:Ox03
!ł !
o o o o
o o o o
!.! !! o o o o
r I'rII
o o o o
113
o o 1 o
II r l !o !o ł1 !1
Rvsunek 2.10
BitoWY operator różnicy symetrycznej Bitowy operator różni cy symetrycznej (ang. Exclusive DR; A) ma podobne dzi ałani e do opera tora alternatywy, ale daje wynik zero, gdy oba operandy m ają w artość jeden. W zwi ązku z tym j ego tabela prawdy przed stawia s i ę następuj ąco: Bitow yEOR
O
O
O O
Spój rzmy na wynik wykonania poni ższej instrukcji przy nych co w przypadku operat ora AND: result = letter1
A
użyci u
tych samych
warto ści
zmien
l et t er2;
Operacj ę tę możemy przedstawić następuj ąco :
l et t er1 01 00 0001 let t er2 0101 lala
Wynik
dział an i a
operatora EOR jest n a st ępuj ący :
resul t 0001 101 1 Wartość
zmiennej result zostaje ustawi ona na Oxlb lub -
w zapisie
d zie siętnym
-
na 27.
Operator ma do ść zaskakuj ąc ą właściwoś ć . Przypuśćmy , że mamy dwie zmienne znakowe: fi rst o warto śc i A i l ast o warto ś ci Z, którym odpowiadają w arto ści binarne 0100 0001 i 0101 lala. W wyniku poniż szych instruk cj i: A
114
Visual C++ 2005. Od podslaw f i rst l ast : I/Wynik fi rslto 00011011. l ast A ~ fi rst : II Wynik last to 0100 0001.
f i rst l ast : II Wynik first to 0101 1010.
A=
A=
zmienne te zamienią się wartościami , nie korzystając z żadnej Działa to ze wszystkimi wartościami typu całkowitego .
pośredniej
lokalizacji
pamięci .
Bitowa negacia Bitowy operator negacji - zamienia wartości pojedynczych operandów na przeciwne. Tak więc jeden staje się zerem, a zero staje się jedynką. Przy zał ożeniu, że wartość zmiennej l etterl wynosi 0100 0001, po wykonaniu poniższej instrukcji: result
=
-l etter l; wartość
zmienna re sult przyjmie sie dziesiętnym .
10ll l ll O, co odpowiada
wartości
OxBE lub 190 w zapi
Bitowe operalory przeSlInlęć Operatory te zwracają wartość zmiennej całkowitej, przesuniętą o określoną liczbę bitów w lewo lub prawo . Operator » służy do przesuwania w prawo, a operator « - w lewo. Bity, które wychodzą poza granice zmiennej, są tracone. Na rysunku 2.11 widać efekt przesunięcia dwubajtowej zmiennej w prawo i w lewo . Pokazano także wartość początkową. Zmienną o
nazwie number deklarujemy za
unsigned int number
~
całkowite
Możemy przesunąć zawartość
« =
2;
instrukcji :
16387U;
Jak już mówiłem, zmienne number
pomocą następującej
bez znaku zapisujemy, dodając literę u lub Una ich końcu . tej zmiennej w lewo za pomocą poniższej instrukcji :
II Prz esunię cie w lewo o dwa bity.
Po lewej stronie operatora przesunięcia znajduje się wartość, którą chcemy przesunąć, a po prawej podajemy liczbę bitów, o którą ma nastąpić przesunięcie . Na rysunku widać efekt takiej operacji . Jak w idać , przesunięc ie wartości 16387 o dwie pozycje w lewo dało w wyni ku liczbę 12. Ta dość drastyczna zmiana wartości spowodowana została utratą bitu wysokiego w wyniku przesunięcia. Wartość tę możemy również przesunąć w początkową -
number » = 2;
16387, a
następnie
prawo. napiszmy:
Przywróćmy
zmiennej number jej
wartość
II Przesunięcie o dwa bity w prawo.
Instrukcja ta przesuwa liczbę 16387 o dwa bity w prawo, w wyniku czego otrzymujemy war 4096 . Przesunięcie o dwa bity w prawo spowodowało podzielenie wartości przez cztery (bez reszty). To także zostało pokazane na rysunku.
tość
Rozllział 2.
RysUnek 2.11
• Dane, zmienne i działania arytmetyczne
115
Liczba dz iesiętna 16 387 w zapisie dwójkowym :
Przesun ięcie
wlewoo2:
.Pr zesunięcie
w prawo o 2:
=4096
Dopóki żadne bity nie są tracone, przesuwanie o n bitów w lewo jest równoznaczne z mnoże niem n razy danej wartośc i przez 2. Inaczej m ówiąc, jest to równoznaczne z mnoż eniem przez 2". Podobnie prze suni ęci e w prawo o n bitów jest równoznaczne z dzieleniem przez 2". Na leży jednak pamiętać , że j e śli w wyniku przesun i ęcia zostan ą utracone jakie ś w ażne bity, to wynik w niczym nie będz i e przypominał tego, czego s ię spod zi e w aliśm y , choć ta operacja nie różn i się od mnożenia. Gdyby śmy pomnożyli l iczb ę dwubajtową przez cztery, to otrzymal i byś my taki sam wyn ik, tak więc prze sun ięcie w lewo i mnożenie nadal są tym samym. Problem z do kładn oś ci ą pojawia si ę , ponieważ wartość wyniku mnożen i a jest poza za si ęgi em dwubajtowej liczby c ałko witej . M o że
nam się wyd aw a ć , że operatory przesuni ę cia m o g ą myli ć si ę z operatorami wej ścia Kompilator zawsze odgadnie z kontekstu, o który operator chodzi. J e śli jednak nie będ zi e to takie oczywi ste, to wygeneruj e komunikat, ale musimy być tutaj bardzo ostro żni. Jeśl i na przykład chcemy wy słać na wyj ś ci e wynik przesunięcia w lewo o dwa bity zmiennej number , to m ożemy u żyć n astępującej instruk cji: -wyj ści a .
cout
«
(number
«
2);
W tym przypadku n ajważniejszą rolę odgrywają nawia sy. Bez nich opera tor przesunięci a zostałby potraktowany przez kompilator jako operator strumieniowy i w związku z tym otrzymany wynik różn iłby s ię od spodziewanego - otrzymalibyśmy warto ść zmiennej nurnber z c yfr ą dwa. Na og ół operacja prze sun ięcia w prawo przypomina o p e rację prz e sunięci a w lewo. Przypu śćmy na przykład, że zmienna nurnber ma w arto ść 24 i wykonujemy następującą in strukcj ę :
number » = 2; W wyniku tego działani a zmienna number - dzięki podzi eleniu oryginalnej w arto ś c i przez cztery - będzie miała w arto ś ć 6. Nal eży jedn ak pami ętać , że operator prze sun i ę cia w prawo działa w szczególny sposób z ujemnymi typami całkowitymi (w których bit przechowujący
116
Visual C++ 2005. Od podstaw znak będący pierwszym bitem z lewej ma warto ś ć l). W takim przypadku bit znakowy jest przenoszony w prawo. Zdefiniujmy i zainicjalizujmy zmienną number typu cha r wartością dziesiętną -104: char number = -104:
II W zap isie binarnym liczba
l a lO
10011000.
Teraz przesuwamy ją o dwa bity w prawo: number » = 2: II Rezultat 11100110.
Wynik w zapisie dziesiętnym wynosi -26, ponieważ bit znakowy jest powtarzany . Oczywiście w przypadku operacji z typami całkowitymi bez znaku bit znakowy nie jest powtarzany i wsta wiane są zera.
Czas życia i zasięg zmiennych Wszystkie zmienne mają podczas działania programu ograniczony czas życia. Są one powoły wane do życia w momencie deklaracji, a następnie znikają w pewnym momencie - najpóźniej w chwili zakończenia programu. To, jak długo dana zmienna jest dostępna, jest określone przez właściwość zwaną czasem życia zmiennej . Istnieją trzy różne rodzaje czasu życia zmiennej : •
automatyczny,
•
statyczny,
•
dynamiczny.
Od którego z nich będzie zależeć czas życia zmiennej, wynika ze sposobu , w jaki została utwo rzona. Opis zmiennych z dynamicznym czasem życia odłożymy do rozdziału 4., a o pozosta łych dwóch rodzajach będziemy mówili w tym rozdziale. Inną właściwością zmiennych jest zasięg, oznaczający tę część programu, w której rozpozna wanajest dana nazwa zmiennej. W ramach zasięgu zmiennej można się do niej odwoływać w celu ustawienia jej wartości lub użycia w wyrażeniu . Poza jej zasięgiem nie można się do niej odwoływać - wszelkie próby takich odwołań zakończą się zgłoszeniem błędu przez kompi lator. Należy zauważyć , że zmienna może istnieć także poza swoim obszarem zasięgu, mimo że nie można się do niej odwoływać za pomocąjej nazwy. Przykłady takich sytuacji zobaczy my za chwilę.
Wszystkie zmienne, które deklarowaliśmy do tej pory, miały automatyczny czas co nazywane są zmiennymi automatycznymi. Przyjrzyjmy się im bliżej .
życia,
przez
Zmienne automatyczne Zmienne dekl arowane przez nas do tej pory zawsze znajdowały s i ę wewnątrz bloku - to znaczy pomiędzy dwoma nawiasami klamrowymi . Są one nazywane zmiennymi auto matycznymi i mają tak zwany zasięg lokalny lub blokowy. Zasięg zmiennej automatycznej rozpoczyna się w momencie jej deklaracji i kończy w miejscu, gdzie kończy się blok zawie
Rozllzial2. - Dane, zmienne i dZiałania arytmetyczne
117
rając y t ę de klarację . P r zest rzeń zajmowana prze z z m ien n ą automatyczną jest przyd zielana automatycznie w specjalnie do tego celu przeznaczonym obszarze pam ię ci, zwanym stosem. Domyśln i e stos ma rozmiar l MB, co w z upełnośc i wystarcza do większo ś ci z astosowań , ale jeżeli wartość ta okaże s ię za mała, to m ożna ją zwi ęks zyć do żąd anej wi elkoś ci za p omoc ą opcji proj ektu /STACK.
Zmienna automatyczna powstaje w momencie jej zdefiniowania. W tej samej chwili przy dzielane jest dla niej miejsce na stosie oraz automatycznie przestaje i stnieć w miejscu , w któ rym znajduje s i ę nawias klamrowy zamykający zawi e raj ący j ą blok. Za każdym razem, gdy wykonyw any jest blok instrukcji zawier aj ący deklar acje automatycznej zmiennej, zmienna ta tworzona je st od nowa. Jeżeli okre śliliśmy dla niej wartoś ć p oc zątkową, to za k ażdym razem , gdy je st tworzona, będz ie ponownie inicjalizowana. Kiedy zmienna automatyczna przestaje i stni eć , to pam ięć zaj mowana przez nią na stosie zostaje zwolniona do użytku przez inną zmi enną automaty c zn ą.
Istnieje s łowo kluczowe auto , za pomocą którego mo żna określa ć zmienne automatyczne, ale jest ono rzadko używane , gd yż zmienne s ą domyślnie automatyczne . P oni żej podaję przykład dotyczący tego, co przed ch wi l ą powied ziałem na temat za s ięgu zmiennych. II Cw2_0 7.cpp
II Prezentacj a zas ięgu zmiennych.
#include
usi ng st d: :cout :
using st d:: endl;
int main()
II Zasięg zmiennej rozpoczyna
{
się
tutaj.
l nt count l = 10;
i nt count3 = 50 ;
cout « endl
« «
" Wart o ś ć zewn ęt rz nej
zmiennej countl
"
~
«
countl
endl ;
II No wy zas ięg rozp oczy na s ię tutaj ...
i nt countl = 20 ; II To chowa zewnętrzną zmienną count /.
i nt count 2 ~ 30 ;
cout « "Warto ś ć wewnę tr z ne j zmiennej countl = .. « countl
« endl ;
countl +~ 3; II To działa na wewn ętrzną zm ienną count/
count 3 +~ count 2;
}
II ...i
cout « « « «
"Wa rto ś ć z e wnęt r z n ej
ko ń czy się
tutaj . «
zmiennej count l = "
endl
"W art oś ć zewnet rznej zmiennej count 3
~
count l
« count 3
endl :
II cout « count 2 « endl ;
II
Us unięcie
tego kom entarza spowoduj e
return O;
Wynik
dział ania
tego programu przed stawia
Wa rt o ś ć z ewnę t r z ne j
Wart o ś ć wewnętr zn ej Wa r t o ś ć z ew n ę t r z n ej
War t o ś ć zewn ę t r z n ej
zmiennej zmiennej zmi ennej zmiennej
count l ~ 10
count l = 20
countl = 10
count3 = 80
się następuj ąco :
b łąd.
118
Visual C++ 2005. Od podstaw
Jak lo działa Dwie pierwsze instrukcje deklaruj ą i d efin iuj ą dwie zmienne typu całkowi tego count l i count3 o warto ś ciach poc zątk owych odpowiednio 10 i 50. Cykl życ ia obu tych zmiennych rozpoczyna się w tym momen cie i końc zy s ię wraz z zam ykaj ącym nawiasem klamrowym na końcu pro gramu. Z asięg tych zmiennych rozci ąga s i ę również do nawiasu zamykającego funkcję ma i n( l. Należy pamiętać, że długość życia
zmiennej i j ej zas ięg to dwa różn e pojęcia. Ważne jest, aby ich nie mylić. Dlugość życia to okres podczas działania programu, rozpoczynający się w momencie utworzenia zmiennej, a koń czący w momencie jej zniszczenia i zwo lnienia zaj mowanej przez nią pamię ci dla innych celów. Zas ięg zmiennej to obszar w kodz ie programu, w którym zmienna j est dostępna . Zgodnie z definicjami zmiennych wartoś ć zmiennej count l je st wysyłana na wyj ście, tw orząc dwa pierwsze wiersze widoczne powyżej . Następnie znajduje s i ę otwarcie drug iego nawiasu klamrowego, który rozpoczyna nowy blok . Wewnątrz nieg o zdefiniow ane są dwie zmienne countl i cou ntż o warto ś ci ach odpowiednio 20 i 30. Zmienn a countl zadeklarowana tutaj ma inną wartość niż pierwsza zmienna count .l, która nadal istniej e, ale jest przesłon i ęta przez tę drugą. Odwołując s ię do zmiennej countl po jej deklaracji w wewn ętrznym bloku, otrzymamy w artość tej zmienn ej zadeklarowanej w tym bloku . Nazwę zmiennej zdupliko wałem tylko po to, aby pokazać, co s ię wydarzy. Mimo że kod ten j est prawidlowy, to nie należy się na nim wzorować. W prawdziwym programie takie coś byłoby bardzo mylące. Takie duplikowani e nazw zmiennych m oż e częs to pro wadz ić do przyp adkowego ukrycia zmiennych zdefiniowa nych w blokach zewnętrznych. Wartoś ć
pokazana w drugim wierszu na wyjści u pokazuje, że w bloku wewnętrznym używamy zmiennej countl w zasięgu lokalnym, to znaczy w obrębie najbardziej wewnętrznych nawiasów klamrowych :
cout «
"War tość wewn ę tr znej
Gdybyśmy
nadal
=
"
«
count l
używal i zewnętrznej
N astępni e wartość
countl
zmiennej countl
endl ;
«
+=
zmiennej count l , to zostałaby wyświetlona w artość la . zmi ennej countl zostaje zwiększona za pomoc ą na stępującej instruk cji:
3;
II To dz ia ła na
wewnętrzną zm ienną
countl.
Zwi ększen ie dotyczy zmiennej o zasięgu wewnętrznym , jako że zewnętrz na wciąż jest scho wana. Natomiast zmienn a count 3, która została zdefiniowana w z a s i ęgu zewnętrznym , została w n astępnej instrukcji bez probl emu zwięk s z on a :
count3
+~
count2:
Dowo dzi to tego, że zmienn e zadeklarowane na początku zas ięgu zewn ętrznego dostępne są równi e ż w zas i ęgu wewn ętrznym (należy zauważyć , że gdyby zmienna COUIlt3 została zade klarowana po klamrze zamykaj ącej blok wewn ętrzny , to i tak byłaby w ob ręb i e zasi ęgu bloku zewnętrzn ego, ale nie istniałaby jeszcze w momencie wykonywania p owy żs zej instrukcji).
Rozdział2.
W momencie
countl
• Dane. zmienne i działania arytmetyczne
119
klamry kończącej blok wewnętrzny, zmienne cou nt2 i wewnętrzna Zmienne count l i count3 nadal istnieją w bloku zewnętrznym, a wy pokazują, że zmienna count3 rzeczywiście została zwiększona w bloku
wystąpienia
przestają istnieć.
świetlone wartości wewnętrznym.
Usunięcie
l i cout
komentarza z wiersza : «
count2
«
endl;
II Us unięc ie tego komentarz a spowoduj e błąd
spowoduje, że program nie będzie mógł zostać poprawnie skompilowany, gdyż znajduje się w nim odwołanie do nie istniejącej zmiennej. Otrzymamy komunikat o błędzie podobny do poniższego :
c:\ microsoft visua l studio\myprojects\Cw2_07\Cw2_07,cpp(29) : error C2065 : 'count2 ' : undeclared identifier Dzieje
się
tak,
ponieważ
zmienna cou nt2 znajduje
się już
poza
zasięgiem.
Pozycjonowanie deklaracii zmiennych Jeśli
chodzi o wybór miejsca do wstawiania deklaracji zmiennych, to mamy dużą swobodę. czynnikiem wpływającym na naszą decyzję powinien być zasięg, jaki ma mieć dana zmienna. Poza tym zmienne powinniśmy zawsze deklarować w pobliżu pierwszego ich użycia. Pisząc program, zawsze trzeba pamiętać o robieniu tego w taki sposób, aby był on jak najbardziej zrozumiały także dla innych, a deklarowanie zmiennych w pobliżu ich pierw szego użycia bardzo w tym pomaga. Najważniejszym
Istnieje możliwość deklarowania zmiennych poza wszelkimi funkcjami w programie. omówimy, jakie są skutki takiego podejścia.
Poniżej
Zmienne globalne Zmienne, które nie są zadeklarowane wewnątrz żadnych bloków ani klas (o klasach będziemy mówić później) , nazywane są zmiennymi globalnymi i mają zasięg globalny (czasami także zwany zasięgiem globalnym przestrzeni nazw lub zasięgiem plikowym). Oznacza to, że są one dostępne w każdym miejscu programu od miejsca, w którym zostały zadeklarowane. Jeśli zadeklarujemy je na samej górze, to będą one dostępne wszędzie. Zmienne globalne mają także domyślnie statyczny czas życia . Zmienne globalne o statycz nym czasie życia istnieją od początku działania programu do jego końca . Jeśli nie określimy wartości początkowej, to zmienna globalna domyślnie przyjmie wartość zerową. Inicjalizacja zmiennych globalnych ma miejsce przed rozpoczęciem wykonywania funkcji ma i nO, dzięki czemu są one zawsze gotowe do użycia w kodzie, który znajduje się w ich zasięgu . Na rysunku 2.12 pokazano każdej ze zmiennych .
zawartość
pliku źródłowego Example.cpp.
Strzałki pokazują zasięg
120
VisIlai C++ 2005. Od podstaw
,
long wartośćl ; int mainO (
int wartość 2; ,
warto ść l
.. (
int warto ść 3;
I
...
wart ść3
1o
}
waTść2
}
int wartość 4; int funct ion(int n) {
i
warto ś ć4
long wa rto ś ć s;
I '
int warto ść 1;
I
".
w artość 5
wal1rś:j
}
i
Rvsunek 2.12 Zmi enna w ar to ś ć l, która znajduj e s ię na początku pliku , ma zasięg globalny, podobnie jak zmienna wa rto ś ć 4, której deklaracj a znajduje się po funkcji ma i n( ). Za s i ęg każdej zmiennej globalnej rozciąga s ię od miejsca jej deklaracji do koń ca pliku . Mimo że zmienna wa rto ś ć4 istnieje w momencie rozpoczęc i a wykonywania programu, to nie możn a się do niej o dwoły w a ć w funkcj i ma i n( ), gdyż nie znajduje s ię ona w zas i ęgu tej zmiennej. Aby mi eć dostęp do tej zmiennej z funkcji ma t nt ), musi el ibyśm y jej dekl ara cję przeni e ś ć na początek pliku. Za równo zmienna wa rt o ś ć l, jak i wa r to ść 4 z ostaną zainicjalizowane wartości ą domyśln ą, czyli zerem, co nie dotyczy zmiennych automatycznych. Zau ważmy, że lokalna zmienna o nazwie war to ś ć l w funkcji f unct ion( ) przesłani a zmienną globalną o tej samej nazwie. Jako że zmienne istni ej ą do samego końc a działania programu, nasuwa się pytani e: "Czy nie m ożn a w takim razie wszystkich zmiennych uczyni ć globalnymi i nie prz ejmować się znika niem zmiennych lokalnych?". Na pierwszy rzut oka wydaje się to bard zo ku szące , ale podobnie jak w przypadku mitologicznych syren - skutki uboczne znacznie przewy ższają ewentu alne korzyści . Prawdziwe programy zazwyczaj s kładają si ę z dużej liczby instrukcji, funkcj i i zmiennych. Zadeklarowanie wszystkich zmiennych ja ko globalny ch znacznie zw ię kszyłoby ryzyko przy padkowego, błędnego zmodyfikowania wartości zmiennej, a także zdecydowanie utrudniłoby odnalezienie odpowiednich nazw dla wszystkich zmiennych. Ponadto każda zmienna zajmuje
Rozdział 2.
• Dane, zmienne i działania ar»tmet»czne
121
pewn ą i lość pami ęci
przez cały czas trwania programu . Dzięki deklarowaniu zmiennych lokal nie, wewn ątrz funkcji lub bloków, zapewniamy im praw ie p ełn ą oc hro nę przed działaniem czynników ze w nętrznych - będą one is tniały i zajmowały pamięć tylko od miejsca ich dekla racji do koń ca zawi erając ego je bloku, d zi ęki czemu o wiele łatwi ej zarządzać całym procesem tworz enia pro gramu . J e śli spojrzymy na panel Class View po prawej stronie okna IDE, m ając otwarty który ś z utwo rzonych do tej pory przykładów , i rozwiniemy drzewo klas projektu , klikaj ąc znak +, to zoba czymy opcję Global Functions and Variables. Klikając ją, spowodujemy pokazanie wszystkie go, co w programie ma zasięg globalny. Znajdą s i ę tam funkcje globalne, jak również wszystkie zadeklarowane przez nas zmienne globalne.
aa:m Operator widoczności Jak już widzieli śmy , zmienna globalna może zostać przesłonięta przez zmien n ą lokalną o tej samej nazwie. Mimo to do zmienn ej globaln ej można nadal u zysk ać dostęp za pomoc ą ope ratora widoczności ( : .), któr y widz i eli śmy już w rozd ziale l. podc zas op isu przestrzeni nazw . Sposób jego działania zademon struj ę za pomocą zmodyfikowanej wersji poprzedniego przykładu :
II Cw2_08.cpp
II Prezentacja zas ięg u zmiennyc h.
#i ncl ude usi ng Std: :cout : using st d: .end l : i nt count l
=
100 :
II Wersja globalna zmiennej count l .
i nt mai n()
II Tutaj rozpo czyn a się zasię g fu nkcji .
{
i nt countl ~ 10: i nt count3 = 50: cout « endl cout
" W ar t o ś ć z ew nę t r z n eJ
« «
endl :
« «
endl :
"W ar to ść
zmiennej countl
global nej zmiennej countl II Nowy
=
zasięg
~
"
"
«
«
countl : :countl
II Z b lo ku z ewn ę t rz n ego .
rozpoczyna s ię tutaj. ..
II Ta instrukcja przesłania zewn ętrzn ą zmienn ą count l . int count l = 20: i nt count2 ~ 30: cout « " Wa r t o ść wewnęt r zne j zmi ennej countl = " « countl « endl: cout « " W arto ść global nej zmi ennej countl = " « : :count l II Z wewnętrzn ego II bloku. «
countl count3
endl : += +=
3; count 2;
II To dzia ła na II ...i
cout
«
"W ar t o ś ć z ewnę t rznej
wewnę trzną zmienną
koń czy s ię
zm iennej countl
=
tutaj. "
«
countl
count l
122
Visual C++ 2005. Od podstaw endl
« « «
/ / cout
"Wartość zewnęt rz ne j
zmiennej count3
= "
«
count 3
end l ; count2 « endl:
«
II Usun ięcie komentarza z tego wiersza sp owoduje bląd.
ret urn O: Po skompilowaniu i uruchomieniu tego programu otrzymamy
następujący wynik:
zmi enne j count l = 10
global nej zmiennej count l = 100
wewn ęt r zne j zmi ennej count l = 20
globa lnej zmiennej count l = 100
z ewnętrz nej zm iennej count l ~ 10
z ewn ę t rz n e j zmiennej count 3 ~ 80
W a rt o ś ć z ewn ę t r z nej
Wa rtość Wa r t o ś ć W ar t o ś ć W art o ś ć
War t o ść
Jak to działa Wiersze kodu na szarym tle oznaczają zmiany w kodzie w stosunku do pierwszej jego wersji. tylko tych właśnie fragmentów. Deklaracja zmiennej countl znajdująca się przed definicją funkcji mai n() jest globalna, a wię c dostępna w każdym miejscu funkcji mai nt ). Zmienna tajest inicjalizowana wartością 100: Zajmę się objaśnieniem
int count l
=
100;
II Wersj a globalna zmiennej countl .
Ale poza powyższą w programie mamy jeszcze dwie inne zmienne o nazwie count l , zdefi niowane wewnątrz funkcji ma i n( ). A zatem w programie globalna zmienna countl jest prze słonięta przez zmienne lokalne o tej samej nazwie . Pierwsza instrukcja wyjśc iowa to:
cout
" W a rto ść
«
«
global nej zmiennej countl = "
«
; :countl
II Z bloku ze wnę trznego.
end l :
Użyty w niej został operator zasięgu , aby kompilator wiedział, że chcemy odnieść się do zmien nej globalnej o nazwie count l, a nie do jej lokalnej wersji . Z wartości wysłanych na wyjście wynika , że to działa .
W bloku
zmienna countl jest przesłonięta przez dwie zmienne countl: we countl i zewnętrzną zmienną count l. Operator zasięgu wykonuje swoje
wewnętrznym
wnętrzną z m i e n ną
zadanie w bloku
wewnętrznym,
co
widać
w danych wygenerowanych przez
wstawioną
tam
instrukcję:
cout
« «
" War tość
global nej zmiennej countl
~
..
«
: .count l
II Z wewnę trznego bloku.
endl :
Powoduje to wysłanie na wyjście wartości 100; podobnie jak poprzednio operator zasięgu w ten sposób zawsze dotrze do zmiennej globalnej . Wcześniej mówiliśmy, że
użyty
do nazw należących do przestrzeni nazw std można odnosić się, nich kwalifikator tej przestrzeni, na przykład: st d : : cout lub st d: : end7. Kom pilator przeszukuje przestrzeń nazw o nazwie podan ej po lewej stronie operatora za s i ęgu w celu znalezienia nazwy podanej po jego prawej stronie. W poprzednim przy kładzie użyliśmy operatora zasięgu do przeszukania globalnej p rzestrzeni nazw w celu dodając do
Rozllział 2.
• Dane. zmienne i działania arytmetyczne
123
znalezienia zmiennej count l . Jeśli nie podamy nazwy przestrzeni nazw po lewej stro nie operatora zasięgu, to oznacza to, że kompilator ma przeszukać przestrzeń globalną w celu znalezienia nazwy, która znajduje się po jego prawej stronie . Operatora tego będziemy używać znacznie częściej, kiedy w rozdziale 9. dojdziemy do pro gramowania zorientowanego obiektowo, gdzie jest on bardzo często używany .
Zmienne statyczne Niewykluczone, że możemy potrzebować zmiennej dostępnej lokalnie, istniejącej jednak także po wyjściu z bloku, w którym została zadeklarowana. Inaczej mówiąc, możemy potrzebować zmiennej o zasięgu lokalnym, ale o statycznym czasie życia. Zmienną taką można uzyskać za pomocą określnika st at ic , a potrzeba jej użycia stanie się dla nas bardziej oczywista, gdy doj dziemy do omawiania funkcji w rozdziale 5. zmienna statyczna będzie istnieć aż do zakończenia programu, mimo że zadeklarowana wewnątrz bloku i dostępna jest tylko w jego obrębie (oraz w blokach w nim zagnieżdżonych). Nadal ma ona z as i ę g lokalny, ale ma także statyczny czas życia . W celu zadeklarowania statycznej zmiennej typu całkowitego o nazwie count możemy użyć następującej instrukcji :
W
rzeczywistości
została
st at ic int count: Je żeli
podczas deklaracji zmiennej statycznej nie podamy wartości początkowej, to zostanie ona zainicjalizowana automatycznie. Zmienna count zadeklarowana przez nas powyżej zosta nie zainicjalizowana wartością zero. Domyślna wartość początkowa zmiennych statycznych to zawsze zero przekonwertowane na odpowiedni typ. Należy pamiętać, że nie dotyczy to zmiennych automatycznych. Jeśli
nie zainicjalizujemy zmiennej automatycznej, to będzie ona przechowywała niepo trzebne wartości pozostałe po programi e, który jako ostatni korzystał z zajmowanego prz ez nią obszaru pamięci. I
Przestrzenie nazw Do tej pory już kilkakrotnie wspominałem o przestrzeniach nazw, a więc nadszedł czas, aby przyjrzeć się im dokładniej . Nie są one używane w bibliotekach obsługujących klasy MFC . Natomiast biblioteki obsługujące CLR i Windows Forms korzystają z nich bardzo często. Oczywiście biblioteka standardowa C++ ANSI także . Wiemy już, że wszystkie nazwy używane w bibliotece standardowej C++ ISO/ANSI zostały zdefiniowane w przestrzeni nazw st d. Oznacza to, że wszystkie nazwy używane w bibliotece standardowej mają dodatkowy kwalifikator st d. A zatem cout na przykład w rzeczywistości ma postać std : :cout. W poniższym prostym przykładzie możemy obejrzeć wykorzystanie pełnych nazw :
124
VisualC++ 2005. Od podstaw II Cw2_09 .cpp
II Preze ntacja prz estrzeni nazw.
#i nclude i nt val ue
=
O;
int mai n() {
st d: ;cout « " wp r owa d ź wa r toś ć ca ł kowi t ą ; ";
st d; ;Cl n » val ue ;
st d: :cout « "\ nWprowadzona wart ość to " « val ue
« st d . : endl :
retur n O;
Deklaracja zmiennej val ue znajduj e się poza d efinicją funkcji ma i n( ). Deklaracja ta znajduje w zasięgu globaln ej przestrzeni nazw, pon ieważ deklaracja ta nie znajduje s i ę w obrę bie żadnej przestrzeni nazw . Zmienna ta d o stępna j est z każdego miejsca w funkcj i ma in (), jak równi e ż z definicji innych funkcji, które m ogą znajdować s ię w tym samym pliku źró dłowym . Deklarację zmiennej va l ue umie ściłem poza funk cją mai n() tylko po to, aby zade monstrować , jak mogłoby to wyglądać w przyp adku przestrzeni nazw . s i ę więc
na brak dekl aracji usin g dla eout oraz endl . N ie jest ona tutaj potrzebna, w tym przypadku podajemy pełne nazwy z kwalifikatorem przestrzeni nazw st d. Mimo że nie byłoby to dobrym pomysłem , to mogliby śmy w tym miejscu użyć słowa eout jako na zwy zmiennej c ałkowitej i nie sp ow od ow ał o b y to żadneg o konfliktu , ponieważ samo s łowo eout to nie to samo co st d: :eout . A zatem przestrzenie nazw s łużą do oddziel ania nazw uży wanych w jednej c zęści programu od naz w u żyw an ych w inn ej czę ści. Są one nie oc en ioną pom ocą prz y pracy nad du żym i projektami, w które zaangaż ow anych jest kilk a ze sp oł ów programistów. Każdy zespół może mieć własną przestrzeń nazw, d zi ęki czemu nie trzeba się obawiać , że dwa ze sp oły użyją tej samej nazwy dla różnych funkcji . Zwróćmy uw agę gdyż
Spój rzmy na p on i ższy wiersz kodu : using namespace st d:
Instrukcja ta je st
dyrektywą
using,
Efektem jej działania jest import wszystkich nazw z przestrzeni nazw st d do pliku źródło wego, dzięki czemu będziemy mogli od w oływać się do wszystkiego, co jest w niej zdefiniowa ne, bez potrzeby używania kwalifikatora. W ten sposób, zamiast pisać st d: :eout lub st d: :endl , mo żemy zapisywa ć krótko eout i endl . Wadą tak iego wykorzystania dyrektywy usi ng jest to, że tracim y n ajważniejsze korzyści płynące ze stosowania przestrzeni nazw - m ożliwość uniknięcia konflikt ów nazw. Najbe zpieczniejszym sposobem uzyskania dostępu do nazw nale żących do przestrzeni nazw j est jawne dodanie kwalifikatora do każdej użytej nazwy - podej ście to niestety znacznie zwi ększa obj ętość kodu i zmniejs za jego czytelnoś ć. Inn ą możliwo śc i ą j e st wpro wadzenie tylko tych nazw , których będziemy używ ali w programie, za pomoc ą deklaracji us i ng. Na przykł ad : usi ng std.: cout: II Pozwala na używa nie słowa cout bez kwalifikatora.
usi ng std: endl : II Porwala na używan ie s łowa endl bez kwalifikatora .
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
125
Instrukcje takie nazywane są deklaracjami using. Każda instrukcja wprowadza jedną nazwę z określonej przestrzeni nazw i pozwala na używanie jej bez kwalifikatora w kodzi e programu, który znajduje s i ę po niej. Jest to o wiele lepszy sposób importowania nazw z prze strzeni nazw , ponieważ importujemy tylko te nazwy, których rzeczywiście używamy. Ze względu na fakt, ż e firma Microsoft wprowadziła zwyczaj importowania wszystkich nazw z przestrzeni nazw Syst emw kodzie w CH /CLI , będę się tego trzymał w przykładach w tej wersji języka . Ogólnie rzecz biorąc , zalecam stosowanie deklaracj i zamiast dyrektyw usi ng podczas pisania większych programów. Oczywiście możemy także definiować własne prze strzenie części będz iemy się właśnie
tym
nazw o wybranej nazwie. W dalszej
zajmować.
Deklarowanie przestrzeni nazw Przestrzeń
nazw deklaruje
się
za
pomocą słowa
kluczowego namespace:
namespace mySt uff { II Kod, który chcemy
zawrz eć
w przestrzeni nazw myStuff. ..
} Powyższy
kod definiuje przestrzeń nazw o nazwie myStuff. Wszystkie deklaracje nazw, które nawiasami klamrowymi , zostaną zdefiniowane w przestrzeni nazw my St uff. W związku z tym w celu odwołania się do której ś z nich poza tą przestrzenią należy użyć kwalifikatora przestrzeni nazw myStuff lub skorzystać z deklaracji usi ng.
znajdą się pomiędzy
Przestrzeni nazw nie można zadeklarow ać wewnątrz funkcji . Ich przeznaczenie jest odwrot ne - to w przestrzeniach mogą znajdować się funkcje, zmienne globalne i inne nazwane encje, takie jak klasy . Należy jednak p amiętać, że defini cja funkcji mai n( ) nie może znajdo wać się wewnątrz definicji przestrzeni nazw . Funkcja na i nt ), od której rozpoczyna s i ę program, musi być zawsze w zasięgu globalnym - inaczej nie zostanie rozpoznana przez kompilator. Zm i e n n ą
val ue z poprzedniego przykładu możemy umieścić w przestrzeni nazw :
II Cw2_10.cpp II Deklarowanie przestrzeni nazw.
#include namespace myStuff {
int value
=
O:
int mai nO {
std : :cout « "Podaj licz b ę cał kowi t ą : ". std: :ci n » mySt uff : :val ue : std : .cout « "\nWprowadzona li czba t o " « mySt uff : .value « std : : end l: ret urn O:
126
Visual C++ 2005. Od podstaw Prz e strzeń nazw myStuff definiuje zasięg i wszystko , co się w nim znajduje, jest zakwalifiko wane do tej przestrzeni. Aby odnieść się do nazwy zadeklarowanej wewnątrz tej przestrzeni nazw, nale ży do nazwy w odwołaniu dołączyć nazwę przestrzeni nazw . Wewnątrz przestrzeni nazw do wszystkich nazw w niej zadeklarowanych można odnosić się bez kwalifikatora wszystkie one należą do tej samej rod ziny. Teraz musimy zakwalifikować nazwę va l ue do przestrzeni nazw myStuff. Jeśli tego nie zrobimy, programu nie będzie można skompilować . Funkcja mainO odnosi się teraz do nazw w dwóch różnych przestrzeniach nazw i, ogólnie rzecz biorąc , można zdefiniować dowolną liczbę przestrzeni nazw w programie . Aby unik nąć konieczności kwalifikowania nazwy val ue przy każdym jej użyciu, możemy skorzystać z dyrektywy using:
II Cw2_ 11.cpp
II Używan ie dyrek tywy using.
#inelude namespaee myStuff
{
int val ue
~
O:
}
usi ng namespaee myStuf f :
II
Udos tępn ij
wszystkie nazwy z przes trzeni naz w myStujf.
int ma in() {
std : :eout « "Poda j l iczbe eałkowit ą :
st d: :ein »val ue :
st d: :eout « "\ nWprowadzona l iczba to " « st d: : endl :
ret urn O:
«
va l ue
Oczywiście, możemy również zastosować dyrektywę usi ng dla przestrzeni nazw st d, aby śmy nie musieli posługiwać się kwalifikatorem dla nazw w bibliotece standardowej, ale zaprze czyłoby to całej filozofii używania przestrzeni nazw. Ogólnie, kiedy używamy przestrzeni nazw, nie powinniśmy dodawać dyrektyw usi ng w całym programie , w przeciwnym przypadku moglibyśmy wcale sobie nie zawracać głowy przestrzeniami. Mimo to dodamy dyrektywę usi ng dla przestrzeni nazw st d w naszych przykładach w celu uproszczenia kodu. Kiedy roz poczyna się naukę nowego języka programowania, można sobie poradzić bez zbędn yc do datków, bez względu na to, jak bardzo są przydatne.
Wielokrotne deklaracje przestrzeni nazw W rozbudowanych programach często można spotkać wiele deklaracji jednej przestrzeni nazw . Można mieć wiele deklaracji prze strzeni nazw o danej nazwie, w których zawartość każdego z bloków należy do tej samej przestrzeni. Możemy na przykład mieć plik programu z dwiema przestrzeniami nazw:
namespaee sort Stuf f { II Wszystko tutaj
}
należy
do przestrzeni nazw sortStujf.
Rozdział 2.
• Dane. zmienne i działania arytmetyczne
127
namespace calculat eStuff
{
II Wszystko tutaj na leży do prz estrzeni nazw calculateS tuf{.
II Aby odnieś ć s ię do nazwy z prz estrzeni sortStujJ. na leży użyć kwalifikatora.
namespace sortStuf f
{
II To jest kontynuacja p rzestrzeni nazw sorzStuff;
II a więc m ożna s tąd odnosić się do nazw z p ierwszej p rzestrzeni sor tStujf
II bez używan ia kwa lifikatora.
Druga deklaracja przestrzeni nazwo danej nazwie stanowi tylko jej k ontynu ację. Dzięki temu w jej o bręb ie możemy odnosić się do nazw z poprzedniego bloku bez potrzeby używani a kwalifikatora. Wszystkie one nale ż ą do tej samej przestrzeni nazw. Oczywi ś cie nikt celowo nie organizuje w ten sposób zawartości swoich plików źródłowych , ale sytuacja taka może po wsta ć w sposób naturalny, gdy dodamy do programu pliki nagłówkowe . Możemy na przykład w programie m i eć coś takiego: #i ncl ude
#i ncl ude "myheader .h" II Zawartość należy do przestrzeni nazw myStuJf
#incl ude <st rl nq> II Zawar tość n ależy do przes trzeni nazw std.
II I tak dalej...
W powyższym kodzie i stri ng to pliki nagłówkowe należące do biblioteki stan dardowej C++ ISO/ANSI, a nagł ówek myheader .h zawiera nasz własny kod. Mamy tutaj do kładnie taką samą sytuację jak w poprzednim przykładzie . Mamy ju ż ogólne rozeznanie w sposobie d zi ałania przestrzen i nazw. M ożna by o nich opo j eszcze dużo więcej , ale je śli zrozumiemy wszystko , co jest zawarte tutaj, to w razie potrzeby będziemy potrafili znaleźć dodatkowe informacje.
w iedzi e ć
Zauważ, że dwie fo rmy dyrektywy #inc 7ude w poprzednim fragmencie kodu zm usiły kom pilator do szukania pliku na dwa różn e sp osoby. Jeśli plik, który ma zos tać dołą czony, zostanie podany w nawiasach trójkątnych, to komp ilator b ędz ie go szukał w ścieżce okre ś lonej w opcji kompilatora / l , ajeśli go nie znaj dzie, to w scizżce określonej przez zmienną ś rodo wiskową lNCLUDE. W tych lokalizacjach znajdują s ię pliki iblioteczne C++ i dlatego form a ta jest zareze rwowana dla n agłó wków biblioteczny ch. Zmi enna śro dowis ko wa lNCLUDE wskazujefolder, w którym znajduje s ię nagłówek biblioteczny, a opcj a / l pozwala na okreś lenie dodatkowej lokalizacj i zawie rającej nagłó wki biblioteczne. Jeżeli nazwa pliku znaj duje się w cudzys łowach, to kompilator przeszuka fo lder zawierający plik, w któ rym znaj duje się dyr ektywa #inc 7ude. Jeżeli plik nie zostan ie odnaleziony, to kompila tor będzie go szukał w innych fo lderach zawierających b ieżący plik. Jeśli nadal go nie znaj dzie, to poszuk iwania są kontynu owane w katalogach bibliotecznych.
128
VisualC++ 2005. Oli podstaw
Programowanie wC++/CLI
Język C++/CLI udostępnia szereg usprawnień i rozszerzeń omawianych do tej pory zagadnień . Zanim przejdziemy do szczegółów , zrobimy krótkie podsumowanie tych nowych właściwości :
• Wszystkie fundamentalne typy danych C++ ISO/ANSI mogą być używane w C++/CLI w taki sam sposób, jak to zostało przedstawione, ale w pewnych sytuacjach mają one pewne dodatkowe cechy , o których będziemy mówić. • C++/CLI posiada własny mechanizm przyjmowania danych z klawiatury i wysyłania ich do wiersza poleceń w programie konsolowym. • C++/CLI wprowadza operator safe_cast, który sprawia, kod wygenerowany w wyniku operacji rzutowania.
że można zweryfikować
• C++/CLI dostarc za alternatywne narzędzie wyliczania oparte na klasach, które oferuje większą elastyczno ść niż deklaracja enum w C++ ISO/ANSI. O klasowych typach referencyjnych CLR będziemy się uczyć dopiero w rozdziale 4., ale jako że wprowadziłem już zmienne globalne w natywnym C++, to wspomnę, że zmienne referen cyjnych typów klas CLR nie mogą być zmiennymi globalnymi . Rozpoczniemy od fundamentalnych typów danych w C++/CLI.
Fundamentalne typy danych wC++ICLI Możemy
i powinniśmy używać nazw fundamentalnych typów danych z C++ ISO/ANSI w programach w C++/CLI. W operacjach arytmetycznych zachowują się one identycznie jak typy w natywnym C++. Dodatkowo C++/CLI definiuje jeszcze dwa typy całkowite :
Typ
Bajly
Zakres wartości
l ong long
8
Od 9 223 372 036 854 775 808 do 9 223 vn 036 854775 807
unsi gned long long
8
Od Odo 18446744073709551615
Aby określić literał typu l ong l ong, do wartości całkowitej musimy dołączyć dwie małe litery 11 lub wielkie litery LL . Na przykład: long long big = 123456789LL ; Aby
otrzymać literał
typu long lo ng bez znaku, do wartości
całkowitej
dodajemy ULL lub u11:
~ unsi gned long long huge ~ 999999999999999ULL: Mimo że wszy stkie operacje na typach fundamentalnych, które widzieliśmy, działają w po dobny sposób także w C++/CLI, to nazwy typów fundamentalnych w programach C++/CLI mają inne znaczenie i w pewnych sytuacjach mają dodatkowe możliwości. Typ fundamen talny w programie w C++/CLI jest typem klasy wartości i może zachowywać się jak zwykła wartość lub jak obiekt, jeśli sytuacja tego wymaga.
Rozdział2.
- Dane, zmienne i działania arytmetyczne
129
W języku C++/CLI każdy typ fundamentalny C++ ISO/ANSI mapuje si ę na typ klasy war tości, który jest zdefiniowany w przestrzeni nazw Syste m. A zatem w programie w C++/CLI nazwy typów fundamentalnych ISO/AN SI s ą skrótami do skoj arzon ych z nimi typów klas wart oś ci . Dzięki temu wart ości typów fundament alnych mogą być traktowane po prostu jako wartośc i lub też , kiedy j est to konieczne, m ogą być automatyczni e konwert owane na obiekt o skojarzonym z nim typie klasy wartości. Typy fundamentalne, i lość zajm owan ej przez nie pamię ci oraz odpowiadaj ące im typy klas w artoś c i pokazano w p oniższej tabeli :
Typ lundamentalny
Rozmiar wbailach
Klasa wartości CLI
boo l
System: :Bool ean
char
Syst em : :Sbyt e
si gned char
Syste m: :Sbyte
unsigned char
System: :Byte
short
2
System: : Int1 6
unsig ned short
2
System: :UInt1 6
i nt
4
System: : Int 32
unslg ned int
4
System: :Ul nt 32
long
4
Syst em: : Int 32
uns igned long
4
Syste m: :U l nt 32
long long
8
Syste m: : Int 64
unsi gned long long
8
Syst em: :Ul nt 64
fl oat
4
System : :Si ngl e
double
8
Syst em: :Doubl e
long double
8
Syst em : :Double
wchar t
2
System: :Char
typ char jest równoznaczny z typem signed cha r, tak w i ęc skojarzony typ klasy to Syste m: :SByt e. Zauważ , że domyślny typ char można z m i en ić na unsig ned char, u stawiaj ąc opcję kompilatora / J, w którym to przypadku skojarzonym typem klasy warto ś ci będzie System : :Byte. Systemto główna przestrzeń nazw, w której zdefiniowane są wszystkie typy klas wa rtoś ci C++/CL I. W przestrzeni nazw Syst em zdefini owanych jest także wiele innych typów, takie jak na przykład typ St ri ng reprez entuj ący ł ań cuch y znaków, z którym zetkniemy się jeszcze w rozdziale 4. W C++/CLI zdefiniowany jest także typ klasy wartoś ci Syst em: :Deci mal w przestrzeni nazw Syst em. Zmienne tego typu przechowuj ą wartości dzie s iętne z dokładno ścią do 28 miejsc po prze cinku .
D omyślni e wartości
J akjuż powiedziałem ,
typ klasy wartości skoj arzony z każdą z nazw typów fundamentalnych znacznie zwięk s z a mo żli w o ści zmi ennych tego typu w C++/CLI. Je śli j est to koni eczne, kompilator dokona automatycznej konwersji z warto ści oryginalnej do skojarzonego typu kla sy i odwrotnie. Procesy te nazywa s i ę odpowiednio opakowywaniem (ang. boxing) i odpa kowywaniem (ang. unboxing). Dzięki temu zmienna jednego z tych typów może zachowy wa ć się jak prosta warto ś ć lub jak obiekt, w zależności od warunków. Więcej na ten temat dowiemy s i ę w rozdzial e 6.
130
Visual C++ 2005. Od podstaw P oniewa ż
nazwy typów fundamentalnych w C++ ISO/ANSI są aliasami nazw typów klas w programach C++/C LI, to w zasadz ie w programach C++/CLI m ożn a u żywać za równo jednych, jak i drugich. Wiem y j uż na przykład , ż e aby utw orzyć zmi enn e całkow i te i typu zmiennopozycyjnego, mo żemy p o służyć s ię n astępującymi instrukcj am i:
w artości
int count ~ 10; double val ue = 2.5; Mogli byś my użyć
mu
s ko mp i low ać
nazw klas program :
wartości o dpowi adającyc h
typom fundamentalnym i bez proble
System; ;I nt 32 count ~ 10; Syst em: :Doub le value = 2.5; Mimo że je st to całkowici e poprawne, to jednak powinniśmy sto sować nazwy typów funda mentalnych, takie jak i nt i doubl e, zamiast nazw klas w arto ści Syst em: : Int3 2 i Syst em: :Doubl e. Pow ód jest taki , że map ow anie pomi ęd zy nazwami typów fund am ent aln ych i typami klas wartoś ci , które op isywałe m, odnosi s ię tylko do komp ilatora Visual C++ 2005. Inne komp i latory nie mus zą po siad ać implementacji tego mapow ania . Na zwy fundamentalnych typów danych są ustalone przez standard języka C++/CLI, ale mapowani e na typy klas wartości jest dla większoś c i typów uzal eżnione od implementacj i. Typ l ong w Visual C++ 2005 jest mapo wany na typ Int 32, ale możliw e jest, że w innych implementacjach będzie mapowany na Int 64. Posiadanie danych typów fundamentalnych reprezentowanych przez obiekty typu klasy warto ści jest ważną cechąjęzyka CH/CLI. W CH ISO/ANSI typy fundamentalne i typy klasowe są c a łki e m inne, natomiast w C++/CLI wszelki e dane przechowywane s ąjako obiekty typu klasy w postaci typu klasy wartości albo jako referencyjny typ klasy. O referencyjny ch typach klas powi emy wi ęcej w rozdziale 7. Teraz wypróbujemy program konsolowy CLR.
Rm!!mI OWOCOWI prOgram konsolowI CLR Utwórz now y projekt i jako typ proj ektu wybierz CLR , zaś jako szablon - CLR Console App lication. N ast ępnie nazwij projekt Cw2_ 12, jak pok azan o na rysunku 2.13. Po
klikni ę c iu
przycisku OK kreator aplikacji wygeneruje projekt
II Cw2_ 12.cpp: main proj ect file.
#lncl ude "stdafx.h" usi ng namespace System; i nt ma i n(array<Syst em : :St ri ng
A >
A
args)
(
Console: :WriteLi neCL "Hell o ret urn O;
\~o r l d " ) :
zawi eraj ący następujący
kod:
Rozdział2.
New Project ~~
'
• Dane. zmienne i działania arytmetyczne
.
131
®r8J
..
N
Project types : Templates : 8 V lsua l -C+-+·--------, r-. r_ . ~. '--a ,-"u '-,---~·i-os.!.':'. .in~t-alIe-d-t-em-p-la-t-es--------------...=; . ·ATL
CLR o
MFC .. SmartD evce Win32
lj;
J!ł. AS P ,NET Web Se-vc e
fł.lc la ss LIbrary
l @i'@.i;;.IWPł@@ii
General
Other Languages
I
C'!il CLR EmptyProject
fJ:jSQLSerYer ProJect ilil'lWindows FOrtns Contra I Lbr ary
.j;l Windows Forms Application ~W indows Service
I _~y !~_~ pl.~ tes
itJ Other Project Types
i5]Search onlne Te mplate s...
l.~project~r creating,; .console appUcatlon Name :
Locaton : so lutionName :
, L~~~Trans la tons\h e lion\l.or .Hor tnns VIS U.;i CH 200?~~łady,, _~~
lB_12
~
0 Create
~.J
I Brawse .. I
d~ectory for solutlon OK
jI
C
Rysunek 2.13 Od razu widać, że w nawiasie po funkcji mai n znajduj e się coś nowego. Ma to zw iązek z prze kazywaniem w artości do funkcj i marn t ), kiedy uruchamiamy program z wiersza poleceń . Wi ęc ej na ten temat dowiem y się przy okazj i bardziej szczegółow ego omawian ia funkcji . Po skompilowaniu i uruchomieniu projektu w dom yśln ej post aci w w ierszu p ol eceń pojawi się napis .Hello World ". Teraz przekonwertujemy ten program w tak i sposób, aby był on wer sj ą CLR pro gramu z przykładu Cw2_0 2, dzięki czemu będziemy mogli stwierdzi ć , jak bardzo są one do siebie podobne. W tym celu zmodyfikujemy kod Cw2_12 .cpp w następujący sposób : II e w2 12.cpp : main project fi le.
#include "stda fx.h"
usi ng namespace Syst em:
i nt mai nC ar ray<System::String
A >
A
args )
(
i nt appl es . oranges : II Deklaracj a dwóch zmiennych ca łko witych i nt f rui t : II ..,ij eszcze jednej .
appl es = 5: oranges = 6: II Ustawianie wartośc i początko wych .
f rui t ~ apples + oranges: II Obliczenieliczby wszystkich owocó w.
Consol e : :WriteLi neC L"\ nMamy nie t ylko p oma r ańcz e ... ") :
Console: :WriteC L"· w sumi e mamy "):
Console: :Wr iteCfruit):
Consol e : :Wr HeCL" owoców .\ n") :
retu rn O:
132
Visual C++ 2005. Od podstaw Nowe wiersze zostały przedstawione na szarym tle. Te, które zn ajdują się w dolnym bloku, są tam umieszczone zamiast dwóch wierszy wygen erowanych automaty cznie w funkcji ma i ru ). Możemy teraz skompil ować i uruchom ić nasz program. Powinn i śmy otrzym ać n a stępujący wynik : Mamy nie tyl ko p om a r a ń c ze .
- w sumi e mamy 11 owoców.
Jak to działa Jedyn ą z n a cz ącą różn icą
jest sposób twor zenia danych w yjści owych . Defini cje zmiennych i wykony wanie obliczeń są takie same. Mimo że u ży wam y takich samy ch nazw typów jak w wersji w C++ ISO/AN SI, efekt nie jest taki sam. Zmienne app l es, oranges oraz fr uit b ędą typu C++/CLI - System: : I nt32, który jest określ ony przez typ i nt , i posiad aj ą one dodatkowe m ożliwości w porównaniu z typem ISO/ANSI. Zmienne te w okre ślonej sytuacji mogą zacho wywać s ię jak obiekty lub jak proste w artości, tak jak tutaj . Jeśli chcemy s i ę przekona ć, że w tym przypadku I nt 32 jest tym samym co i nt , to wystarczy zamieni ć typ i nt na I nt 32 i ponownie sko mp i lować projekt. Progr am powinien dzi ałać dokIadnie tak samo . P on iższy
wiersz kodu odpowiedzialny jest za
Conso le : :WriteLi ne(L "\ nMamy ni e ty l ko
wysłanie
p oma r a ńcze ..
pierwsze go wiersza danych :
");
Funkcja Wri t el i ne( ) należy do języka C++/CLI i jest zdefiniowana w klasie Co nsol e w prze strzeni nazw System. Bardziej szczegółow o na temat klas będziemy mówili w rozdziale 6., a na razie wystarczy nam wiedza, że klasa Consol e reprezentuj e stand ardowe strumienie wej śc ia i wyjś cia, które odpo w iadaj ą klawiaturz e i wierszowi p oleceń . A zatem funkcja Wri te l i neO drukuje w wierszu poleceń wszystko, co znajduje się pomi ędzy nawiasami znajduj ącymi s ię po jej nazwie, a n astępnie wstawia znak nowego wiersza w celu przeniesienia kursora do na stępnego wiersza gotowego na kol ejną o peracj ę wyj śc ia . W ten sposó b poprzednia instrukcja drukuje tekst \ nMamy ni e t yl ko p om a r a ńc z e ... w wier szu pole c eń . Litera l poprzedzaj ąca łań cuch oznacza, że jest to typ szeroki, w którym k ażdy znak zajmuje dwa bajty p amięci . Funkcja Write () w klasie Consol e jest w zasadzie taka sama jak funkcja Wri te l i net ). Jedyna ró żnica pole ga na tym, że nie dod aje ona znaku nowe go wiersza automatycznie na koń cu danych wyjściowych. Mo żemy zatem używać funkcji Wri te () wtedy, gdy chcemy wysłać dwa lub więc ej fragmentów tekstu do tego samego wiersza na wyj śc iu za pomocą oddzielnych instrukcji . Wartości ,
które umieszczamy pom iędzy nawiasami znajdującymi s i ę po nazwie funkcji, nazy wane są argumentami. W zależności od tego, jak zo stał a napisana, funkcja może przyjmować zero, jeden lub wi ę cej argumentów przy wywołaniu . Kiedy chcemy dostarczyć więcej niż je den argument, to musimy je oddzielić przecinkami. W klasie Console jest wi ęcej funkcji zwi ą zanych z wysyłani em danych, a w i ęc funkcjami Write ( ) i Wt ritel i ne( ) zajmiemy si ę trochę bardziej s z cz eg ó ło w o .
ROZllzial2. • Dane. zmienne i działania arytmetyczne
Wysyłanie
133
danych do wiersza poleceń wC++ICLI
W poprzednim przykładzie zaobserwowaliśmy,w jaki sposób można u żyć metod Con so l e : :Wri t e() i Cansa 1e : :Wri tel i ne () do drukowania łańcuchów znaków lub innych danych w wierszu poleceń. Pomiędzy nawiasami znajdującymi się po nazwie funkcji możemy umie ścić zmienną dowolnego typu, ajej wartość zostanie wysłana do wiersza poleceń. Możemy na przykład napisać następujące instrukcje w celu wysłania na wyjście informacji o paczkach:
int pack ageCount = 25: Console: :Write (L" Jest "): Console: :Wri t e(packageCount): Con sole : :WriteLine(L " pak ietów. "): Wykonanie tych instrukcji da
II Liczba paczek. II Wyślij lańcuch bez znaku nowego wiersza. II Wyślij wartos ć bez znaku nowego wiersza. II Wyślij lańcuch ze znakiem noweg o wiersza.
następujący
rezultat:
Jest 25 paczek. C ał y
umieszczony w jednym wierszu, gdyż w pierwszych dwóch instrukcjach funkcja Write O , która nie wysyła automatycznie znaku nowego wiersza. Wostat niej instrukcji użyto funkcji Wr itel i ne( ), która wysyła znak nowego wiersza na końcu wy syłanych danych, dzięki czemu wszystko, co zostanie wysłane po niej , będzie przeniesione do nowego wiersza. wynik
został
użyta została
Używani e
trzech instrukcji do drukowania jednego wiersza danych wydaje się dość praco nie powinno być zaskoczeniem, że istnieją lepsze sposoby na osią gnięcie tego celu. Jest to związane z formatowaniem wysyłanych danych do wiersza poleceń w programach platformy .NET i o tym będziemy teraz mówić. chłonną czynnością, więc
C++/CLI-Iormatowanie danych wyjściowych Zarówno funkcja Conso1e : :Wri te ( ), jak i funkcja Conso l e : :Wri t el i ne( ) pozwalają na kon trolowanie formatu danych wyjściowych i mechanizm ten działa w obu przypadkach w iden tyczny sposób. Najłatwiej jest to zrozumieć na przykładach . Najpierw spójrzmy, w jaki sposób możemy pobrać dane, które w poprzednim przykładzie zostały wysłane za pomocą trzech instrukcji, przy użyciu tylko jednej:
int packageCount = 25: Console . :Writ eL i ne(L"Jest {O} paczek.". packageCount ): Druga instrukcja w powyższym przykładzie wyśle na wyjście takie same dane , jak widzieliśmy Pierwszym argumentem funkcji Conso1e: :Wri tel i ne( ) jest w tym przypadku łań cuch l "Jest {O} paczek . ". Fragment, który informuje, że wartość drugiego powinna zostać wst awiona do łańcucha, to {O}. W klamrach zawarty jest łańcuch formatujący, który ma za stosowanie do drugiego argumentu funkcji , mimo że w tym przypadku łańcuch ten jest bardzo prosty - zawiera tylko zero . Argumenty, które następują po pierwszym argumencie funkcji Censol e : :Wr itel i ne ( ), są ponumerowane, zaczynając od zera, w następujący sposób: wcześniej.
odm esienie za
pomocą :
C o ns o l e : : W r it e L i n e( " l a ń c uc h fo rmat uj ący " .
O
2
itd .
arg2. arg3. arg4. . .. ) :
134
Visnal C++ 2005. Od podstaw A zatem cyfra Oumieszczona pomiędzy klamr ami w poprzednim fragm encie kodu wskazuje, że warto ść argumentu packageCount powinna zostać wstawiona w miejs ce {O} w łańcuchu, który ma z o s tać wysłany do wiersza poleceń. Jeśli
poza
liczbą paczek
chcemy wysłać
i nt packageCount = 25; double packageWeight = 7.5 ; Consol e; ;Writ eLi ne(L"Jest { O} paczek
także
ich
ważą cy ch
wagę,
to
możemy napi sa ć :
{ l} k t l ccr ama . ". pa ckageCount .
packageWe igh tl :
W tej postaci nasza instrukcja wyjścia ma trzy argumenty. Do drugiego i trzeciego z nich odwołujemy się odpowiednio za pomocą cyfr Oi l , umieszczonych w nawiasach klamrowych. Powyższy kod da nam następujący wynik: Jest 25 paczek
ważą cyc h
7.5 ki l ogr ama.
Możemy także instrukcję tę za pi sać, zamieniając kolejność
dwóch ostatnich argumentów, jak
poniżej:
Consol e: ;WriteLi ne( L"Jest {I } paczek
ważących
{O} ki l ogr ama." .
packageWe i ght .
packageCount) :
Do zmiennej pac kageWei ght odnosimy się tera z za pomocą cyfry O, a do packageCount za l w łańcuchu formatującym . Wynik będzie taki sam jak poprzednio.
pomocą
sposobu , w jaki dane m ają się prezentować w wierszu aby wartość zmiennopozycyjna zmiennej packageWe i ght była wysyłana z dokładnością do dwóch miejsc po przecinku. Możemy tego dokonać za pomocą poni ższej instrukcji:
W podłańcuchu {l ;F2} dwukropek oddziela wartość indeksową l , która wskazuje argument do sformatowania, od okre śl enia formatu - F2. Litera F w określeniu formatu oznacza, że dane wyj ściowe powinny być w formacie "eccc .cc . .. " (gdzie c reprezentuje cyfrę) , a 2 informuje , że chcemy mieć dwa miejsca po przecinku. Wynik tej instrukcji będzie następujący : Jest 25 paczek
ważący c h
7. 50 ki logr ama .
Ogólny format specyfikacji wygląda następująco: {n. w : Axx}, gdzie n to warto ś ć indeksowa argument znajdujący się po łańcuchu formatującym, wjest opcjonalnym określe niem szerokości pola, A to pojedyncza litera określająca sposób formatowan ia wartości, zaś xx jest opcjonalne i składa się z jednej lub dwóch cyfr określających dokładnoś ć wartości . Okre ślenie szerokośc i pola jest typu całkowitego ze znaki em. Wartość będzie wyrównana w polu do prawej, jeżeli wma wartość dodatnią, lub do lewej, j eżeli wma wartość uj emną. Jeżeli war tość zajmuje mniej miejsc niż określono za pomoc ą parametru w, to dane zostaną dopełnione spacjami. Jeżeli natomiast wartość wymaga więks zej liczby pól niż przewidzi ano w parame trze w, to określenie to jest ignorowane. Poniżej znajduje się jeszcze jeden przykład: wybierająca
Rozdział 2.
• Dane, zmienne i działania arytmetyczne
135
Console: :WriteLine (L"Li czba paczek:{0.3) W aga: {1.5:F2) ki lograma ". packageCount . pa ckageWeight l : Liczba paczek została wysł an a w polu o szero kośc i 3, a waga w polu o z tym wynik przed stawia się n a stępuj ąc o:
szeroko ś c i
5. W związku
Liczba pac zek: 25 Waga : 7.50 kil ograma. I stn i ej ą także inne okreś lni ki format ów, za pom o c ą któryc h mo żn a preze ntować dane róż nego typu na wiele sposo bów . P o ni ż ej znajduje s i ę kilka najbardziej przyd atnych specyfikatorów forma tu:
Specyfikator formatu
OpiS
Club c
Wy s ył a
D lub d
W ys ył a l i c z b ę całkowitąjako wartość d z i e s i ętn ą. J e śl i określo na dokła dność
na wyjście
będ z i e większa ni ż W y s ył a wartości
E lub e
z
wykła dnikiem.
wartość
w walucie lokalnej.
liczba cyfr, to n a stąpi
dopełn ie n i e
spacjami do lewej .
zmiennopozycyj ne w notacji nau kow ej - to znaczy Liczba cyfr wyświetlana po przecinku jest okreś lan a przez
warto ś ć dokładności . Wysyła wa rto ść zmien nopozycyj nąjako wart ość s ta łopozycyj n ą
F lub f
w formacie
±dddd.dd.... Wysyła wa rtość
Glub g
w naj krótszej fonnie w za leżności od jej typu i tego, czy z o s tał a nie została określo na, uży ta zosta nie
o kreś lona d o kł a dn o ś ć . Jeś li dokład ność
d o m y śl n a d okł a d n oś ć .
Nlub n
Wy s ył a wartość jako dziesiętną liczbę s tałopozycyj ną, używając przec inka do rozdzielenia trzycyfrowych grup cyfr, jeś li jest to konieczne.
Xlub x
W y s yła li c zb ę całkowitą w postaci szesnastkowej . W zależ ności od tego, czy w wyniku podamy X, czy x, zos ta ną podane wielkie lub małe litery.
Ty le informacj i wystarczy nam, abyśmy mogli kontyn uować z przykł adam i w C++/CLI . Teraz spójrzmy, jak to wszystko dz i ała .
~ Formatowanie danych wyjściowych Po n iżej
znajduje się przykł adowy program ob liczający cenę dywanu, m aj ący na celu zademon strowanie wysyła nia danych w programach konso lowyc h CLR. II Cw2_ 13.cpp : główny plik p rojektu. II Obliczanie ceny dywan u.
#include "stdafx .h" using namespace System ; int main (array<System : :Stri ng A> Aargsl {
Console: :WriteL i ne(L"Pokój ma wymi ary {O: F2} met rów na {l F2} metrów". roomLengthMetr. roomWidthMet r ): Conso le: :WriteLine(L"Powie rzchn ia pokoju to {O: F2} metró w kwadrat owych ". roomLengthMet r*roomWi dthMet r); Conso le: :WriteLine(L"Cena dywanu to {O:F2} z ł o ty ch " . carpet Price) : ret urn O: Wynik powinien
być na stępujący :
Pok ój ma wymiary 7.54 met rów na 4.11 metró w
Powierzchn ia pokoju t o 31 .04 met rów kwad rat owych
Cena dywanu to 867.60 z ł o ty c h
Jak to dziala Wymiary pokoju zo stały podane w metrach, natomiast cena dywanu podana jest w złotówkach za metr kwadratowy. W zw iąz ku z tym zdefiniowaliśmy stałą Cm Na Met r, której użyjemy do wykonania konwersji centymetrów na metry. W wyrażeniu konwertującym wszystkie wymiary wartość typu doubl e jest dzielona przez wartość typu całkowitego . Kompilator wstawi kod do konwersji wartoś ci typu całkowi tego na typ doubl e przed wykonaniem mnożen ia . Po przekon wertowaniu wymiarów pokoju na metry obliczamy cenę dywanu, najpierw mnożąc szerokość pokoju przez je go dług oś ć w celu uzyskania powierzchni w metrach kwadratowych, a następ nie mnożąc otrzymany wynik prz ez cenę jednego metra kwadratowego. W instrukcji wyj ściowej użyli śmy specyfikatora formatu F2 w celu ograniczenia liczby miejsc po przecinku do dwóch . Gdybyśmy tego nie zrobili, wynik zawi erałby więcej miejsc po prze cinku, co nie byłoby właściwe, zwłaszcza w przypadku podawania ceny . Mo żna u sunąć ten specyfikator, aby zobaczyć , jaka będzie różnica. Zauważmy, że
w instruk cji
wysyłającej powierzchnię
jako drugi argument funkcji Wr iteLi arytmetyczne. Kompilator obliczy najpierw warto ść tego wyra żenia, a dopiero jeg o wynik zostanie przekazany jako właściwy argument funkcji . Jako argu ment funkcj i zawsze m o żna p odać wyrażenie , pod warunkiem ż e jego wynik będzie typu zgodn ego z typem parametrów przyjmowanych przez funk cję . ne ( )
użyte zo s tało wyrażenie
C++/CLI- wprowadzanie danych zklawiatury Możliw o ści wprowadzani a danych z klawiatury, dostępne w programach konsolowych plat formy .NET, są d o ść ograniczone. Można wczytać cały wiersz dany ch w postaci łańcucha znak ów przy u życiu funkcj i Consol e : :ReadL i ne() lub pojed yn czy znak przy u życiu funkcji Consol e : :Read(). M ożna także sprawdzić, który klawisz zo stał wci śni ęty , za p omoc ą funkcji Consol e : :ReadKey ().
Rozdział 2.
Poniżej
znajduje
się przykład użycia
• Dane. zmienne i działania arJtmetJczne
137
funkcji ( ansol e : :ReadL i ne() :
St ri ng li ne = Conso le : :ReadLi ne( ): A
Powyższy kod wczytuje pełny wiersz tekstu, który kończy się w momencie wciśnięcia kla wisza Enter. Zmienna l i ne jest typu Str i ng i przechowuj e referencję do łańcucha powstałego w wyniku wykonania funkcji (a nsol e: :Rea dL i ne() . Znak znajdujący się po nazwie typu St r i ng oznacza, że jest to uchwyt wskazujący obiekt typu St ri ng. Więcej na temat typu St ri ng i uchwytów do jego obiektów dowiemy się w rozdziale 4. A
A
Instrukcja przyjmująca pojedynczy znak z klawiatury
wygląda następująco:
char ch = Conso le: :Read(): Za
pomocą
funkcji Rea d( ) można wczytać dane wejściowe znak po znaku, a następnie prze i przekonw ertować do odpowiadających im wartości numerycznych.
analizować je
Funkcja (ansol e: :ReadKey( ) zwraca wci śnięty klawisz jako obiekt typu (ansol eKeylnfo, który j est typem klasy wartości, zdefiniowanym w przestrzeni nazw System. Poniżej znajduje się przykładowa instruk cja sprawdzaj ąca wci śnięty klawisz:
Con soleKeylnfo keyPress
=
Console ReadKey(true);
Argument t rue podany jako parametr funkcji Rea dKey( ) powoduje, że znak odpowiadający wci śniętemu klawiszowi nie pojawi się w wierszu poleceń . Gdybyśmy zamienili ten argument na fa l se (lub go nie podali), znak by się pojawił . Wynik wykonania tej funkcji przechowy wany jest w zmiennej keyPress. W celu sprawdzenia, który klawisz (lub klawisze) został wci śnięty, należy posłużyć się wyrażeniem keyP ress .Key(ha r. W ten sposób możemy wysła ć na wyjście komunikat zawierający informacje o wciśniętych klawiszach za pomocą następującej instrukcji :
Co nso le : :'i
Wci śnięty
śc i
Mimo że w trakcie nauki brak formatowania danych wprowadzanych do programów konso lowych C++/CLI może być trochę niewygodny, w praktyce ma to bardzo małe znaczenie . Wszystkie prawdziwe programy, które będziemy tworzyć , będą przyjmowały dane za po śred nictwem komponentów okna, a więc nie będzie potrzeby wczytywania ich za pomocą wiersza poleceń .
Bezpieczne rzutowanie W środowisku CLR do jawnego rzutowania służy operacja safe_cast. W większości przypad ków w programach w C++/CLI do konwersji jednego typu danych na inny wystarczy st a t ic_cast, ale ze względu na pewne wyjątki, które mogą spowodować błąd, lepiej jest używać metody safe_cast . Metody tej używamy w podobny sposób jak sta t i c_cast . Na przykład:
138
Visual C++ 2005. Od pOdstaw double val ue1 ~ 10 .5:
double value2 = 15 .5:
int whole number ~ safe cast(val ue1)
+
safe cast (val ue2):
Ostatnia z powyższych instrukcji konwertuje wszystkie wartości typu doubl e do typu i nt przed dodaniem ich i zapisaniem wyniku jako wartości zmiennej who le_number .
Wyliczenia wC++ICLI Wyliczenia w programach C++/CLI znacznie różnią s i ę od swoj ego odpowiednika w C++ ISO/ANSI. Pierwsza różnica to sposób definiowania wyliczeń:
enumclass Ko lor{trefl. ki er. ka ro, pi k}: Powy ższy kod definiuje typ wylic zeniowy Kolo r, a zmiennym typu Ko lor można przypisać tylko jedną z w artości podanych w wyliczeniu - karo, trefl, kier lub pi k. Podczas uzyski wan ia dostępu do stałych wyliczeniowych w C++/CLI zawsze trzeba podać kwalifikator typu wyliczeniowego, do którego ona należy. Na przykład :
Kolor kolor = Kolor : :tref l : Powyższa instrukcja przypisuje wartość trefl z wyliczenia Ko l or do zmiennej o nazwie kolor. Znaki : :, oddzi elające nazwę typu wyliczeniowego Kol or od nazwy stałej wyliczeniowej t refl , to operator zasięgu , który informuje, że nazwa trefl istnieje w zakresie wyliczenia Kolo r .
Zauważmy, że po słow i e kluczowym enumw definicji wyliczenia znajduje się słowo kluczowe cla ss. Jak zapewne zauważyliśmy, słowa tego nie ma w definicji wyliczenia w CH ISO/ANSI, ponieważ identyfikuje ono wyliczenie jako część języka C++/CLI. Wskazuje ono na jeszcze jedną różnicę w stosunku do C++ ISO/ANSI stałe zdefiniowane w tym wyliczeniu (karo, t refl itd.) są obiektami, a nie prostymi wartościami jednego z typów fundamentalnych C++ ISO/ANSI. W rzeczywistości domyślnie są one obiektami typu I nt 32, a więc przechowują warto ści typu i nt . Należy jednak pami ętać , że aby używać tych s tały c h jako typu całkowitego, nale ży je poddać rzutowaniu na ten typ .
Ze względu na fakt, że wyliczenie jest typem klasowym , nie można go zdefiniować lokalnie (na przykład wewnątrz funkcji). Jeśli chcemy zdefiniować wyliczenie do stępne na przykład w funkcji ma in( ), to musimy je zd efiniować w zasięgu globalnym. Łatw o
to
zaobs erwować
na przykładzie .
~ Definiowanie wyliczenia wC++/CLI Poniż ej
znajduje
się
prosty przykład wyliczenia:
// Cw2_ 14.cpp: main proj ect fil e.
// Defini owanie i używanie wyliczeń w C++/CLI.
#include "stdafx.h" usi ng namespace System :
Rozdzial2. • Dane. zmienne i dZiałania arytmetyczne
139
II Definiowanie wyli czenia w zasięgu globalnym.
enumclass Kolor{t refl. ki er . karo. pik}: i nt ma inCarray<Syste m: :St ri ng
Ą > Ą a r g s)
{
Kolor kolor = Ko lor : :trefl: int value = safe castCkolor) : Conso le: :Writ eLi neC L"Ko lor t o {O} . ko lor ~ Kolor: :ki er : value = safe_castC kolor ) : Console: :WriteLi neCL"Ko lor t o {O}. ko lor = Kolor : :karo:
value = safe castCko lor ):
Conso le: :WriteL ineC L"Kolor t o {O }. kolor = Ko lor : :pi k:
value = safe_castCko lor ) :
Console: :WriteLineCL"Kolor to {O}. ret urn O:
Wynik
działania
Kolor Kolor Kolor Ko lor
to to to to
tego kodu
a
war t ość
{l }
a
wa r tość
{l }
kolor . va lus ):
n
kolor. value) :
a wa rt o ś ć {1}
ko lor . value):
a
kolo r . value) :
war t ość
{l}
będzie następujący :
t refl . a war t o ś ć O
ki er . a wa rt o ś ć l
karo. a wa r tość 2
pik. a war toś ć 3
Jak to działa Jako że wyliczenie Kol or jest typem klasowym, nie można go zdefiniować wewnątrz funkcji mai n( ). Z tego powodu jego definicję umieściliśmy przed definicją funkcji ma i n( ), dzięki czemu wyliczenie ma zasięg globalny. Poniższy fragment kodu definiuje zmienną kol or typu Ko lor i początkowo przydziela jej wartość Kol or : :t ref l :
Ko lor kolor
=
Ko lor : .tref l:
Przyporządkowanie stałej niezbędną.
o nazwie t refl do typu Kol or jest w tym przypadku Bez tego kompilator nie rozpoznałby nazwy t ref l .
czynnością
W wyniku otrzymanym z programu widać, że wartość koloru jest pokazana jako nazwa odpo wiadającej mu stałej; w pierwszym przypadku jest to t refl . Aby otrzymać wartość stałej odpowiadającą obiektowi, należy wykonać rzutowanie jawne na typ i nt za pomocą instrukcji podobnej do poniższej :
value = safe_cast Ckolor ) : W danych wygenerowanych przez program widać, że stałym wyliczeniowym zostały przy pisane wartości od zera. Typ stałych wyliczeniowych możemy oczywiście zm i en i ć . Jak tego d okonać , dowiemy się poniżej.
Określanie lypu slałych Stałe
wyliczeniowych
wyliczeniowe w C++/CLI
mogą być
jednego z następujących typów :
140
Visual C++ 2005. 011 podstaw
short
i nt
long
lang long
si gned cha r
cha r
uns igned short
uns igned int
unsigned long
unsigned long long
unsigned cha r
bool
W celu określen ia typu s tałej wyliczeniowej po nazwie typu wyliczenia należy podać wybrany typ, a następni e ro zdzieli ć je dwukropkiem, podobnie jak w przypadku s łowa kluczowego enum znanego nam z natywn ego C++. Aby na przykład zm i e n i ć typ stałej wyli czeniowej na char, m ożna posłu żyć s ię następuj ącą instru kcją:
enumclass Flgura
cha r {as . dwój ka , tró j ka . czwórka. p ią t k a . szóst ka . siódemka . ósemk a, dz i ew ią tk a . d z i e s i ąt ka. wal et , dama , król };
S ta łe wyli czen iowe w tym wy liczeniu b ęd ą typu char i taki te ż b ędz i e pod stawowy typ fundamentaln y. Pierw sza stała dom yślni e odpowia da warto ści o kodzie 0, a następn e zmienne kolejnym warto ściom . Aby dostać si ę do rzeczywistej warto ści , musimy wykonać jawne rzuto wanie warto ś c i na ten typ .
Określanie wartości stałych
wyliczeniowych
Domy ślne stały c h
kody wartośc i m ożna zm i eni ć poprzez jaw ne przypisanie wartości do jedn ej lub kiLku zdefin iowanych w wyli czeniu. Na przykł ad :
enumclass Figura
char {as
~
l, dwóJka. t rój ka . czwórka . p i ątk a . szóst ka. siódemka. ósemka. d z ie w i ątk a. d z iesi ąt k a . wa let . dama , król }.
W wyniku działania p owyż szego kodu as b ędzie reprezentowany przez jedyn kę , dwójka przez dwójk ę itd. aż do króLa, który będzie miał wartoś ć 13. Jeśli chcemy, aby wartości repre zentujące karty odzwierciedlały ich wartośc i (tzn. as najstarszy itd.), to nasze wyliczenie mo żemy zapi sać w następuj ący sposób:
enumclass Fi gura
char {as
~
14, dwójka ~ 2, t róJka . czwórka . piąt k a . szóst ka. siódemka, ósemka. d z i ew i ą t k a . d z ie si ąt k a. wa l et . dama, król }:
w tym przypadku dwój ka będzi e reprezentowana przez cyfrę 2, a n astępne stałe przyjmą kolejne w artości ,
tak
że
król nadal
będz i e miał
13. As
będzie mi ał warto ś ć
14, czyl i
t aką, jaką
mu
nadaliśm y. Warto ści
to ściom
przypisywane do s tałyc h wyliczeni owych nie mu szą być unikalne. D zi ęki temu war tych stałych m o żna nad awać okre śl on e specjalne znaczenie. Na przykł ad :
enumclass DniTygodni a Po wyższy
baal { pon ~ t rue , wt ~ t rue. sr ~ t rue , czw : true, pia : t rue. so : fal se , nd
~
fal se };
kod definiuj e wyliczenie Dni Tygodni a, w którym stałe wyl iczeniowe są s tałym i logicznymi (typu boal ). Wartości podstawowe zostały przypisane na zasadzie okre ś len i a dni roboczych i wolnych od pracy.
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
141
Podsumowanie
W rozdziale tym omówiłem podstawy wykonywania obliczeń w C++. Nauczyli śmy się wszyst kich podstawowych typów danych d o stępnych w tym języku oraz wiemy już, jak używać wszystkich operator ów służących do manipulow ania tymi danymi . Poni ższa lista zawiera spis najważniej szych tematów poru szonych w tym rozdziale: •
Każdy
program w C++
składa s i ę
z co najmniej jednej funkcji main ( l .
• Wykonywalna c zęść funk cji zbudowana jest z instrukcji zawartych nawiasami klamrowymi.
• Instrukcja w C++ zawsze
zako ńczo n a
jest
p omiędzy
średn iki em .
• Nazwy obiektów w CH (na przykład zmiennych lub funkcji) mogą składać się z liter i cyfr, pod warunkiem że pierwsza jest litera. Znak podkreślenia traktowany jest jako litera. W nazwach rozróżniane są wielkie i małe litery. • Nazwy obiektów w C++ (na przykład nazwy zmiennych) nie mogą b yć takie same jak słowa zarezerwowane. P ełny spi s sł ów zarezerwowanych w C++ znaj duj e s i ę w dodatku A. • Wszystkie s tałe i zmienne w C++ maj ą okre ślony typ . Typy fundam entalne w C++ ISO/ANSI to char, i nt , l ong, fl oat oraz double . W C++/CLI zdefini owane są dodatk owo typy Intl 6, Int 32 oraz Int 64. •
Nazwę i typ zmiennej definiuje s i ę w jej deklaracji , która zakończona jest ś redn i kiem. Podczas deklaracji zmiennym m o żna nadawać warto ści po czątkowe .
modyfikatora const m ożna chroni ć warto ść zmiennej . Zapobi ega on zmienianiu wartośc i zmiennej w programie i powoduje bląd podczas kompilacji, jeżeli jej warto ś ć jest zmieniana.
• Za
pomocą
bezpośredniemu
•
Domyślnie
wszystkie zmienne są automatyczne. Oznacza to, że istn i eją one tylko od momentu ich deklaracji do końc a zasięgu, w którym zostały zadeklarow ane, czyli do pierwszego zamykającego nawiasu klamrowego, który pojawi s ię po miej scu deklaracji tych zmiennych.
•
Zmienną można zad ekl arować
jako s t atyczną (st at i c). W takim przypadku b ęd zie ona istniała aż do zakoń czeni a programu . Do st ęp do niej można uzyskać tylko z miejsca znajdującego s i ę w jej zasi ęgu ;
• Zmi enne można defin iowa ć poza wszystkimi blokami w programie. W tak im przypadku mają one z as ięg globalny. Zmienne z globalnym zasięgiem przestrzeni nazw są dostępne w całym programie z wyj ątki em miejsc, w których i stn iej ą zmienne lokalne o tej samej nazwie. W takich przypadkach dostęp do nich mo żna uzyskać za pomocą operatora zas ięgu. •
Przestrzeń
nazw definiuje zakres, w którym po zarejestrowaniu każda nazwa musi być z kwalifikatorem przestrzeni nazw. O d nos ząc się do nazw z miejsc poza przestrzenią nazw, trzeba je poprzedzić odpowiednim kwalifikatorem.
używana
142
Visual C++ 2005. Od podstaw • Biblioteka standardowa C++ ISO/ANSI zawiera funkcje i operatory, których można używać we własnych programach. Wszystkie one należą do przestrzeni nazw st d. Główna przestrzeń nazw dla wszystkich bibliotek C++/CLI nosi nazwę Syst em. Dostęp do poszczególnych obiektów w przestrzeni nazw można uzyskać za pomocą nazwy przestrzeni nazw, stanowiącej kwalifikator i oddzielonej od niej operatorem dostępu nazwy obiektu. Dla wybranej nazwy z przestrzeni nazw można także użyć deklaracji using. • l val ue to obiekt , który może występować po lewej stronie tego typu ob iektów są zmienne niestałe .
wyrażenia. Przykładem
• W jednym wyrażeniu można używać różnych typów zmiennych i stałych, ale w razie potrzeby zostaną one automatycznie przekonwertowane na jeden typ. Jeśli okaże się to konieczne, to może zostać wykonana także konwersja typu z prawej strony wyrażenia na typ po lewej stronie. Jeśli typ z lewej strony nie może przechowywać takich samych danych jak typ po prawej stronie , to część z nich zostanie utracona, na przykład przy konwersji typu doubl e na typ i nt lub typu long na short . • Rzutowania typów można dokonać także w sposób jawny. Jeżeli w wyniku konwersji może doj ść do utraty części danych, to operacja ta zawsze powinna być wykonywana jawnie. Czasami zdarzają się sytuacje, w których trzeba zastosować rzutowanie w celu otrzymania żądanego wyniku . •
Słowo
kluczowe t ypedef
umożliwia
tworzenie synonimów różnych typów.
Mimo że opisałem już wszystkie typy fundamentalne , to jednak nie wyczerpałem jeszcze tego tematu . Istnieją także bardziej złożone typy danych , oparte na podstawowych, możliwe jest także tworzenie własnych oryginalnych typów. Z rozdziału tego wynika, kodowania: •
że programując
w języku C++/CLI ,
można przyjąć
trzy strategie
Powinniśmy
zawsze używać nazw typów fundamentalnych dla zmiennych, pamiętając synonimami nazw wartości typów klasowych w programach w C++/CLI. Dlaczego jest to takie ważne, dowiemy się, kiedy poszerzymy naszą wiedzę na temat klas. równocześnie, że są one
• W kodzie w C++/CLI powinniśmy korzystać z rzutowania bezpiecznego sa fe_cas t, a nie st at i c_cas t. Różnica będzie o wiele ważniejsza w kontekście rzutowania obiektów klas. Ogólnie rzecz biorąc, wyrobienie sobie nawyku używania rzutowania bezpiecznego zaoszczędzi nam wiele problemów. • Do deklarowania typów wyliczeniowych w C++/CLI kluczowych enum cla ss .
powinniśmy używać słów
Ćwiczenia Kod
źródłowy
pobrać
wszystkich przykładów w tej książce oraz ze strony http://helion.pl/ksiazki/vcppo .htm .
rozwiązania
do
ćwiczeń można
Rozdział 2.•
Dane, zmienne i dZiałania arytmetyczne
143
1. Napisz program w CH ISO/ANSI żądający podania jakiejś liczby i wysyłający ją wyjśc ie . Użyj
na
zm ienn ej lokalnej typu
całkowitego .
2. Napisz program, który przyjmuje wartość typu
całkowitego z klawiatury i zapisuje ją do zmiennej typu i nt oraz odnajduje dodatniąwartość reszty z dzielenia tej liczby przez osiem za p omocą operatorów bitowych (np. operator %nie może zostać użyty) . Na przykład wyrażenia 29 = (3x8l +5 i -14 = ( - 2x8l +2 m ają resztę z dzielenia odpowiednio 5 i 2.
3. Dodaj nawiasy do
poniższych wyrażeń,
tak żeby wskazywały one kolejność
wykonywania działań i łączność .
1+2+3+4 16 a
>
*
4 I 2 * 3
b? a. c > d? e: f
a&b&&c&d
pomocą poniższych
4. Za
kształtu obrazu
instrukcji stwórz program, który obliczy współczynnik Twojego mon itora, podając szerokość i wysokość w pikselach:
i nt widt h = 1280; i nt height = 1024; double aspect = width I height ; Jaką odpow iedź
otrzymasz po wyświetleniu wyniku na ekranie? Czy jest ona nie , to pomyśl , j akie poprawki należy wprowadzić do kodu, żadnych zmiennych.
zadowalająca?Jeżeli
nie
dodając
5. Dla zaawansowanych: jaki wynik da
poniższy
kod i dlaczego? Odpowiedz bez
wykonywania go na komputerze.
unsigned s
~
i nt i = (s cout « i;
»
555; 4) &- (-O
«
3);
6. Napisz program konsolowy w C++/CLl, który używa wyliczenia do identyfikacji mi esięcy
za
powinien
wyświetlić każdą stałą wyliczeniowąoraz jej podstawową wartość .
pomocą wartości
skojarzonych z
miesiącami
od l do 12. Program
7. Napisz program w C++/CLl, który oblicza powierzchnie trzech pomieszczeń w
zaokrągleniu
do
najbliższej pełnej
liczby metrów kwadratowych. Wymiary
pomieszczeń są następujące:
Pomieszczenie l: 10. 5 na 17,6; pomieszczenie 2: 12 .7 na 18. 9; pomieszczenie 3: 16,3 na 15 .4 . Program powinien także obliczać średnią oraz łączną powierzchnię tych trzech pomieszczeń. Za każdym razem wynik powinien być zaokrąglony do najbliższej liczby całkowitej.
144
Visual C++ 2005. Od podstaw
3
Decyzje i pętle
W tym rozdziale nauczymy się , w jaki sposób umożliwić programowi samodzielne podej mowanie decyzji, a także jak spowodować, aby program powtarzał wykonywanie określonych czynności tak wiele razy, aż zostanie spełniony pewien warunek. Pozwoli nam to obsłużyć różne ilości danych wejściowych , jak również sprawdzić ich poprawność . Nauczymy się także pisać programy wykonujące różne czynności zależnie od wprowadzonych danych oraz roz wiązujące różne zagadnienia logiczne. W tym rozdziale dowiesz się : •
Jak porównywać warto ści .
•
W jaki sposób
•
Jak
stosować
•
Jak
postępować
•
Jak
pisać
zmieniać kolejność
wykonywanych
czynności
w oparciu o wynik.
operatory logiczne i wyrażenia logiczne. w sytuacjach, w których do wyboru jest kilka
i używać
pętli
czynności .
w programach.
Zaczniemy od jednego z naj potężniejszych i fundamentalnych narzędzi w programowaniu: instrukcji porównujących zmienne i wyrażenia z innymi zmiennymi i wyrażeniami, które na podstawie uzyskanego wyniku podejmują określone czynności.
Porównywanie wartości Jeśli
nie chcemy podejmować nieprzemyślanych decyzji, musimy użyć mechanizmu porów nywania. Mechanizm ten wymaga poznania kilku nowych operatorów , tzw. operatorów rela cji. Ze względu na fakt, że wszelkie informacje w komputerze przechowywane są w postaci liczbowej (w poprzednim rozdziale dowiedzieliśmy się, w jaki sposób dane znakowe są repre zentowane za pomocą liczb), to porównywanie wartości liczbowych stanowi podstawę prak tycznie wszystkich procesów decyzyjnych. Dostępnych jest sześć podstawowych operatorów służących do porównywania dwóch wartości :
146
Visual C++ 2005. Od pOIlstaw niż
<=
mniejszy lub równy
większy niż
>=
większy
równy
1=
różny
<
mniejszy
>
lub równy
Operator rćwnosci s kłada się z dwóch znaków = umieszczonych j eden po drugim . Nie jest on równoznaczny z operatorem przypisania, który składa się z j ednego znaku =. Sto sowanie operatora przypisania zamia st operatora porównania j est częs tym błędem, któ rego należy się wystrz egać. Każdy
z tych operatorów porównuje wartości dwóch operandów i zwraca jedną z dwóch moż liwych wartości logicznych : true, jeżeli porównywanie da wynik pozytywny, lub fa l se, jeżeli porównywanie da wynik negatywny. Sposób działania tych operatorów prześledzimy na kilku prostych przykładach . Przypuśćmy, że mamy dwie zmienne całkowite i oraz j. Ich wartości to odpowiednio 10 i - 5. Spójrzmy na poniższe wyrażenia: i >j Każde
z tych
wyrażeń
Przypuśćmy także, że
char t t rst Poniżej
f i rst
~
podano kilka == 65
f i rst
zwróci wartość t rue. mamy zdefiniowane
last
' A' .
~
następujące zmienne:
'Z ';
przykładowych
<
15
i <= j +
j > -8
i != j
operacji
porównujących wartości
tych zmiennych:
'E' <= f i rst f i rst 1= l ast
l ast
Wszystkie powyższe wyrażenia porównują wartości kodów ASCII. Pierwsze wyrażenie zwróci wartość t r ue, ponieważ zmienna f i rst została zainicjalizowana wartością A, co w zapisie dziesiętnym odpowiada liczbie 65. Drugie wyrażenie sprawdza, czy wartość zmiennej f i rst (A) jest mniejsza od wartości zmiennej l ast (Z). Z danych zawartych w dodatku B wynika, że wielkie litery reprezentowane są w standardzie ASCII przez liczby w porządku rosnącym od 65 do 90 (wartość A to 65, a Z - 90). W związku z tym to wyrażenie również zwróci wartość true. Trzecie natomiast zwróci fl ase, gdyż wartość liczbowa litery Ejest większa niż war tość zmiennej fi rst. Ostatnie wyrażenie zwróci true, ponieważ A to zdecydowanie nie jest to samo co Z. Spójrzmy teraz na nieco bardziej skomplikowane mamy następujące zmienne:
wyrażenia
z użyciem operatorów porównania.
Załóżmy, że
int i = -10. j = 20; doubl e x = 1.5. Y = -0.25E-10;
Przeanalizujmy poniższy fragment kodu: - l < y j
2.0*x>=( 3 + y)
których wynikiem jest liczba, można używać jako operandów w wyra zajrzymy do tabeli priorytetów operatorów w rozdziale 2., to stwierdzimy, że żaden z zastosowanych tutaj nawiasów nie jest konieczny ; zostały one użyte w celu ułatwienia czytania kodu. Wynik pierwszego porównywania to t rue. Zmienna y ma
Jak
widać, wyrażeń,
żeniach porównujących . Jeśli
Rozdział3 .•
DecYlie i pętle
147
bardzo małą wartość ujemną - O. 000000000025, dzięki czemu jest większa od -1. Druga operacja porównywania zwróci wartość fal se. Wyrażenie 10 - i dało wynik 20, który jest równy warto ści zmiennej j . Trzecie wyrażenie ma wartość t rue, ponieważ wartość wyrażenia 3 + y jest nieco mniejsza niż 3. Operatorów relacji można używać do porównywania wartości dowolnego z typów podstawowych , tak więc teraz potrzebujemy tylko wiedzy na temat praktycznego wykorzystania zwróconych wyników w celu modyfikacji zachowań programu. .
Instrukcja warunkowa ił Instrukcja warunkowa i f w podstawowej formie pozwala programowi wykonać pojedynczą instrukcję lub cały blok instrukcji otoczony nawiasami klamrowymi , jeżeli wartością podanego wyrażenia warunkowego jest true , lub pominąć pojedynczą instrukcję lub blok instrukcji, jeżeli wartości ą wyrażenia warunkowego jest fal se. Zasady te przedstawiono na rysunku 3.1 w postaci schematu. if( warunek)
Rvsunek 3.1
I warunek ma wartość true
warunek ma wartość false
II Instrukcje
• Poniżej
znajduje
się
prosty
przykład
II Dalsze instrukcje
instruk cji warunkowej i f :
if(l ett er == 'A') cout « "Pi erwsza l itera alfabetu ." : Wyrażenie
warunkowe , którego wartość chcemy sprawdzić, znajduje się w nawiasie , bezpopo słowie kluczowym i f . Po nim znajduje się instrukcja, która ma zostać wykonana, w przypadku gdy warunek ma wartość t rue. Zwróćmy uwagę , gdzie jest średnik. Znajduje się on dopiero na końcu instrukcji wyj ściowej. Po wyrażeniu warunkowym nie powinno być śred nika, gdyż oba te wiersze tworzą razem jedną instrukcję . Warto także zauważyć, że instrukcja po wyrażeniu warunkowym i f jest wcięta, co wskazuje na to, że zostanie ona wykonana tylko wtedy, gdy zostanie spełniony warunek. Dla programu nie ma różnicy, czy stosujemy wcięcia, czy nie, ale nam samym łatwiej jest zorientować się w relacjach pomiędzy wyrażeniem warunkowym i f i instrukcją, która jest od niego zależna . Instrukcja wyjściowa z powyższego kodu zostanie wykonana tylko wtedy , gdy zmienna l etter będzie miała wartoś ć A. średnio
148
Visual C++ 2005. Od podstaw Możemy rozsze rzyć nasz przykładowy kod w taki sposób, aby zmieniał on wartość zmiennej
l ette r , jeżeli zawie ra ona
warto ść
A:
if (let t er == 'A' ) {
cout « "Pierwsza l itera alfabet u. let t er = ' a' : }
Blok instrukcji, który pod lega kontroli za pomocą instrukcji warunkowej i t, otoczo ny jest nawiasami klamrowymi. W tym przykładzie instrukcje w bloku zostaną wykonane tylko wtedy, gdy warunek (l etter = ' A') zostanie spełniony . Gdybyśmy nie zastosowali nawiasów klam rowych, to tylko pierwsza instrukcja pod legałaby kontroli instrukcji warunkowej i t , a instrukcja zmieniająca wartość zmiennej l etter byłaby wykonywana zawsze. Zauważ, że średnik znaj duje się po każdej instrukcji w bloku, ale nie po zamykającym nawiasie klamrowym. W bloku można umieścić dowolną li czbę instrukcji. W naszym przykładzie , jako że zmienna l etter ma wartość A, po wysłani u na wyjście takiej samej wiadomości jak za pierwszym razem zmie niamy tę wartość na a. J eże li warunek nie zostanie spełniony, to nie zostanie wykonana żadna z podanych instrukcji .
Zagnieżdżanie instrukcji warunkowych ił In stru kcj ą, która ma zostać wykon ana po spełnieniu warunku, może być inna instrukcja wa runkowa i t . Taki sposób użyc i a instrukcji warunkowych nazywa się za g nieżdża n ie m . Wyra żenie warunkowe wewnętrznej instrukcji if zosta nie sprawdzone tylko wtedy, gdy zostanie spełniony warunek instrukcji zew nętrz nej . Instrukcja warunkowa i t, która jes t zagnieżdżona w innej instrukcji warunkowej, również może zawierać kolejne instrukcje warunkowe. W rze czywistości instrukcje warunkowe i t można zagnieżdżać tak długo , jak długo sami orientu je my się w naszym kodzie.
~ Używanie zagnieżdżonych instrukcii warunkowych P on i ższy
kod zawiera przykład
użyc i a zagnieżdżonej
instrukcji warunkowej i t .
II Cw3_0/ .cpp
II Prezentacja zagnieżdżo nych instrukcji warunkowych if.
#incl ud e using std : .ci n: usi ng st d: :cout : using st d: :endl : int main() {
cha r letter cou t cin
« « »
=
O:
end l "W pi sz letter:
lit e r ę:
II Zmienna prz ech o wująca dane
wejś ciowe.
II Poproś o wprowadzenie dany ch. II Wczytaj literę.
Rozdział 3.
i f( letter
>~
'A' )
i f( l ette r
>~
i f ( letter
zosta ła
wie lka lite ra . "
'a' ) <~
149
II Sprawdź, czy wpr owadzona litera j est większa II lub równa 'A '. II Sprawdź, czy wprowadzona litera j est mni ej sza II lub równa '2'.
i f( l ette r <= ' Z' ) cout « endl
« "Podana « endl ;
ret urn O;
• Decyzje i pętle
II Sprawdź. czy wprowa dzo na litera j est większa II lub równa 'a'. II Sprawdź, czy wprowadzona litera j est mniej sza II lub równa 'z'.
' z' )
cout « endl
« "Podana zosta ł a ma ł a l itera. "
« endl ;
ret urn O; cout « endl « "Ni e podano ret urn O;
ża dnej
litery . " « endl ;
Jak to działa Powyższy
program rozpoczyna s i ę , jak zwykle, od dwóch wierszy komentarza. Następnie #i ncl ude dołączającą plik nagłówkowy zawierający funkcje obsługi operacji wej ś c i a -wyj ś c i a , deklaracje usi ng dla strumieni ci n i cout oraz znak końca wiersza, które zdefiniowane są w przestrzeni nazw st d. Pierwszą czynno ścią w funkcji mai n( ) jest prośba o wpisanie litery. Będzie ona przechowywana w zmiennej znakowej o nazwie l etter. mamy
dyrektywę
Znajdująca się poni żej instrukcj a warunkowa i t sprawdza, czy wprowadzona litera to A, czy j akaś inna. Jako że kody ASCII małych liter (97 do 122) są większe niż wielkich liter (65 do 90), wpisanie małej litery spowoduje, że zostanie wykonany pierwszy blok i t , ponie waż wynikiem wyrażenia (l etter>= ' A' ) będzie zawsze t rue. W tym przypadku sprawdzany jest warunek zagnieżdżonej instrukcji warunkowej, która sprawdza, czy podana litera to Z, czy jakaś o mniejszej wartości ASCII. Jeżeli jest to Z lub jakaś o niż szej wartości , to wiemy , że mamy do czynienia z wielką literą, wysyłamy wiadomo ść i koń czymy program za pomocą instrukcji ret urn. Obie instrukcje znajdują s i ę w nawiasach klamrowych , a więc obie zostaną wykonane, w przypadku gdy zagnieżdżona instrukcja warunkowa zwróci warto ść logiczną t rue. Nas tę p n a
instrukcja warunkowa sprawdza za pomocą w zasadzie takiego samego mechani zmu jak poprzednia, czy wpisany znak to mała litera, wyświetla informację i wraca.
Jeżeli podany znak nie jest literą, to wykonywany jest blok po ostatniej instrukcj i warunkowej. Jego zadaniem jest wyświetlenie wiadomo ści , że podany znak nie jest literą. Po tej czynności następuje koniec programu.
Jak widać , dzięki odpowiednim wcięciom można znacznie łatwiej zorientować si ę, jakie są relacje pomiędzy zagnieżdżonymi instrukcjami warunkowymi a instrukcjami wyj ściowymi .
150
Visual C++ 2005. Od podstaw Kod ten
może d ać n astępujący
wynik:
Wpis z l iterę: T Wpi sana zosta ła wielka litera. Można łatwo zmo dy fikować
kod w taki sposób, aby zamieni ał wielkie litery na m ał e. Doko namy tego, dodaj ąc tylko jedną in strukcj ę do bloku instrukcji warunkowej sprawdzaj ącej , czy wprowadzono wi elką, czy małą literę.
i f( lette r >= ' A' ) II Sprawdź, czy wprowadzona litera j est większa lub równa A. i f( lette r <= ' Z' ) II Sprawdź, czy wprowadzona litera j est mniej sza lub równa Z. (
cout
«
« «
endl
"Podana
zosta ł a
wiel ka l it era.";
endl :
letter += 'a ' ret urn O;
' A' ;
II Konwersj a do
małych
liter.
Dodana instruk cj a k on w ertuj ąc a wielk ie litery na małe zwięk s za wa rtość zmiennej l ette r o wartość równą a- A. Można tak zrobić , pon iew aż kody ASCII liter od A do Z i od a do z sta nowi ą dwie następuj ąc e po sobie grupy kodów numerycznych . W zw iązku z tym wynik wy rażen i a a - A stanowi wartość , którą należy dodać do wartości wielkiej litery w celu otrzyma nia jej m ałego odpowiednika. Równie dobrze mogliśmy s ko rzy s tać tutaj z odpowiednich kodów ASCII tych liter, ale uży liter, mamy pewn oś ć , że nasz kod zadzi ała również na komputerach, w których litery nie są kodowane za pom ocą standardu ASCII. Oc zywi ś cie wielkie i m ałe litery muszą być re prezent owan e przez dwie grupy n astępujących po sobie warto ści numery cznych .
wając
W C++ ISO/ANSI istniej e junkcja biblioteczna konwertująca litery na wielkie, a więc nie ma p otrzeby programować tej właściwośc i na własną rękę. Nazyw a s ię ona t oupper ( ) i znaj duje s ię w standardowym pliku bibliotecznym . Wię c ej na temat właściwości biblioteki standardowej dowiemy się, kiedy dojdziemy do szczegó łów na temat p isania funkcj i.
Rozszerzona instrukcja warunkowa ił Instruk cja warunkowa i f , której używal iśmy do tej pory, wykonuje pewne działania , je żeli zostanie sp ełn i ony podany warunek. Następnie program przechodzi do kolejnej instrukcji. Ist nieje także rozszerzona wersja instrukcji warunkowej i f , która pozwala na wykonanie jednego działan i a , j eżeli warunek zostanie sp ełn i ony , i innego działania w przec iwnym przypadku . N astępn ie program przechodzi do kolejnej instrukcj i. Jak już widzieliśmy w rozdziale 2., blok instrukcj i m oże zastąp ić poj ed yn cz ą in strukcję - zasada ta odnosi s i ę również do instrukcji warunkowych i f.
~ Rozszerzanie funkcjonalności ił Poniżej
znajduje si ę
przykład
zastosowania rozszerzonej instrukcji warunk owej i f.
Rozdział 3.
II Cw3_02.cpp II Używanie rozszerzo nej instruk cji warunkowej
• Decyzje i pętle
151
if
#i ncl ude usin g std: :ci n: usi ng st d: :cout : usi ng st d: :endl: int mai n() { l ong number ~ O: cout « endl «
"Wpr owad ź
II Zmienna do przecho wywania dany ch
l i c z bę
c a ł kowitą
wejśc iowych.
mni e jsz ą ni ż 2 mili ardy :
ci n » number : if ( number % 2l)
II Sprawdź
resztę
z dzielenia przez 2.
cout « endl II Jeśli reszta wyn osi l . prowadzona l iczba j est nie parzyst a . " « end l : « "W el se cout « endl II Jeśli reszta wynosi O. prowadzona l icz ba j est parzyst a. " « end l: « "W retu rn O:
Wynik
dz iałania
tego programu
może być następujący:
Wp rowa dź li c z b ę c a ł k ow i t ą m n ie j s zą n iż
dwa mili ardy : 123456
Wprowadzona l iczba j est parzyst a .
Jak lo działa Po zapisaniu podanej liczby do zmiennej number wartość jest sprawdzana poprzez uzyskanie reszty z dzielenia przez dwa (za pomocą operatora %, o którym mówiliśmy w poprzednim roz dziale) i użycie otrzymanego wyniku w warunku instrukcji warunkowej. W tym przypadku wyrażenie warunkowe zwraca wartość całkowitą, a nie logiczną. Instrukcja warunkowa traktuje wartości niezerowe zwrócone przez wyrażenie warunkowe jako t rue, a zerowe jako f al se. Mówiąc inaczej , poniższe wyrażenie warunkowe instrukcji warunkowej i f : (number % 2l)
jest równoznaczne z: (number %2L != O)
reszta z dzielenia wynosi jeden, to warunek ma wartość t rue i wykonywana jest in strukcja znajdująca się bezpośrednio po instrukcji warunkowej i f . Je żeli natomiast reszta z dzielenia wynosi zero, to warunek ma wartość fa l se i wykonywana jest instrukcja znajdująca się za słowem kluczowym e l se.
Jeżeli
Warunkiem instrukcji warunkowej j f może być wyrażenie. którego wynikiem j est wartość jednego z typów fundamentalnych omawianych w rozdziale 2. Jeżeli wartością wyrażenia jest liczba. a nie wartość logiczna. to kompilator dokona automatycznej konwersji wyniku na typ logiczny. Wartość niezerowa przekonwertowana na wartość logiczną odpowiada true, a zerowa - fa 7se.
152
Visual C++ 2005. Od podstaw Jako że w wyniku dzielenia liczby całkowitej przez dwa można otrzymać tylko resztę jed en lub zero, zaznaczyłem to w komentarzach do kodu. Bez względu na wynik, na końcu wyko nywanajest instrukcja return kończąca działanie programu. Po słowie kluczowym e 7se nie umieszcza s ię ś redn ika, podobnie jak w przypadku i f. Tak samo jak poprzednio, tym razem również zastoso waliśmy wcinanie, które ułatwia czytanie kodu. Dzięki niemu od razu widać, które instrukcje zostaną wykonane po otrzymaniu każ dego z możliwych wyników. Należy zawsze s tos ować wcinanie kodu, aby ukazać jego logiczną strukturę.
Instrukcja if -el se pozwala na wybór spomiędzy dwóch opcji. Ogólna zasada instrukcji widoczna jest na rysunku 3.2.
działania
tej
if{ warunek)
Rysunek 3.2
I warunek ma wartość true
l {
warunek ma wartość false
II Instrukcje )
I
else
+ {
II Dalsze instrukcje )
!+
II Jeszcze więcej instrukcji Strzałki na wykresie wskazują kolejność wykonywania instrukcji, w zwróconego przez warunek.
zależności
od wyniku
Zagnieżdżanie instrukcji warunkowych il-else Jak już
się przekonaliśmy,
instrukcje if można zagnieżdżać w innych instrukcjach if. Można instrukcje i f -el se w instrukcjach i f, instrukcje i f w instrukcjach i f el se, a także instrukcje if -el se w innych instrukcjach i f -el se. Ponieważ może się to wydawać nieco skomplikowane, przeanalizujemy kilka przykładów . Poniżej mamy in strukcj ę if-el se zagnieżdżoną wewnątrz instrukcji i f. również zagnieżdżać
Sprawdzanie, czy mamy pączki, wykonywane jesttylko wtedy, gdy sprawdzanie dotyczące kawy da wynik pozytywny, a więc nasze komunikaty właściwie odzwierciedlają sytuację, chociaż łatwo jest się tutaj pogubić . Gdyby w tym samym kodzie zastosowane zostały nie właściwe wcięcia, moglibyśmy się pomylić i wpisać nieprawidłowe komunikaty: i f(coffee == 'y' ) if( donuts == ' y' ) cout « "Mamy k a wę i pączk i . el se cout « "Ni e mamy kawy. .. " :
li Ta instrukcjajest źle II Źle !
wcię t a .
W tym przykładzie błąd jest łatwo zauważalny , ale w przypadku bardziej skomplikowanych struktur z użyciem instrukcji i f należy cały czas uważać , co zawiera dana instrukcja. Instrukcja el se zawsze należy do najbliższej poprzedzającej ją instrukcji i f . która nie jest jeszcze zajęta przez inną instrukcję el se. Można tę zasadę stosować w przypadkach, gdy mamy do czynienia ze skomplikowanymi strukturami, w celu rozszyfrowania, o co chodzi. Pisząc własny program, można zawsze dla większej przejrzysto ści stosować nawiasy klamrowe. W naszym przykładzie nie są one ko nieczne, ale moglibyśmy go zapisać w następujący sposób :
if(coffe e == ' y ' )
{ i f( donut s == ' y' ) cout « "Mamy k a wę i p ączk i . "; else cout « "Mamy h e r b a t ę . ale nie mamy kawy.
Kiedy znamy już podstawowe zasady, zrozumienie sposobu strukcjach if -el se będzie łatwe.
zagnieżdżania
instrukcji i f w in
if( cof f ee == ' y ' )
{ if(donut s == ' y ' ) cout « "Mamy
k a wę
if (t ea == ' y ' ) cout « "Mamy
h er ba tę .
i
pąc z k i
.";
} else al e ni e mamy kawy . " ;
W tym przypadku nawiasy klamrowe są niezbędne . Gdybyśmy je opuścili, to instrukcja el se należałaby do instrukcji i f , która sprawdza, czy są pączki. W tego typu sytuacjach łatwo można zapomnieć o klamrach i spowodować trudny do wykrycia błąd . Program zawierający tego typu błędy bez problemu poddaje się kompilacji i w niektórych przypadkach daje nawet prawi dłowy rezultat.
154
Visual C++ 2005. Od podstaw Po usunięciu nawiasów klamrowych z powyższego przykładu otrzymalibyśmy prawidłowy wynik tylko w przypadku, gdyby zmienne coffee i donuts były równe y, czyli gdyby instrukcja (t ea = y ) nie była wykonana. Poniżej znajduje się przykład instrukcji i f -el se zagnieżdżonych w instrukcjach if -el se. Tego typu struktury mogą być bardzo skomplikowane, nawet przy zagnieżdżeniu tylko na jednym poziomie.
if(coffee ~= 'y ' ) if(donuts == 'y ') cout « "Mamy kaw ę i pączk i . " ; el se cout « "Mamy kaw ę. ale nie mamy pączków . "; else if(tea = = 'y ') cout « "Nie mamy kawy. ale mamy he r ba t ę i moż e pą czk i . . . . else cout « "N ie mamy kawy ani herbaty . ale może c hociaż pąc zki ... " :
Logika powyższego kodu nie jest już taka oczywista, nawet przy zastosowaniu odpowiedniego wcinania. Nie ma konieczności używania nawiasów klamrowych, ponieważ wszystko się zga dza z regułą, którą sformułowaliśmy wcześniej . Gdybyśmy jednak wstawili klamry , to kod byłby trochę bardziej przejrzysty. if(coffee = = 'y' )
(
if(donut s =~ ' y' )
cout « "Mamy else
cout « "Mamy
k a wę
kawę.
i
p ą c zki. ";
al e ni e mamy
p ąc z ków. " ;
}
el se ( ifC tea = = 'y' ) cout « "N i e mamy kawy. al e mamy her ba tę i moż e pącz ki .. . "; el se cout « "Ni e mamy kawy ani herbaty. ale m o ż e ch oc iaż pączki . . . " ;
Do wykonywania tego typu operacji logicznych dostępne są lepsze metody. Zagnieżdżając coraz więcej instrukcji warunkowych, niemal dajemy sobie gwarancję, że gdzieś w końcu po pełnimy błąd . Poniżej opisuję, jak można to wszystko uprościć.
Operatory logiczne i wyrażenia Jak już się przekonaliśmy, używanie instrukcji warunkowych i f w przypadkach, gdy mamy do czynienia z dwoma lub większą liczbą powiązanych ze sobą warunków, może okazać się trochę nieporęczne. Łącząc nasz talent z instrukcjami warunkowymi i f , napisaliśmy instruk cje sprawdzające istnienie pączków i kawy, ale w praktyce może zajść potrzeba sprawdzenia o wiele bardziej skomplikowanych warunków. Może to być sprawdzanie danych personelu w celu odnalezienia osoby w wieku powyżej 21 lat, ale poniżej 35, która jest kobietą posia dającą dyplom, niezamężną oraz znającą język hindi lub urdu. Poprawne wykonanie takiego sprawdzania za pomocą instrukcji warunkowych i f graniczyłoby z cudem.
Rozdział 3.
• OecJzie i pęlle
155
Prostym i eleganckim rozwiązaniem takiego problemu jest zastosowanie operatorów logicz nych. Za ich pomocą kilka operacji porównujących można zawrzeć w jednym wyrażeniu logicznym, dzięki czemu wystarczy użycie tylko jednej instrukcji warunkowej i f , bez względu na stopień złożoności warunków, oczywiście, jeśli podejmowanie decyzji sprowadza się za każdym razem do wyboru pomiędzy dwiema opcjami (t rue lub f al se). Istnieją tylko
trzy operatory logiczne :
&&
Operator iloczynu logicznego AND
II
Operator sumy logicznej OR Negacja logiczna (NOT)
Operator iloczynu logicznego AND Operatora tego (&&) używamy w przypadkach, gdy mamy dwa warunki, z których każdy musi mieć wartość t rue, aby zwrócona została wartość t rue. Na przykład: chcę być bogaty i zdrowy. Operatora && można użyć do sprawdzania, czy dany znak jest wielką literą. Spraw dzana wartość musi być zarazem większa lub równa wartości kodu ASCII A i mniejsza lub równa wartości Z. Jeśli ma to być wielka litera, to oba warunki muszą mieć wartość t rue.
Tak jak poprzednio, warunki, które łączymy za pomocą operatorów logicznych, mogą liczbowe. Należy pamiętać, że w tym przypadku wartość niezerowa zastanie przekonwertowana na true, a zerowa na f a lse.
zwracać wartości
Biorąc
jako przykład wartość przechowywaną w zmiennej znakowej o nazwie l ette r, spraw dzanie złożone z dwóch instrukcji warunkowych i f możemy zastąpić wyrażeniem złożonym z jednej instrukcji i f oraz operatora iloczynu logicznego &&: if((l etter >= 'A' ) && (letter <= ' Z' )) cout « "To jest wielka litera. ";
Nawiasy wewnątrz wyrażenia warunkowego i f zapewniają, że operacje porównywania zo staną wykonane na samym początku, dzięki czemu instrukcja jest bardziej przejrzysta . W tym przypadku instrukcja wyjściowa zostanie wykonana, gdy oba warunki złączone operatorem && mają wartość t rue.
Podobnie jak w przypadku operatorów bitowych, o których mowa była w poprzednim roz dziale, efekt działania każdego z operatorów logicznych można przedstawić za pomocą tabeli prawdy . Dla operatora && przedstawia się ona następująco :
&&
false
true
false
false
false
true
fals e
true
~'\II~W,t},q, %~~V.~ UJ."ee.",~ ~ \~V.~ ",tt~l},,,~, S'>n~ " ~~%l}, lł?n ~7fc t",eept",~~~ill,S'\~~\b~h wyrażeń logicznych łączonych za pomocą operatora &&. Aby poznać wynik połączenia warunku o wartości true z warunkiem o wartości fa l se, n al eży sprawdzić wartość znajdującą się na przeci ęciu wiersza oznaczonego jako true z ko lumną o nagłówku 'false. W rzeczywistości nie potrzebujemy do tego żadnej tabeli, gdyż wartość true można uzyskać tylko wtedy, gdy oba warunki są t rue.
Operator sumy logicznej DR Operator sumy logicznej OR (II) ma zastosowanie, gdy mamy dwa warunki i chcem y otrzymać warto ść true, jeżeli przynajmniej jeden z nich ma wartość true. Na przykład bank może uznać , że kwalifikujemy się do otrzymania pożyczki , je śli zarabi amy 100 000 złotych rocznie lub mamy l 000 000 złotych w gotówce. Takie sprawdzanie można wykonać za pomocą nast ępującej instrukcj i warunkowej i f : i f ( ( i hcome >~ 100000 .00) II (capi t al cout « "Il e wie lce szanowny pan
>~
1000000.00) ) od nas
c hci a łby
pożyczyć ? " ;
Pochlebstwa posypią się na nas tylko wtedy, gdy co najmniej jeden z warunków ma wartość tru e (lepsze byłoby pytanie : "Czemu pan chce pożyczać pieniądze?" - zadziwiające jest, że banki chcą pożyczać pieniądze tylko tym, którzy już je mają). Dla operatora
II również można stworzyć tabelę prawdy:
II
false
true
false
fa l se
t rue
true
t r ue
t rue
W tym przypadku możemy otrzymać
również można w bardzo prosty spo sób okre śl i wynik : warto ś ć f al se tylko wtedy, gdy oba oper andy mają wartość fa l se.
Negacja logiczna NOT Trzeci operator logi czny! przyjmuje tylko jeden operand typu logicznego i odwraca jego wartość . Tak w i ęc je żeli zmienna test ma wartość t rue, to ! test ma warto ś ć fa l se. l podobnie, jeżeli zmienna t est ma wartość fa l se, to ! te st ma wartość t rue. Spójrzmy na prosty przykład : jeżeli zmienna x ma wartość 10, to wyrażenie : I
(x > 5)
wartość
ma
fa l se, ponieważ
Operatora ! możemy I
(dochód
>
także użyć
w ulubionym
x > 5 jest true.
wyrażeniu
Karola Dickensa:
wydatk i)
Jeżeli wyrażenie
bank zacznie
wartością wyrażenia
to ma wartość true, to mamie z nami, przynajmniej od momentu, w którym nasze czeki.
zwracać
Rozdział 3.
• Decyzje i pętle
157
Operatora ! możemy także używać z innymi podstawowymi typami danych . Przypuśćmy , że mamy zm ienną typu zmie nnopozycyjnego o nazwie rate i wartości 3. 2. Może zdarzyć się sytuacja, w której chcemy sprawdzić, czy wartość zmiennej rat e nie jest zerowa. W takim przy padku możemy u żyć następującej instrukcji: I
(rate )
Wartość 3. 2 jest różna od zera, a więc zostanie przekonwertowana na co z kolei spowoduje, że wynik tego wyrażenia będzie fa l se.
wartość logiczną
t rue,
l!mImI ~ączenie operatorów logicznych Operatory i wyrażenia logiczne można dowolnie łączyć . Na przykład sprawdzanie, czy zmien na zawiera literę, można wykonać za po mocą pojedynczej instrukcji warunkowej i f. Przed stawiam to na poniższym listingu : II Cw3_03.cpp
II Sprawdzanie za pomo cą operatorów logiczny ch, czy zmi enna zaw iera
literę.
#i ncl ude using std: :ci n: usi ng std: :cout: using std: :endl: int mai n() (
char l et t er = O: cout « endl « "Wpisz ci n » l etter :
j a ki ś
II Zmi enna do p rzechowywania II wprowadzonych dany ch.
znak :
if«( letter >= 'A') && ( let t er <= ' Z' ) ) II «letter >= ' a' ) && (letter <= ' z' ll) cout else cout
« «
end l "Wprowad zony znak to l i te ra . "
« «
end l "Wprowadzony znak ni e jest
«
II Sprawdzanie, czy mamy do czynienia II z literą .
endl :
lit e r ą . "
«
endl :
retu rn O;
Jak to działa Program ten rozpoczyna się tak samo jak w przykładzie Cw3_01.cpp - od żądania wprowa dzenia znaku i jego wczytan ia. Najbardziej interesującą częścią tego programu jest wyrażenie warunkowe zawarte w instrukcji warunkowej i f : Składa się ono z dwóch wyrażeń logicznych połączonych operatorem II, dzi ęki czemu jeś li któryś z nich ma wartość t rue, zostanie zwró cona wartość t r ue oraz wyśw iet lony komunikat: Wp rowadzony znak to litera .
158
Visual C++ 2005. Od podstaw Jeżeli wartośc i
obu
wyrażeń
logicznych
są
f al se ,t o wykonywana jest instrukcja el se, która
wyświet la wiadomość :
W prowadzony znak nie jest
li terą.
W każdym wyrażeniu logicznym znajdują się dwa porównania z użyciem operatora &&, a więc oba porównania muszą zwrócić t rue, aby całe wyrażenie rów nież zwróciło true. Pierwsze wyrażenie logiczne zwraca tr ue, jeżeli wprowadzona zostanie wielka litera, a drugie - jeże li wprowadzona zostanie mała litera.
Operator warunkowy Operator warunkowy nazywany jest czasami operatorem tró j a rg ume ntowy m, ponieważ wymaga on aż trzech operandów. Najlepiej jest to wyjaśnić na przykładzie . Przypuśćmy , że mamy dwie zmienne a i b. Wartość tej, która j est więk sza, chcemy przypisać zmiennej c. Możemy tego doko nać za pomocą następującej instrukcji :
c=a
>
b ? a : b:
II Przypisz zmiennej c II która jes t większa .
wartość
zmi ennej a lub b, w zależn ości od tego,
Pierwszy operand operatora warunkowego musi być wyrażeniem , którego wynikiem jest war logiczna - w tym przypadku jest to wyrażenie a > b. Jeżeli wyrażenie to zwróci wartość t rue, to jako rezultat tej operacji zwróco ny zosta nie drugi operand, czy li a. J eże li natomiast pierwszy argument zwróci wartość f al se, to trzeci operand, tutaj b, zostanie wybrany jako war tość całej operacji . A zatem wartością wyrażenia warunkowego a > b?a :b bę dz ie a, jeże l i a jest większe od b, lub b w przeciwnym przypadku. Użyc ie operatora warunkowego w tej in strukcji przypisania jest równoznaczne z następuj ącą i n stru kcj ą i f :
to ś ć
if(a
> b )
c = a:
else
c
~
b:
Operator warunkowy
warunek ?
m o żn a przedstaw i ć
wyrażenie]
:
za
po mocą następ uj ącego
ogó lnego wzoru :
wyrażenie2
Jeże li
wynikiem warunku jest t rue, to zwrócona zostanie wartość pierwszego wyrażenia, w prze ciwnym przypadku zwrócona zostanie wartość drugiego wyrażen ia .
~ Używanie operatora warunkowego zdanymi wyjściowymi Operatora warunkowego najczęściej używa s ię w celu kontrolowania danych wysyłanych na wyjście - zależnie od wyniku wyrażenia lub wartości zmiennej. Można spowodować wysłanie różnyc h komunikatów w za leż ności od okreś lonego warun ku. II Cw3_04.cpp
II Operator warunk owy sterujący danymi
#incl ude usi ng std : :cout:
wyjś c iowym i .
Rozdział3.
• Decyzje i pętle
159
using st d: :endl: int main ( ) { int nCakes
=
l:
II Licznik liczby ciastek.
cout « endl « "Mamy" « nCakes « « endl :
" cias t " «
((nCa kes > 1)
"ka." : "ko, ")
" ci ast" «
(( nCa kes > 1) ? "ka."
++nCakes: cout « endl « "Mamy " « «
nCakes «
"ko. " )
endl :
retu rn O:
W wyniku
działania
tego programu otrzymamy:
Mamy 1 cias tk o. Mamy 2 ci ast ka.
Instrukcja switch Instrukcja swi tc h pozwala na wybór spośród wielu opcji na podstawie zbioru określonych wartości danego wyrażenia. Przypomina ona prawdziwy przełącznik obrotowy, z tym że można wybrać jedną z kilku dostępnych opcji. W niektórych pralkach na przyklad w taki spo sób wybiera się odpowiedni program prania. Podanych jest kilka możliwych ustawień prze łącznika, takich jak bawełna, wełna, włókna syntetyczne itd., i za pomocą przełącznika wybie ramy interesującą nas opcję . W instrukcji swi t ch wybór opcji uzależniony jest od wartości określonego wyrażenia . Poszcze gólne opcje do wyboru definiuje się za pomocą słowa kluczowego case. Określona opcja zosta nie wybrana, jeżeli wartość wyrażenia swi t ch równa będzie jej wartości . Dla każdego moż liwego wyboru jest tylko jedna wartość case, ale wszystkie wartości case muszą być różne. Każda opcja definiowana jest oddzielnie za pomocą słowa kluczowego case.
swi t ch nie pasuje do żadnej ze zdefiniowan ych opcji, to automatycz nie wybierana jest opcja domyślna (default). Opcję domyślną można w razie potrzeby okre ś l i ć samodzielnie, co zrobimy w poniższym przykładowym kodzie. Jeżeli jej nie zdefiniujemy, to nie będzie ona robiła nic.
Jeżeli wartość wyrażenia
~ Instrukcja switch Na
poniższym przykładzie prześledzimy
II Cw3_05.cpp
II Używanie instruk cji switch.
#i ncl ude
sposób działania instrukcji switch.
160
Visual C++ 2005. Od podstaw
using st d: :ci n:
using st d: :cout :
using st d: :endl :
int mai n()
{
i
nt cho i ce
=
O:
II Zmienna do przechowywa nia
wa rtości
wybranej opcji.
cout « endl
« "Twoj a elektroniczna ks ią żka kucharska jest do t woj ej dyspozycj i ." « "Do wyboru ma sz jedno z po n i żs zyc h pysznych d a ń :
Jak to działa Po zdefiniowaniu opcji w wyrażeniu wyjściowym i zapisaniu numeru wybranej opcji w zmien nej choice wykonywana jest instrukcja switc h z warunkiem w postaci zmiennej choice umiesz czonej w nawiasach znajdujących się bezpośrednio po słow ie kluczowym swit ch. Możliwe opcje wyboru w instrukcji switc h znajduj ą s i ę pomiędzy nawiasami klamrowymi i każda z nich zdefiniowana jest za pomocą słowa kluczowego case, po którym następuje warto ść zmiennej cho i ce, odpowiadająca tej opcji . Na końcu znajduje s i ę ś redn i k . Jak widać , instrukcje, które mają zostać wykonane w przypadku wyboru każdej z opcji, napi sane są po dwukropku na końcu każdej etykiety case, a ich działan ie kończone jest i nstrukcj ą break, która przenosi wykonywanie kodu do pierw szej instrukcji po switch. Zastosowanie break nie jest obowiązkowe, ale bez niej wykonane zostałyby wszystkie pozostałe instrukcje, czego zazwyczaj nie chcemy . Aby sp raw dz ić , co si ę stanie, mo żn a usunąć instrukcje brea k z powyższego przykładu .
Rozdział 3.
• Decyzje i pętle
161
zmiennej cho i ce nie odpowiada żadnej z warto ści case, to wykon ywana jest instrukcja default. Nie jest ona n iezbędna . Jeżeli jej nie ma i warto ś ć wyrażenia sprawdzają cego nie pasuje do żadnej opcji case, to następuje wyjście z instrukcji swit ch i program jest kontynuowany od instrukcji, która znajduj e s ię bezpośrednio po switch. Jeżeli wartość
~ WsPóldzielenie jednego przypadku Każde
z wyrażeń , które określamy w celu ident yfik acji opcji , musi b yć s tał e, aby w arto ś ć w czasie kompilacji, oraz jego wynikiem musi być unikalna warto ść typu całkowitego . Powodem, dla którego dwie stałe wartoś ci case nie mogą by ć takie same, jest fakt, że kompilator nie wiedziałby, na którą z nich s ię zdecydować . Oczywi ście , różne opcje case nie muszą wykonywać unikalnych czynności . Kilka opcji case może dzielić jedną czyn ność , jak pokazano poni żej . mo żna było określić
II Cw3_06.cpp
II Używan ie wielu czyn ności case.
#include using std : .ct n:
usi ng std: :cout ;
using std : .endl :
int mainO {
char l et t er = O:
cout « endl
« "Podaj ma ł ą ci n » letter;
l it e r ę :
switch(letter*(letter >= 'a ' && l ette r <= ' z' ) ) {
case case case case case case
' a ': 'e' : ' i' : ' o' : 'u ' : 'y ' :
cout « endl « "W prowadzona l itera to break:
case O: cout « end l « "To nie je st break:
ma ł a
samogło sk a.":
litera." :
default : cout « endl « "Podana l i t era t o
spó łgłosk a . ";
}
cout « endl :
retu rn O:
dak to działa W tym przykładzie w instrukcji switch mamy bardzi ej skomplikowane wyrażenie . podan y znak nie jest małą literą, to wynikiem poniższego wyrażenia jest fal se:
Je żeli
162
Visual C++ 2005. Od podstaw (l et t er
>~
'a' &&let t er
<=
'z' )
W przeciwnym przypadku wynikiem będzie t rue. Jako że wartość zmiennej l etter jest mno przez to wyrażenie , warto ść wyrażenia logicznego jest konwertowana do typu całkowi tego (O, jeśli wartość wyrażenia to fa l se, oraz l, je śli warto ść wyrażenia to t rue). A zatem wyrażenie swit ch ma wartość O, jeżeli nie zos ta ła wprowad zona mała litera, lub równą warto śc i zmiennej letter, jeżeli została wprowadzona mała litera. Instrukcje znajdujące s i ę po opcji case Owykonywane są za każdym razem, gdy kod znaku przechowywany w zmiennej letter nie reprezentuje małej litery .
żona
Je żeli zostanie wprowadzona mała litera, to wyrażenie swi t ch przyjmuje taką samą wartość jak zmienna letter, a więc dla wszystkich warto ści odpowiadających samogłoskom zostanie wykonana instrukcja następująca po sekwencj i etykiet case, których wartościami s ą samo głos ki . To samo wyrażenie wykonywane jest dla każdej samogłosk i, ponieważ gdy wybrana zostanie którakolwiek z tych etykiet, wykonywane są następujące po niej instrukcje aż do napotkania instrukcji break. Jak widzimy, poprze z zapisanie przed instrukcjami kilku opcji jedna po drugiej w przypadku wystąpienia dowolnej z tych opcji zostanie wykonana poje dyncza czynność. Jeżeli podana zostanie mała s p ółgłosk a, to wykonana zostania instrukcja domyślna.
Przejście bezwarunkowe W zależności od warunku instrukcja warunkowa i f pozwala dokonać wyboru , czy wykonać jeden zbiór instrukcji, czy drugi . W ten sposób kolejność wykonywania instrukcji w pro gramie u zależniona jest od wartości danych w programie. Instrukcja goto jest natomiast "tępym" narzędziem pozwala bezwarunkowo przejść do wykonywania określonej in strukcj i. Instrukcja, do której ma nastąpić przejście , musi zostać opatrzona identyfikatorem definiowanym według tych samych zasad co nazwy zmiennych. Znajduje się on przed instruk cją wymagającą etykiety , a po nim znajduje si ę dwukropek. Poni żej znajduje s i ę przykładowa instrukcja z identyfikatorem:
mylabel : cout Powyżs za
«
" P r z ej ś c i e
do myla bel
zos t a ł o
instrukcja ma identyfikator myla bel.
aktywowane"
Przej ście
«
endl:
bezwarunkowe do tej instrukcji wy
gląda następująco:
got o myl abel : Powinno się unikać używania instrukcji goto, o ile to możliwe . Powoduje ona, że kod staje bardzo zawiły i niezwykle trudn y do rozszyfrowania.
się
Jako że instrukcja goto teoretycznie nie jest potrzebna (zawsze można to samo wykonać w inny sposób), wielu programistów uważa, że nie powinno się j ej w ogóle używać. Ja nie podzielam aż tak skraj nych poglądów. Mimo wszystko jest ona częś ciąjęzyka i w pewnych sytuacjach j ej użycie może być bardzo wygodne. Zachęcam j ednak do używania j ej tylko w takich przypadkach, gdy widzimy, że j ej zastosowanie da nam znaczne korzyś ci w po równaniu z innymi wyjściami. W przeciwnym przypadku może się okazać, że nasz kod stanie s ię bardzo zawiły oraz trudny do zrozumienia i utrzymania.
Rozdział 3.
• Decyzje i pętle
163
Powtarzanie bloków instrukcji
Zdolność
powtórnego wykonywania grup instrukcji jest jedną z fundamentalnych cech każ dego programu. Bez tego firmy musiałyby modyfikować programy obsługujące listy płac za każdym razem, gdy zatrudniany jest nowy pracownik, a gracz, aby zagrać jeszcze raz w grę Halo 2, musiałby ją uruchomić ponownie. Na początek musimy zrozumieć sposób dzia łania pętli .
Czym iest pętla Pętla
wykonuje szereg instrukcji aż do chwili, gdy zostanie spełniony pewien warunek (ma t r ue lub fal se). Tak naprawdę, dysponując zdobytą dotychczas wiedzą, możemy już stworzyć pętlę. Potrzebujemy do tego instrukcji warunkowej i f oraz niesławnej goto. Spójrzmy na następujący przykład :
wartość
II Cw3_07.cpp
II Tworzeni e pętli za pomocą instrukcj i warunkowej
if i instrukcji goto.
#i nclude using std: :ci n:
usi ng std: :cout :
usi ng std : :endl :
i nt mai nO (
i nt i = O. sum ~ O:
const int max = 10:
i = 1: l oop:
sum+= i: i f( H i <= max)
goto loop: cout
« « « « «
II Dodaj
=
"
zmiennej i do zmiennej sum .
II Wracaj do pętli, dopóki i nie b ędzie równ e 11.
endl
"sum= " endl .
"i
bieżącą wartość
«
sum
«
i
endl:
ret urn O:
Przykład
ten oblicza sumę liczb całkowitych ze zbioru l - 10. Za pierwszym razem zmienna l, która zostaje dodana do zmiennej sumo wartości początkowej wynoszącej O. W instrukcji warunkowej i f wartość zmiennej i zwiększona zostaje do 2 i - dopóki jest ona mniejsza od max - następuje bezwarunkowe przej ście do loop, zaś wartość zmiennej i, teraz wynosząca już 2, dodawana jest do zmiennej sum. Sytuacja ta powtarza się tak długo, aż zmienna i będzie miała wartość 11 i nie zostanie wykonane przejście bezwarunkowe z powro tem do instrukcji o identyfikatorze loop. Po uruchomieniu tego programu otrzymamy wynik: i ma
wartość
sum = 55
164
Visual C++'2005. Od podstaw i = 11
Na tym przykładzie do ść wyraźnie wid a ć sposób dzi ałania pętl i , cho ć nasza p ętla używa in strukcji goto i wprowadza dodatkową etykietę do programu , czego powinni śmy w miarę moż liwośc i un ikać . Taki sam wynik, a nawet więcej , możemy uzyskać przy zastosowaniu następ nej instrukcji, która zo stała specjalnie do tego stworzona .
~ Używanie pętli lor Pow y ższy przykł adowy
kod
m ożna zm odyfikować, s tos uj ąc
tak
zw aną pętlę
for.
II Cw3_0S.cpp II Dodawanie liczb ca łko witych za po mocą pętli f or.
#i ncl ude usi ng st d: :ci n:
usi ng st d: :cout:
using st d: :endl:
int mai n() {
int i = O. sum= O:
const int max ~ 10 :
for Ci = 1: i <= max: i ++) sum+= i: cout « end l
« "sum= " « end l
« «
"i
= " «
II Wyrażenie sterujące pę tli.
II Instrukcj a pę tli.
sum
«
i
end l :
return O:
Jak to działa Po skompilowaniu i uruchomieniu tego programu otrzymamy dokładnie taki sam wynik jak w poprzednim przypadku, ale użyty tutaj kod jest znacznie prostszy. Warunki sterujące zacho waniem pętli znajduj ą s i ę w nawiasach po słowie kluczowym for. Zn ajduj ą się tam trzy wyra żen i a oddzielone przecinkami: • pierwsze wyrażenie wykonywane je st jeden raz na początku działani a pętli i ustawia początkowe warunki . W tym przypadku ustaw ia wartość zmiennej i na 1; • drugie wyrażenie stanowi wyrażenie logiczne, które okre śla, czy instrukcja pętli (lub blok instrukcji) powinna nada l b y ć wykonywana. Jeże li ma warto ś ć fa l se, to d ziałanie p ęt l i koń czy s i ę i wykonywanie programu j est kontynuow ane od instrukcji występujących po pętli . W naszym przypadku instrukcja pętli będzie wykonywana tak dług o , aż zmienna i będzie miała wartość mniej szą lub równ ą max;
Rozdział 3.
•
• Decyzje i pętle
165
wartość
trzeciego wyrażenia obliczana jest po wykonaniu instrukcji pętli (lub bloku instrukcji). U nas zwiększa ona wartość zmiennej i o jeden w każdym powtórzeniu.
W rzeczywistości pętla ta nie jest taka sama jak przedstawiona w przykładzie Cw3_0 7.cpp . Aby to sprawdzić, można ustawić wartość zmiennej max na zero w obu programach, a następ nie je uruchomić. Stwierdzimy, że zmienna sum w programie Cw3_0 7.cpp ma wartość l, a w programie Cw3_08.cpp - O. Wartości i również są różne . Jest to spowodowane tym, że w wersji z if pętla wykonywana jest zawsze co najmniej raz, ponieważ warunek sprawdzany jest dopiero na samym końcu . W przypadku pętli for jest odwrotnie - warunek sprawdzany jest na samym początku. Ogólna forma for
pętli
for przedstawia
( wyra żenie_inicjalizujące
;
się następująco:
wyrażenie_sprawdzają ce
wyrażenie_modyfikujące)
instrukcja-Pę t 7 i ;
Oczywiście instrukcja pętli może być pojedynczą instrukcją lub całym blokiem instrukcji w nawiasach klamrowych. Kolejność wykonywania poszczególnych operacji w pętli for pokazano na rysunku 3.3.
Jak już powiedziałem , instrukcja pętli pokazana na rysunku 3.3 może być również blokiem instrukcji. Wyrażenia sterujące pętlą for są bardzo elastyczne. Można nawet napisać dwa lub większą liczbę wyrażeń oddzielonych przecinkami. Daje to duże możliwości zastosowania pętli fo r.
Różne sposoby użyci a pę tli lor W większości przypadków wyrażenia w pętli f or używane są w standardowy sposób : pierw sze służy do inicjalizacji jednego lub większej liczby liczników pętli, drugie do sprawdzania, czy powtarzanie powinno być kontynuowane, zaś trzecie do zwiększania lub zmniejszania jednego lub większej liczby liczników pętli. Nie ma jednak obowiązku używania tych wy rażeń w taki właśnie sposób . Jest kilka innych możliwości. Wyrażenie inicjalizujące w pętli for może zawierać deklarację zmiennej pętlowej . W poprzed nim przykładzie mogliśmy zapisać pętlę w taki sposób , aby na pierwszym miejscu wyrażenia kontrolnego zawierała deklarację licznika pętli i.
for (i nt i = 1; i sum += i;
<=
max; i ++)
II Specyfikacja pętli.
II Instrukcja pętli.
Oryginalna deklaracja i musiałaby zostać pominięta w programie. Jeżeli dokonamy tej zmiany w ostatnim przykładzie , to stwierdzimy, że nie chce się on już skompilować, ponieważ zmienna pętlowa i przestaje istnieć poza pętlą, a więc nie można się do niej odwołać z instrukcji wyj ściowej. Zasięg pętli ograniczony jest z jednej strony słowem kluczowym for i rozciąga się na całe ciało pętli , którym może być pojedyncza instrukcja lub blok instrukcji. Licznik i jest w tej chwili zadeklarowany w zasięgu pętli, a więc nie można się do niego odwołać z instrukcji wyjściowej, gdyż znajduje się ona poza zasięgiem pętli. Domyślnie kompilator wymusza sto sowanie się do standardu C++ ISO/ANSI i nie dopuszcza do tego, by do zmiennej zdefinio wanej wewnątrz warunku pętli można się było odwoływać spoza niej. Jeżeli chcemy używać
166
Visual C++ 2005. Od podstaw
Rysunek 3.3
Wykonaj wyrażenie _inicjalizujące
wyrażenie _ sprawdzające
Nie
>- - _ ---,
ma w artoś ć true Tak
Wykonaj instrukcję _pętli
Wykona j wyrażenie _modyfikujące
Przejdź
do nas tępnej instrukcji
wartości w liczniku po zakończeni u wykonywania pętli, musimy nika poza z a sięgi em pętli . Możemy c ałkiem pominąć wyraże nie i nicj al i zujące
je my zm ienn ą i w deklaracji , to pętlę
w
zadekl arować zm ienn ą
pętl i . Je żeli
licz
odpowiednio zainicjalizu
możemy z ap i sać następująco:
i nt i = l: rorr : i <= max: i++ ) II Specyfi kacja pętli. sum += i; II Instrukcj a pętli.
Ś rednik oddzi e l aj ący wyrażenie inicjalizujące od wyrażen ia sprawdzaj ące go nadal je st po
trzebny. W
rzeczywi stości
zawsze obecne bez względu na li czbę nie napisali pierwszego ś redni ka, kompilator nie potrafiłby zos tało p ominięte ani który śred n i k zo stał pominięty .
oba
ś re dn i ki musz ą być
pomin iętych wyrażeń . Gdybyśmy zdecydować ,
Instrukcja
które wyraże n ie
pętli może być
byśmy umi eścić
w
pusta. Na przykład instrukcję pętli z poprzedniego przykładu mogli inkrementacyjnym. W takim przypadku pętla przedstawia s i ę
wyrażeniu
następująco :
for( i = 1: i <= max: sum += i ++) :
Po
zamykającym
pętli
II Ca/a pęt/a.
nawiasie nadal musi znajdować s i ę ś redn i k, który informuje , że instrukcja jest pusta. Jeżeli go nie wstawimy, to instrukcja znajdująca s ię bezpośredn io po pętli
Rozdział 3. •
zostanie potraktowana jako jej instrukcja. Czasami w osobnej linijce :
for(i = 1; i <= max : sum += i++ )
można spotkać
Decyzje i pęile
167
zapis pustej instrukcji
pętli
II Ca /a pęt /a.
~ UiYWanie wielu liczników Za pomocą operatora przecinkowego w pętli for można umieszczać Przykład takiego zastosowania widać na poniż szym listingu :
więcej n i ż
jeden licznik.
/I Cw3_09.cpp
II Używan ie wielu liczników w celu obliczenia pot ęg cyf ry 2.
#incl ude #include using using usi ng using
st d: .c m :
st d: :cout:
st d: :endl :
st d: :setw:
int main t ) {
long i = O. power = O:
const int max = 10:
for (i = O. power ~ l : i <= max: i++. power += power) cout « end l « set w( 10) « i « setw(10) « power : II Instruk cj a pętli. cout « endl : retur n O:
Jak to działa W pierwszej części pętli for zainicjalizowali śmy dwie zmienne, oddzie lając je operatorem przecinkowym , a w części inkrementacyjnej wskazaliśmy , że każda z nich ma być zw i ęk szan a. W każdej części pętl i możemy umieścić dowolną liczbę wyrażeń. Można
na
nawet podać kilka warunków oddzi elony ch przecinkami w części przezna czonej ale tylko ostatni warunek ma wpływ na to, kiedy pętla zakoń
wyrażenia sprawdzające,
ery działanie.
przypisania d e fi n i uj ąc e wartości początkowe zmiennych i oraz power a nie instrukcjami . Instrukcja zawsze zakońc zona jest średn ikiem.
Zauważmy , że
żeniami,
są
wyra
Za każdym razem, gdy zmienna i jest zwi ększana, zmienna power jest podwajana. W ten spo sób uzyskujemy warto ści potęgowe dwójki . Wyn ik programu przedstawia s i ę następująco: O l 2
l 2 4
168
Visual C++ 2005. Od podstaw 3
8
4
16
32
5 6 7 8 9
64
128
256
512
1024
10
Manipulator set w( ), o którym mówil iśmy już w poprzednim rozdz iale, służy do ułożenia wy ników w odpowiedni sposób. Dzięki dołączeniu pliku nagłówkowego oraz doda niu deklaracji using dla nazwy manipulatora w przestrzeni nazw std możemy używać nazwy setw( ) bez kwalifikatora.
~ Wykonywanie bez końca Je żeli
pominiemy drugie
przyjęta domyślna warto ść
wyrażen ie
kontrolne, które
określa
warunek dla
pętli ,
to zostanie
t rue. Spowoduje to wykonywanie pętli bez końca, chyba że dostar
czymy jakiegoś innego sposobu na wyjście z niej. Tak naprawdę możemy pominąć nawet wszystkie wyrażenia w nawiasie po słowie kluczowym for. Może się to wydawać niezbyt przy datne, ale jest akurat odwrotnie. Często zdarza si ę, że chcemy, aby pętla powtórzyła się kilka razy, ale nie wiemy ile. Spójrzmy na poniższy listing: II Cw3_ / 0.cpp
II Używan ie nieskoń czon ej pętli do obliczania ś redniej.
#include using st d: .cin:
using st d: :cout :
usi ng st d: :end l :
i nt mai n() (
double va l ue = 0.0: double sum = 0.0: i nt i = O: char i nd icat or = 'n' :
II Zmie nna przechowująca II Suma wartości.
II Liczn ik liczby wartości.
II Kon tyn uo wać czy nie?
fort : :)
II Pętla n ieskończona.
wprowadzoną wartość.
{
cout
« «
ci n »
endl
"Podaj value:
++ i :
sum += val ue:
j a k ąś wart o ś ć :
II Wczytaj wartość . II Zwiększ licznik. II Dodaj b ieżące dane wejściowe do sumy ogó lnej.
cout
endl "Czy chcesz pod a ć jeszcze j edną warto ś ć (wpisz n. aby cin i ndicat or : II Wczy taj wskaźn ik. i f « i ndicat or == 'n' ) II (indi cat or == ' N' )) break : II Wyjdź z pętli. « « »
cout
«
endl
z ak o ń c zyć ) ?
Rozdział3.
«
" Ś redn ia " «
wpisanych « endl :
ret urn O;
«
"
i
wa rtości
• Decyzje i pęlle
169
to " « sum/i «
Jak to działa Program ten oblicza średnią dowolnej liczby wartości. Po podaniu wartości musimy poinfor mować program, czy chcemy dodać jeszcze jedną wartość, czy nie, poprzez wpisanie litery t lub n. Wynik tego programu może być następujący:
Podaj j akąś wa r toś ć : la
Czy chcesz podać jeszcze je dną wartość (wpisz n. aby Podaj j a k ą ś wartość: 20
Czy chcesz pod a ć jeszcze j e d n ą wartość (wpisz n. aby Podaj j akąś wartoś ć: 30
Czy chcesz po dać jeszcze j edn ą war t o ść (wpisz n. aby Ś re dn ia 3 wpisanych war tości to 20.
z a k o ń c zyć ) ?
t
zakończyć)?
t
zakoń czyć )?
n
Po zadeklarowaniu i zainicjalizowaniu zmiennych , których mamy zamiar użyć, pętla for roz poczyna się bez żadnych wyrażeń, a więc nie ma powodu do jej zakończenia w tym miejscu . Blok znajdujący się bezpośrednio po niej to temat pętli , który ma być powtarzany. Blok
pętli
wykonuje trzy
czynności :
•
wczytuje wartość,
•
do zmiennej sumdodaje
•
sprawdza, czy chcemy
wartość wczytaną ze
strumienia wejściowego ci n,
kontynuować wprowadzanie wartości.
Najpierw blok prosi nas o podanie jakiejś wartości , a następnie wczytuje ją do zmiennej val ue. Podana wartość zostaje dodana do zmiennej sumi zwiększony zostaje licznik liczby podanych wartości i . Po dodaniu wartości do sumy ogólnej w zmiennej sumprogram pyta nas, czy chcemy podać następną wartość , oraz informuje, że wpisanie litery n zakończy podawanie nowych wartości. Wpisana litera przechowywana jest w zmiennej i ndicat or użytej w instruk cji warunkowej i f do sprawdzenia, czy podana litera to "n", czy "N" . Jeżeli żadna z nich nie zostanie znaleziona, to pętla jest kontynuowana, w przeciwnym przypadku wykonywana jest instrukcja break. Efekt działania tej instrukcji w pętli jest taki sam jak w przypadku instrukcji switch . W tym przykładzie powoduje ona wyjście z pętli i kontynuowanie programu od instruk cji występującej bezpośrednio po niej. Na zakończenie wyświetlana jest liczba podanych wartości oraz ich średnia , która została obliczona poprzez podzielenie wartości zmiennej sumprzez wartość zmiennej i . Oczywiście zmienna i zostaje przekonwertowana na typ doubl e przed dokonaniem obliczeń, jak pamię tamy z rozdziału 2. poświęconego rzutowaniu.
Inslrukcja conlinue Poza break istnieje jeszcze jedna instrukcja, która służy do zmieniania cja conti nue. Zapisuje się ją w następujący sposób:
działania pętli
-
instruk
170
Visual C++ 2005. Od podstaw continue: Wykonanie instrukcji cantin ue wewnątrz pętli powoduje natychmiastowe wykonanie następ nej iteracji z pominięciem wszystkich instrukcji pozostałych w bieżącej iteracji. Sposób dzia łania tej instrukcji przedstawię na poniższym przykładowym kodzie:
#include using st d: .cm:
using st d: :cout :
using st d. rendl :
i nt main() {
int i = O. val ue = O. product = 1:
tort t = 1: i <= 10: i ++ )
{
cout « "Podaj ci n » va lue:
lic zbę ca łkowitą:
if( va l ue == O) continue:
II Jeś li zmienna value ma wartoś ć zero, II przeskocz do następn ej iteracji.
product *= va lue:
}
cout
« «
"W ynik (zera end l :
return O:
są
ignorowane ): .. II
Wyjś cie
«
product
z pętli.
} Pętla ta wczytuje dziesięć wartości , z których ma stworzyć liczbę . Instrukcja warunkowa i f sprawdza każdą wprowadzoną wartość i jeżeli jest to zero, to instrukcja cent i nue powoduje rozpoczęcie nowej iteracji. Dzięki temu mamy pewność , że na koniec nie otrzymamy zero wego wyniku. Oczywiście, gdyby wartość zero pojawiła się w ostatniej iteracji, to pętla zosta łaby zakończona. Istnieją także inne sposoby wykonania tego zadania , ale instrukcja canti nue jest w takich przypadkach bardzo użyteczna, w szczególności gdy potrzebujemy przeskoczyć do końca bieżącej iteracji z różnych miejsc w pętli. Wpływ
instrukcji break i cant inue na
działanie pętli
far przedstawia rysunek 3.4.
W prawdziwym programie użylibyśmy instrukcji break i cent i nue z jakim ś wyrażeniem sprawdzającym warunek w celu określenia, kiedy należy wyj ść z pętli lub kiedy powinna zostać pominięta iteracja. Instrukcji break i cant i nue można używać także z innymi pętlami , o których będziemy mówić w dalszej części rozdziału . Ich działanie jest zawsze takie samo.
~ Używanie danych innegotypU wpętlach Do tej pory do liczenia powtórzeń w pętlach używali śmy liczb całkowitych. Jednak w przypad ku typu używanych do tego celu zmiennych nie ma żadnych ograniczeń . Spójrzmy na poniż szy przykład:
Rozdział 3.
• Decyzie i pętle
Rysunek 3.4 Wykonaj wyraże nie_ i n icjal izujące
~ sp rawdzające ma war tość
Nie
true
Tak
~
conti nue;
, break; pętl i zostają po mini ęte
Dalsze instrukcje w
, Wykonaj wy rażen ie _modyfi kuj ące
Następuje natych miast owe
opuszczenie pętli
I
P rz ejd ź
do na stę pnej inst rukcji
II Cw3_11.cpp
II Wyświetlanie kodów ASCII liter alfabetu.
#incl ude #i nclude usi ng usi ng using usi ng usi ng
st d: :cout: st d: :endl : st d: :hex: st d: :dec : st d: :setw:
i nt mai n() {
tor tcher capit al = ' A' . smal l 'a' : capita l <= 'Z' : capital ++. smal l++) cout « endl « "\t " « capita l II Wyświetl wielką literę j ako znak, « hex « set w(l O) « st at ic_cast : capital ) IIj ako liczbę szesnas tkową, « dec « setw( l O) « sta t i C cast (capita1) Ilj ako liczbę dziesiętną .
171
172
Visual C++ 2005. Od podstaw « " "« sma11 II Wyświetl malą literę jako znak, « hex « setw(lO ) «static_cast(sma111 /ć jako liczbę szes nastkowq, « dec« setw(l O) «static_cast(smalll ; Iljako liczbę dz ies ię tną. cout « end1 ; return O;
Jak lo działa W programie pojawiły s i ę deklaracje using dla kilku nowych manipulatorów, mających na celu odpowiednie sformatowanie wyświetlanych wyników. Pętla jest tym razem kontrolowana przez zmienną typu cha r o nazwie capita l , która została zadeklarowana razem ze zmienną sma11 w wyrażeniu inicjali zującym . Obie zmienne zw i ęk szamy w trzecim wyrażen iu kontrolnym pętli tak, że warto ść zmiennej capi t al waha się od A do Z, a zmiennej sma11 od a do z. Pętla zawiera tylko z nich to:
jedną instrukcję wyjściową rozciągniętą
na siedem wierszy. Pierw sza
cout « endl
Oznacza ona Następne
rozpoczęcie
nowego wiersz a na ekranie .
trzy wiersze to:
« "\ t " « capi t al II Wyświetl wielką literę j ako znak, « hex « set w(l O) « sta t i c_cast (capi ta l ) Iljako liczbę szesnas tkową, « dec « setwrl u) «static_cast(capitall Iljako liczbę dz ies ię tną.
Przy każdym powtórzeniu, po wysłaniu znaku tabulacji , wartość zmiennej capi t al prezen towana jest trzy razy : w postaci litery alfabetu , liczby szesnastkowej oraz liczby dziesiętnej . Umieszczając
manipulator hex w strumieniu wyj ściowym, powodujemy, że znajdujące się przedstawione w postaci liczb szesnastkowych, a nie dziesiętnych , które są domyślne dla liczb całkowitych . Dzięki temu druga instrukcja wyj ściowa zmiennej capit al stanow i szesnastkową reprezentację kodu znaku .
po nim dane
zostaną
Następnie
do strumienia wstawiamy manipulator dec w celu zmiany formatu wysyłanych danych z powrotem na dziesi ętny . Domyślnie zmienna typu cha r interpretowana jest przez strum i eń jako znak, a nie wartość liczbowa. Aby wysłać zawartość zmiennej znakowej capi t al w postaci liczbowej , należy zastosować jej rzutowanie na typ i nt za pomocą operatora st at i c_cast() , o którym już mówiliśmy w poprzednim rozdziale. W następnych trzech wierszach instrukcji wyjściowej wartość zmiennej sma11 jest na wyjście , podobnie jak to było w przypadku zmiennej capi t al : « " " « sma11 hex « setw(lO) « static_cast(s ma11) dec « set w(lO) « sta t i c_cast (sma11 ) ;
« «
II Wyświetl malą literę j ako znak, IIj ako liczbę szesnas tkową, IIj ako liczbę dziesiętną.
ZmiennopoZYCyjne liczniki pętli Jako licznika pętli można również użyć liczby zmiennopozycyjnej. Poniżej znajduje się przy kład pętli for z liczn ikiem tego typu:
double a = 0.3. b = 2.5:
for( doub le x = 0.0: x <= 2. 0: x += 0.25)
cout « "\ n\ tx ~ " « X
« "\ t a*x + b ~ " « a*x + b:
Powyższy fragment kodu oblicza wartość wyrażen ia a*x+b dla wartości x zawierających s i ę w zbiorze 0,0 - 2,0, przeskakując o 0,5. Używając liczb zmiennopozycyjnych w licznikach, trzeba bardzo uważać . Wiele liczb dziesiętnych nie może być dokładnie reprezentowanych w binarnej formie zmiennopozycyjnej, przez co mogą pojawić się pewne odchylenia przy dużej ilości danych. Oznacza to, że nie pow inno się kodować pętli f or w taki sposób, że jej zakoń czenie uzależnione jest od zmiennopozycyjnego licznika dochodzącego do ściśle okre ślonej wartości . Na przykład poniższa źle zaprojektowana pętla nigdy się nie kończy .
for(double x = 0.0 ; x != 1.0 : x += 0.1)
cout « x:
Przeznaczen iem tej funkcji jest sprawdzenie zmieniających się wartości x od 0,0 do 1,0. Niestety liczba 0,1 nie ma dokładnej reprezentacji w postaci binarnej wartości zmiennoprze cinkowej, a więc x nigdy nie będzie mieć wartości 1. W ten sposób wyrażenie sprawdzaj ące pętli ma zawsze wartość true i pętla będzie powtarzać się w nieskończoność .
174
Visual C++ 2005. Od podstaw
Pętla
while Drugim rodzajem pęt li w C++ jest pętla whi l e. W przeciwieństwie do pętl i f or, która została zaprojektowana z myś lą o powtarzaniu instrukcji lub bloku instrukcji określoną liczbę razy, pętla whil e używana jest do wykonywania instrukcji lub bloków instrukcji tak długo , jak długo określony warunek ma wartość t rue. Ogó lna forma pętli whi l e wyg ląda n a s tęp uj ąco : whi l e (warunek ) instrukcj a -P ę t 7 i;
Instrukcja pętli wykonywana jest, dopóki warunek ma wartość t rue. W momencie zmiany wartości warunk u na f al se program kontynuuje działanie, przechodząc do instrukcji znaj duj ąc yc h się za pętlą. Jak zwyk le w miejsce instrukcji pętli można wstawić blok instrukcji . Zas adę działania pęt li
whi l e przedstawiono na rysunku 3.5.
Rysunek 3.5 schemat blokowy
pęt l i
wh ile
warunek
ma war tość
Nie
true
P r zejd ź
do n a stęp nej instrukcji
~ Używanie pętli while Wcześniejszy przykład ob liczający średnią pętli
whi l e.
II Cw3_ 12.cpp
II Używan ie pętli while do obliczania ś redn iej.
#include using st d: .c m :
using st d; ;cout:
using st d: .endl :
int mai nO {
podanych liczb
możemy zmodyfikować
z użyciem
Rozdzial3. • Decrzie i pętle dou ble value = 0.0: double sum = 0.0: i nt i = O: char i ndicator = ' t ' :
II Zmienna przechowująca wprowadz one dane. II Zmienna przechowująca sumę podanych wartoś c i. II Liczba podan ych war tości.
while(i nd icat or
II
cout ci n
« « »
endl "Podaj val ue:
II Kontynuować czy nie?
' t' )
==
ci n »
true tak dlugo j ak y.
II Wczytaj wartoś ć. II Zwiększ licznik. II Dodaj bieżące dane
sum += value: « «
Wartoś ć
j a k ą ś liczbę :
Hi :
cout
175
endl "Czy chcesz i ndi cator:
po dać
jeszcze
jedną liczbę
II Wczytaj
wejściowe
do sumy ogólnej.
(wpi sz n. aby
zako ńczyć)?
wskaźnik.
cout « endl
« « «
"Śre d ni a " « i
" wprowadzonych
w artości
wynosi "
«
sum/i
«
endl :
return O:
Jak to działa Jeżeli
wprowadzimy takie same dane jak poprzednio, to w wyniku działania tego programu taki sam wynik. Zmodyfikowana została jedna instrukcja i jest jedna całkiem nowa (w kodzie znajdują się one na szarym tle). Instrukcja for została zastąpiona instrukcją whi l e, a sprawdzanie stanu zmiennej i ndi cator w pętli i f zostało całkowicie usunięte , ponieważ czynność ta jest teraz wykonywana przez warunek pętli whi l e. Zmienną i ndicator musimy zainicjalizować wartością t, a nie n, tak jak było w poprzednim przypadku - jeśli tego nie zrobimy, to działanie pętli whi l e natychmiast się zakończy. Dopóki warunek w pętli whi l e ma wartość tr ue, pętla kontynuuje działanie . otrąmamy
Jako warunek pętli whi le można zastosować dowolne wyrażenie, którego wynikiem jest war tość t rue lub fa l se. Nasz przykład byłby lepszy, gdyby pozwalał na wpisanie zarówno litery T, jak i t w celu poinformowania , że chcemy kontynuować . Aby to zrobić , możemy zmodyfi kować warunek pętli w następujący sposób : while « i ndicator == ' t ' ) l I (indicato r == 'T' )) Można także stworzyć pętlę mocą
warunku
while(true)
{
mającego
whil e, która potencjalnie powtarza się w nieskończoność, za po wartość true. Warunek taki możemy napisać następująco :
zawsze
176
Visual C++ 2005. Od podstaw Wyraże nie
kontrolne p ętl i moglibyśmy także zap isać w postaci liczby całkowitej I, która zo przekonwertowana na wartość lo g ic zną t rue. Oczywiście w tym przypadku, podobnie jak w instrukcji for, w bloku pętl i musimy dostarczyć jakiegoś sposobu wyjśc ia z niej . Więcej sposo bów zastosowa nia pęt li whi l e zobaczymy w rozdzia le 4. s tałaby
Pętla
do-while Pę tla
do-whi l e jest podobna do pętl i whi l e w tym, że powtarza się, dopóki podany warunek ma t rue. Główna różnica polega na tym, że warunek jest sprawdzany na końcu pętli (w pętli whil e odbywa się to na samym początku) . W konse kwencji pętla do-whi l e zawsze wykonywana jest co naj mniej jeden raz. Ogólna postać pęt li do-whil e przedstawia s ię nastę wartość
pująco :
do ( inst ruk cje-Pę t 7 i ;
}whi le(warunek ); Zasada dz i ałania tej
pę t li została
przedstawiona na rysunku 3.6.
Rysunek 3.6
warunek ma wa rto ść true
Tak
Nie P rzejd ź
do n astę p n ej instr ukcji
Pętlę
whi le w ostatniej wersji naszego programu
obliczającego średn ią podanych
liczb mogli
byśmy zastąpić pęt lą do-whi l e:
do cout ci n »
« «
endl "Podaj va l ue;
j a k ąś lic z b ę ;
II Wczytaj wartość. II Zwiększ licznik . II Dodaj b ieżące dane wejściowe do sumy ogó lnej.
H i ;
sum += value; cout « endl « "Czy chcesz
pod a ć
jeszcze
jedn ą l i cz bę
(wpisz n. aby
zako ń czyć )?
Rozdział 3.
• Decyzje i pętle
177
ci n » i ndicator: II Wcz tai wskaźnik.
} whil e« indicator == 't' ) II (indicat or == 'T' )):
Pom iędzy
tymi dwiema wersjami pętli nie ma zbyt wielkiej różnicy, z wyjątkiem tego, że do-whi l e jest niezależna od warto ści początkowej , na którą została ustawiona zmienna i ndi cat or. Jeżeli chcemy, aby wprowadzona zos tała co najmniej jedna wartość (co jest nawet zro zumiałe w tego typu oblic zeniach), to lepiej jest wybrać pętlę do-whi l e. pętl a
Zagnieżdżanie pętli W pętli można zagnieździć inną pętlę. Typowe zastosowanie takich struktur stanie się bardziej oczywiste w rozdziale 4. - zazwyczaj potrzebne są one do wykonywania powtarzających się czynności na różnych poziomach klasyfikacji . Przykładem tego może być oblic zanie wszystkich ocen ucznia w danej klasie, a następnie powtórzenie tych samych obliczeń dla następnych klas.
~ Zagnieżdżone pętle Efekty zagn ieżdżania pętli możemy zaobserwować poprzez obliczenie warto ś ci prostej for muły . Silnia liczby całkowitej jest iloczynem wszystkich następujących po sobie liczb c ałko witych od l do tej liczby. Zatem silnia liczby 3 wynosi: l razy 2 razy 3, czyli 6. Poniższy pro gram oblicza s ilnię podanych liczb całkowitych (ile tylko ich mamy) : II Cw3_13.cpp
II Stosowanie zagn ieżdżonych pętli w celu obliczenia silni.
#include usi ng std: .ci n:
using st d: :cout :
using st d: :endl :
i nt ma i n() {
char i ndicator = 'n ' :
long value = O,
factoria l = O: do
{
cout cin
« «
»
endl
"Podaj value:
ja k ą ś lic z bę c a ł k ow it ą:
factor ial = 1:
for (i nt i = 2: i <= value: i++)
factorial *= i :
cout cout
"Si lnia" « value « " wynos i " « fact oria l :
endl
"Czy chcesz p od a ć nastę pną l i c z bę (t lub n)? ".
cin » i ndi cat or :
} whi le« indicator == ' t ' ) I I (i ndicato r == 'T' )) :
« « «
178
Visual C++ 2005. Od podstaw
ret urn o; Po skompi lowaniu i wykonaniu pod spodem:
powyższego
programu otrzymamy wynik podobny do tego
Podaj j a ką ś l i c zbę ca ł k ow i t ą ; 5
Silni a 5 wynosi 120
Czy chcesz podać n a s t ę p n ą liczbę (t l ub n)? t
Podaj j a kąś l i c z b ę c a ł kowitą : 10
Silnia 10 wynosi 3628800
Czy chcesz podać n a s t ęp ną li c z bę (t lub n)7 t
Podaj j akąś li c z b ę c a łkowitą: 13
Si lnia 13 wynosi 1932053504
Czy chcesz poda ć n ast ępną l i czbę (t l ub n)7 t
Podaj j a k ąś liczbę c ałkowitą: 22
Silnia 22 wynosi -522715136
Czy chcesz pod a ć n a s t ę pn ą l i c z bę (t l ub n)? n
Jak to działa Wartości liczb w wyniku działania silni rosną bardzo szybko . W rzeczywistości nasz przykła dowy program potrafi ob liczyć prawidłowe wartości maksymalnie dla liczby 12. Silnia 13 wynosi 6 227 020 800, a nie l 932 053 504, jak ob liczył program. Jeże li podamy jeszcze większe liczby, to w zmiennej factor i a l zostaną zapisane wartości z obciętymi początkowymi cyframi , a mogą nawet pojawić s i ę wartości ujemne, tak jak w przypadku silni liczby 22.
Tego typu sytuacje nie powodują b łędów, a więc jest bardzo ważne, aby zawsze się upew czy typ naszy ch zmiennych zdoła pomieścić wartości, którymi będziemy operować. Należy także pamiętać o możliwoś ci pojawienia się nieprawidłowych danych wejścio wych. Błędy tego typu pojawiają się dyskretnie i bardzo trudno je wychwycić. nić,
Zewnętrzna pętla to do-wh i l e, która decyduje, kiedy n a s tąp i koniec programu. Dopóki bę dziemy wpi sywać t lub T w odpowiedzi na pytanie o kontynuację, program będzie ob liczał silnie podanych liczb. Są one obliczane przez wewnętrzną pę tlę for. Wyko nywana jest ona val ue razy w celu po mnożenia zmiennej fact ori al (o wartości po cz ątkowej l ) przez następu jące po sobie liczby całkowite od 2 do va l ue.
~ Jeszcze iedna zagnieżdżona pętla Zrozumienie sposobu działania pęt li spójrzmy na jeszcze jeden przykład. nym rozmiarze. II Cw3_ 14.cpp
II Tworzenie tabliczki
mn ożenia
#include
#incl ude
zagn ieżdżonych może być trochę Poniższy
program tworzy
za pomo cą zagn ieżdżonych pętli.
trudne . Z tego względu o okreś lo
tab liczkę mnożenia
Rozdział 3.
• OecJzie i pętle
using st d: :cout :
using st d: :endl :
usi ng st d: :set w:
i nt mai nO {
const int size = 12: i nt i = O. j = O: cout
II Rozmiar tabliczki.
II Liczn iki pętli.
endl II Wysy łan ie tytułu tabliczki . " Tabl i czka m nożeni a « si ze « " na " « si ze « endl « end l :
« «
cout « endl « " I": for(i = 1: i <= size: i++ ) cout « setw(3) « i «
II Pętla
cout « endl : for (i = O: i <= si ze: i++) cout « for(i
=
1: i
<=
wysyłająca nagłówki
kolumn.
II Znak nowego wiersza dla podkreś łeń . II Podkreślenie każdego
si ze : i++)
II Zewnę trzna pę tla
nagłówka .
tworząca
wiersze.
{
cout « endl « setw(3)
«
i
«
"
I":
II
for( j ~ l : j <= size : j ++ ) cout « set w(3) « i*j « "
Wyś lij etykietę
wiersza.
II Pętla wewnętrzna tworząca pozostałe wiersze. II Koniec pętli wewnętrznej.
}
II Koniec pętli zewnętrznej.
cout « endl :
retur n O:
Wynik dz i ałan ia tego programujest n astępuj ący : Tabli czka
l 2 3 4 5 6 7 8 9 10 11 12
l 2 3 4 5 6 7 8 9 10 11 12
m noże nia
12 na 12
2
3
4
5
6
7
8
9
10
11
12
2 4 6 8 10 12 14 16 18 20 22 24
3 6 9 12 15 18 21 24 27 30 33 36
4 8 12 16 20 24 28 32 36 40 44 48
5 10 15 20 25 30 35 40 45 50 55 60
6 12 18 24 30 36 42 48 54 60 66
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64
9 18 27 36 45 54 63
11 22 33 44 55 66
12
24
36
48
60
77
84
72
72
84
81 90 99 108
10 20 30 40 50 60 70 80 90 100
88 99 110 121 132
108
120
132
144
77
80 88 96
72
110
120
72
96
179
180
Visual C++ 2005. Od podstaw
Jak to działa Tytuł
tabliczki jest tworzony w pierwszej instrukcji wyj ściowej. Następna w połączeniu ze po niej pętlą tworzą nagłówki kolumn . Każda kolumna ma szerokość pię ciu znaków. Nagłówek zajmuje trzy znaki (wartość zdefiniowana za pomocą manipulatora set w( 3) , a pozostałe dwa pozostają puste. Instrukcja wyj ściowa przed pętlą wysyła cztery spacje i pionow ą kreskę nad pierwszą ko lum ną, która zawiera nagłówki wierszy. Następnie pod nagłó wkam i kolumn wyświetla się szereg znaków podkreślenia . znajduj ącą się
Zagnieżdżona pętl a
wiersza, a więc i
tworzy główną tre ść tabeli. Zewnętrzna powtarza się jeden raz dla każdego wierszy. Instrukcja wyj ściowa:
określa l i c z b ę
cout « endl « set w(3) « i « " I"; II Wyślij etykietę wiersza.
przechodzi do nowego wiersza w celu rozpoczęcia nowego wiersza, a następnie wysyła na wiersza stanowiący wartość zmiennej i w polu o szerokości trzech znaków, po którym następuje spacja i pionowa kreska .
wyjście nagłówek
Wartości
w wiersz u generowane
są
przez
wewnętrzną pęt lę:
for (j = l ; j <= S i ze: j++) II Pętla wewnętrzna tworząca pozostałe wiersze. cout « setw(3) « i * j « " " ; II Koniec pętli wewnętrznej. Pętla ta wysyła warto ści i * j, gdzie i to bieżąca warto ść wiersza, a j to warto ść kolumny, której zakres wynosi od l do size. A zatem przy każdej iteracji pę tli zewnętrznej pętla wewnętrzn a jest powtarzana s i ze razy . Wartośc i pozycjonowane są w taki sam sposób jak nagłówki kolumn.
Po zakończeni u działania czenia programu .
pętli zewnętrznej
wykonywa na jest instrukcja ret urn w celu zakoń
Programowanie wC++/CLI Wszystko, co napi sałem w tym rozdziale do tej pory, odnosi się także do języka C++/CLI. Aby to wykazać , utworzymy teraz kilka przykładowych programów konsolowych CLR, w których zastosujemy niektóre z omawianych właśc iwości języka . Poniżej znajduje się program stanowiący nieco zmodyfikowaną wersję programu Cw3_01.cpp.
~ PrOgram CLR z użyciem zagnieżdżonych instrukcii warunkowych ił Utwórzmy program konso lowy CLR z modyfikacji :
nastę p ujących
II Cw3_ / 5.cpp : main pr oj ect fi le.
#incl ude "stdafx.h "
domyś lnym
kodem i w funkcji ma in () dokonajmy
Rozdział 3.
• Oecvzie i pętle
181
using namespace System; int mai n(array<Syst em : :Stri ng A> Aargs) (
wcha r_t let ter : Console : :Write(L"Podaj j a k ą ś letter = Console: :Read() ; if(l etter >= ' A' )
II Odpowiednik typu Char w C++ICLf. lite r ę:
if(l etter <= ' Z')
") ; II Spra wdź, czy wprowadzona litera j est większa
II lub równa A. II Sprawdź, czy wprowadzo na litera j est mniej sza II lub równa Z.
Console; :WriteLi net l. "Podano wi el k ą retur n O; i f (l ett er >= 'a ')
l i te r ę ." ) ;
II Sprawdź, czy wprowadzona litera j est większa II lub równa a. II Sprawdź, czy wprowadzona litera j est mniej sza II lub równa z.
if( letter <= 'z' ) Console : :Writ eLi ne(L"Podano ret urn O;
ma ł ą li t er ę . " ) ;
Console: :WriteLi ne( L"Nie podano lite ry. ") : return O; Nowy kod jak zwykle
został
umieszczony na szarym tle.
Jak to działa Zasada działania jest dokładnie taka sama jak w przykładzie Cw3_Ol (w rzeczywistości wszystkie instrukcje są takie same, z wyjątkiem wysyłających dane na wyjście i deklaracji zmiennej l ett er). Zmien iłem typ na wcha r_t , ponieważ typ Char ma pewne dodatkowe wła ściwo ści , o których powiem . Funkcja Consol e; ;Read () przyjmuje pojedynczy znak z klawiatury . Ze względu na fakt, że do wy świetlenia pierwszego komunikatu proszącego o podanie litery użyli śmy funkcji Consol e: ;W r i t eO, znak nowego wiersza nie został automatycznie wstawiony, a więc możemy wprowad z ić literę w tym samym wierszu co komunikat. Platforma .NET posiada własne funkcje służące do konwersji kodów znaków na wielkie lub małe w obrębie klasy Char . Funkcje te to: Char : :ToUpper ( ) oraz Char : :ToLower ( ). Znak, który ma być poddany konwersji, umieszczam y w nawiasie jako argument funkcji . Na przykład :
wchar t uppcaseLet t er = Char: :ToUpper(let t er ); Oczywi ście
wynik konwersji
moglibyśmy przechować
w oryginalnej zmiennej ,jak poniżej :
let t er = Cha r : :ToUpper (let t er ); W klasie Char dostępne są także funkcje I sUpper ( ) oraz I sLower , które sprawdzaj ą, czy litera jest wielka , czy mała . Literę , którą chcemy sprawdzić , podajemy w nawiasie jako argument funkcji zwracającej wartoś ć logiczną. W funkcji mai nO moglibyśmy również użyć następują cego kodu:
182
Visual C++ 2005. Od podstaw wchar_t letter : II Odp owiada typowi Char w C++ICL/. Conso le: :Write CL"Podaj jaką ś l it e r ę : "): let t er = Conso le : :ReadC): wchar_t upper = Cha r: :ToUpperClet t er ): i f Cupper >= ' A' && upper <= ' Z' ) II Sprawdź, czy zawiera s ię w zbiorze A - Z. Consol e: :Writ eL ineCL "Podano l i t e rę (O}. ". Char : :IsUpper Cletter) ? " w i el k ą" " ma ł ą " ) :
else Console: :WriteLi neCL"Nie podano litery." ): ret urn O: Ten kod jest znacznie prostszy. Po przekonwertowaniu litery na wielką wykonujemy sprawdzanie , czy należy ona do zbioru A -Z. Je żeli tak, wy świetlamy komunikat, którego tre ść uzależn iona jest od wyrażenia operatora warunkowego, tworzącego drugi argument funkcji Writ eli net ). Wartością wyrażenia operatora warunkowego jest capi ta l, je śli zmienna l etter jest wielką literą, lub sma11 w przeciwnym przypadku. Następn i e wynik ten umieszczany jest w strumieniu wyjściowym , co okre śla pozycja łańcucha formatującego {O} . Poniżej
w
pętli
znajduje się jeszcze jeden przykład, który wykorzystuje i zagłęb ia s i ę nieco bardziej w kl asę Conso1eKeyl nf o.
funkcję
Consol e: :ReadKey()
~ Sprawdzanie naciśnięć klawiszy Utwórzmy program konsolowy i dodajmy
na stępujący
kod do funkcji mai n( ) :
#i ncl ude "stdafx.h" using namespace Syst em: i nt ma inCarray<Syst em: :String A> Aargs) (
Console: :Writ e L i n e C L "Wc i śn i j Escape.") :
j aką ś k omb i n a c j ę
klawiszy - aby
z a kończyć . wc i ś n i j
Conso leKeylnfo keyPress : do (
keyPress = Conso le: :ReadKey(t rue): Console : :Wr i t e C L " W c i ś ni ę t e klawisze to") : if Csa fe_cast CkeyPress .Modi fie rs»O) Conso le : :Write CL" (O }.". keyPress .Modifi ers ): Console : :WriteLineCL" {O } czyli zna k (l )". keyPress .Key. keyPress. KeyChar): }whi leCkeyPress .Key != ConsoleKey : :Escape) : return O: Pon i żej
znajduje
s i ę przykładowy
wynik
działania
tego programu.
Rozdział3.
• Decyzie i pętle
183
klawiszy - aby za k o ńczyć . wciśnij Escape. Shift. B. czyli znak B Shift. Control. N. czyli znak _ Shift. Contro l. Deml. czyl i znak Deml. czyli znak; Dem3. czyli znak ' Shift . Dem3 . czyl i znak @ Shift . Dem?, czyli znak Shift. Dem6. czyli znak } 03. czyli zna k 3 Shift . 03 . czyli znak? Shift 05, czyli znak % Dem8. czyli znak' Escape . czyl i znak
Wc iśn ij jakąś kombinację Wciśnięte
Wciśnięte Wc iśnięte Wc i ś n i ęt e W ciśnięte Wc i śn i ę t e Wciśnięte Wci ś ni ęt e Wciśnięte Wciśnięte Wc i śn i ęt e
Wciśnięte Wc i ś n i ęt e
klawisze to klawisze to klawisze to klawisze to klawisze to klawisze to klawisze to klawisze to klawisze to klawisze t o klawisze to klawisze to klawisze to
O c zywiśc i e istni eją
komb inacje klawiszy reprezentuj ące znaki, których nie można wyśw ie na ekranie. W takich przypadkach nie pojawia się żaden znak. Program zakończy działanie także w przypadku naciśnięcia kombinacji klawiszy Ctrl+C, ponieważ system operacyjny rozpozna je jako polecenie zakończenia programu. tlić
Jak to działa Wciśnięcia klawiszy sprawdzane są w pętli do-wh i l e i jej wykonywanie jest kontynuowane do momentu wciśnięcia klawisza Esc. Wewnątrz pętli wywoływana jest funkcja Consol e ; ;ReadKey( l, a wynik jej działania przechowywany jest w zmiennej keyPress, która należy do typu Consol eKeyl nfo. Klasa Consol eKey lnfo ma trzy właściwości , za pomocą których można zidentyfikować wciśnięty klawis z lub klawisze właściwość Key identyfikuje wciśnięty klawisz , właściwo ść KeyCh ar reprezentuje kod Unieode znaku klawisza, a wła ściwość M odi fier s jest bitową kombinacjąstałych Consol eM odi fi ers , które reprezentują klawisze Shift, Alt oraz Ctrl. Consol eModi fi ers jest wyliczeniem zdefiniowanym' w bibliotece System, a stałe zdefiniowane w tym wyliczeniu maj ą nazwy Alt, Shift oraz Cont rol .
Jak widać po argumentach do funkcji Wri t el i ne t l w ostatniej instrukcji wyjściowej, aby uzyskać dostęp do właś ciwo ści obiektu , należy umieścić nazwę właś c iwości przed nazwą tego obiektu i oddzielić je kropką. Kropka ta zwana jest operatorem wyboru składowej. Aby uzyskać dostęp do właściwości KeyCha r obiektu keyPress , należy napisać keyPress . KeyChar. Zasada działania programu jest bardzo prosta. Wewnątrz pętli wywoływana jest funkcja ReadKey ( l, sprawdzająca wci śnięte klawisze i zap i s uj ąc a wynik w zmiennej keyPres s. Następnie początkowa czę ść danych wyjściowych wysyłana jest do wiersza poleceń za pomocą funkcji Write( l . Jako że funkcja ta nie dodaje znaku nowego wiersza, następna instrukcja wyj ściowa zapisuje do tego samego wiersza. Następnie sprawdzamy, czy właśc iwo ść Modi fi ers jest więk sza od zera. Jeżeli tak, to oznac za to, ż e zo s tały wciśnięte klawi sze funkcyjne i należy je wyświetlić . W przeciwnym przypadku instrukcję wyświetlania klawiszy funkcyjnych pomijamy. Pewnie pamiętamy , że stałe wyliczeniowe C++/CLI są obiektami, które musimy jawnie przekonwertować na typ całkow ity przed użyciem ich jako wartości numerycznych stąd też rzutowanie na typ i nt w wyrażeniu warunkowym i f . Intere sująca jest instrukcja wysyłając a na wyj ście warto ść zmiennej M odi f i ers . Jak widać , w przypadku wciśnięcia więcej ni ż jednego klawisza funkc yjnego wszystkie one zo staną wysłane na wyjście za pomocą jednej instrukcji. Dzieje się tak, gdyż wyliczenie M odi fi ers
184
Visual C++ 2005. Od podstaw jest zdefiniowane z atrybutem Fl agsAttri but e, który oznacza , że to wyliczenie składa się ze zbioru pojedynczych znaczników bitowych . Dzięki temu zmienna wyliczeniowa może zawierać kilka znaczników połączonych operatorem AND, a poszczególne znaczniki rozpoznają i wysyłają na wyjście funkcje za pomocą funkcji Wri te ( ) lub WriteLi net ). Pętla będzie
wykonywana, dopóki warunek keyPress .Key ! = Conso1eKey: :Escape ma wart rue. Jego wartość zmieni się na fa1se, gdy właściwość keyPress. Key będzie równa Con sa1ekey: : Escape, czyli gdy zostanie wciśnięty klawisz Escape .
tość
Pętla
lor each Wszystkie opisywane pętle mogą być stosowane w języku C++/CLI. Ale język ten pozwala na jeszcze jednego rodzaju pętli , a mianowicie for each. Służy ona do iteracji wszystkich obiektów w określonego rodzaju zbiorze obiektów , ale jako że jeszcze nie omawialiśmy obiektów, przedstawiam tutaj tylko krótkie wprowadzenie do tej pętli . Bardziej szczegółowo omówię ją w odpowiednim czasie . używanie
o której już trochę wiemy, jest obiekt Stri ng, reprezentujący zbiór znaków, a więc za pomocą pętli for each przejść przez wszystkie znaki w łańcuchu. Spójrzmy na przytakiej operacji.
Jedną rzeczą, można kład
~ Uzyskiwanie dostępu do wszystkich znaków
włańcuchu za pomocą pętli lor each Utwórzmy nowy program konsolowy CLR o nazwie Cw3_17 i zmodyfikujmy w nim kod do następującej postaci :
#inc l ude "st dafx.h" using namespace System: int mai n(arr ay<Syst em: :Stri ng
A>
A
args )
(
int vowels = O: int consonants = O; St ring proverb = L" Ś l e pemu nic po okularach .": for each( wchar t ch in proverb) A
-
(
if (Char ; :IsLetter (ch) (
ch
~
Char: :ToLower (ch) :
II Przekonwertuj na małe litery.
swttcht cn)
{
case 'a ': case 'e' : case l : case 'o '; case ' u' : case 'y' : ++vowels;
znajdujący się
Rozdział3.
• Decyzie i pętle
185
break; default ; ++consona nts ; break; }
Console; ;Writ eL ine(proverb); Console ; ;WriteLine(L "Powiedzenie zawie ra {O}
sa mo g ło s ek
i {l } s pó ł g ł o s e k. ", vowel s . consonants);
ret urn O: W wyniku
działania tego
kodu otrzymamy następujący rezultat:
Ś lepemu
nic po okula rach. Powi edzeni e zawiera 9 s a mogł ose k i 12
s pó ł gło se k.
Jak to działa Program zlicza liczbę samogłosek i spółgłosek w łańcuchu wskazywanym przez zmienną proverb. Dokonuje tego, sprawdzając każdy znak w łańcuchu za pomocą pętli for each. Najpierw zdefiniowane zostały dwie zmienne do przechowywania liczby samogłosek i spółgłosek.
i nt vowel s = O; int consonants = O: Obie te zmienne w C++/CLI N astęp n i e
są typu
I nt 32, który przechowuje 32-bitowe liczby całkowite.
definiujemy tekst do anali zy.
Stri ng proverb = A
L" Ś l epemu
nic po okularach
Zmienna proverb j est typu Stri ng który jest opisywany jako "uchwyt do łańcucha" . Uchwyt używany jest do przechowywania lokalizacji obiektu na stercie, którą zarządza CLR. Na temat uchwytów i typu St ri ng dowiemy się więcej , gdy dojdziemy do typów klasowych C++/CLI. Na razie wystarc zy nam wiedza, że typ ten w C++/CLI używany jest dla zmiennych przechowujących łańcuchy znaków. A
,
A
Pętla
ma
for each sprawdzająca wszystkie znaki w łańcuchu wskazywanym przez zm i enną proverb
następującą formę :
for each( wch ar_t ch in proverb) { II Przetwarzaj
b ieżący
znak w zmiennej ch.
}
Znaki w zmiennej proverb są znakami Unicode, a więc używamy zmiennej typu wcha r_t (równoznaczny z typem Cha r) do ich przechowywania. Pętla zapisuje jedenpo drugim znaki ze zmiennej proverb do zmiennej pętlowej ch, która jest typu C++/CLI Char. Zmienna ta ma w pętli zasięg lokalny (inaczej mówiąc, istnieje tylko wewnątrz bloku pętli). Po pierwszej iteracji zmienna ta zawiera tylko pierwszy znak z łańcucha, po drugiej dwa pierwsze znaki, po trzeciej trzy itd., aż skoń czą się znaki i zakończy się działanie pętli .
186
Visual C++ 2005. Od podstaw Wewnątrz pętli
za
pomocą
instrukcji warunkowej i f sprawdzamy, czy dany znak jest
literą:
i f( Ch ar :: IsL etter(c h))
Funkcja Char: : I sLett er ( ) zwraca true , jeśli argument (w tym przypadku ch) jest literą, i f al se w przeciwnym przypadku . Tak więc blok znajdujący się po instrukcji warunkowej i f zostanie wykonany tylko wtedy, gdy zmienna ch zawiera literę. Jest to konieczne, gdyż nie chcemy, aby znaki interpunkcyjne były traktowane jak litery. Po ustaleniu, że zmienna ch jest
literą,
konwertujemy ją na
małą literę
za pomocą instrukcji :
ch = Cha r : :ToLower(ch) ; II Przekonwertuj na male litery.
Do tego celu używamy funkcji Char : :ToLower ( ) z biblioteki platformy .N ET, która zwraca odpowiednik argumentu (w naszym przypadku ch) przekonwertowany do małej litery. Jeżeli argument jest już małą literą, to funkcja zwróci go w niezmienionej postaci . Dzięki konwersji na małe litery unikamy konieczności sprawdzania zarówno wielkich, jak i małych liter. Za
pomocą
instrukcji switch sprawdzamy, czy zmienna ch zawiera
spółgłoskę,
czy samo-
głoskę .
switch( ch)
( case ' a' : case 'e': case 'i ' : case ' o ' : case 'u' : case ' s ' : ++vowels; bre ak : default : ++consonant s ; break :
W każdym z pięciu przypadków, kiedy zmienna ch zawiera samogłoskę, wartość zmiennej vowe l S jest zwiększana o jeden. W przeciwnym przypadku zwiększamy wartość zmiennej consonants . Instrukcja swi t ch wykonywana jest dla każdego znaku w łańcuchu proverb, a więc kiedy pętla kończy działanie , zmienna vowel S zawiera liczbę samogłosek w łańcuchu , a zmienna consonants liczbę spółgłosek . Następnie wynik wysyłamy na wyjście za pomocą następujących instrukcji : Console: :Writ eLi ne( pro ver b) ; Console: :Wr iteLi ne( L" Powi edzeni e zawier a {O} sa mog łos e k i { l } spó ł g łos e k. ", vowe ls . consonants) ;
W ostatniej instrukcji wartość zmiennej vowels zostaje wstawiona w miejsce sekwencji {O} w łańcuchu , a wartość zmiennej consonants w miejsce sekwencji {l }. Jest to możliwe, ponieważ do argumentów występujących po pierwszym argumencie łańcucha formatującego odnosimy się za pomocą wartości indeksowanych, zaczynając od O.
Rozdział 3.
• Decyzje i pętle
187
Podsumowanie W rozdziale tym poznaliśmy wszystkie najważniejsze mechanizmy podejmowania decyzji w programach w C++. Przejrze liśmy także wszystk ie narzędzia służąc e do powtarzania bloków instrukcji. A oto lista najważniejszych poruszonych zagadnień : •
Podstawowa zdo lność podejmowania decyzji oparta jest na zbiorze operatorów relacji, które pozwa lają na sprawdzanie i porównywanie wyrażeń oraz zwracają wartość logiczną jako wynik (t rue lub f al se).
•
Decyzje
można także podejmować na
podstawie warunków, które nie zwrac aj ą logicznych. W takim przypadku podczas sprawdzania wartości warunku wszys tkie warto ści niezerowe s ą konwertowane na t rue, a zerowe na f al se. wartości
•
Najbardziej podstawową strukturą pozwal ając ą na podejmowanie dec yzji w C++ jest instrukcja warunkowa i f . Dodatkowej e lastyczności n a d aj ą instrukcja swi tc h i operator warunkowy.
•
W C++ ISO /ANSI dostępne s ą trzy podstawowe metody powtarzania bloków instrukcji - pęt le : fo r , wh i l e oraz do-whil e. Pętl a for umożliwia wykonanie określonej liczby powtórzeń . Pęt la whi l e powtarza się, dopóki warunek ma wartość true. P ętl a do-whi l e wykonuje się co najmniej jeden raz i kontynuuje powtórzenia tak długo , jak długo warunek ma wartość tr ue.
•
W C++/CLI
•
Każdą pętlę można zagnieździć
•
Słowo kluczowe cont i nue pozwala na przeskoczenie i przejście do następnej iteracji .
•
Słowo kluczowe break powoduje natychmiastowe wyjście z pętli . Służy ono do wyc hodzenia z instrukcji swi t ch umieszczonej po instrukcjach case.
dostępna jest także pętla
for eac h.
w dowo lnej innej
pętli .
pozostałej części
iteracji w pętl i także
Ćwiczenia Kod
źródłowy
pobrać
wszystkic h przykładów w tej książce oraz ze strony: http://helion.pl/ksiazki/vcppo .htm.
rozwiązania
do
ćwiczeń można
1. Napisz program przyjmujący liczby ze strumienia wejściowego ci n, który je następn i e sumuje. Program powinien zatrzymać się w momencie podania zera . Utwórz ten progra m w trzech wersjach: przy u życ iu pętli wh i le, do-whi l e oraz for .
2. Napisz program w C++ ISO/ANSI samogłoski .
Liczenie powinno
p rzyj muj ący
znaki z klawia tury i zliczający w momencie napotk ania litery "q" pobierz znaki, a zliczania dokonaj za pomo cą
się zatrzymać
lub "Q". Za pomocą ni eskończonej instrukcj i switc h.
pętli
3. Napisz program wyświetlający w kolumnach tab liczkę
mnoże nia
od 2 do 12.
188
Visual C++ 2005. Od podstaw 4. Masz program, w którym chcesz ustaw i ć
zmienną określającą tryb otwarcia pliku w oparciu o dwa atrybuty: typ pliku, który może być tekstowy lub binarny, oraz tryb otwarcia pliku - do odczytu , zapisu lub dołączenia danych. Za pomocą operatorów bitowych (& i I) oraz zestawu znaczników opracuj metodę pozwalającą na ustawienie pojedynczej liczby całkowitej na dowolną kombinację tych dwóch atrybutów. Napisz program, który ustawia taką zmienną, a następnie ją dekoduje , wyświetlając jej ustawienia dla wszy stkich możliwyc h kombinacj i atryb utów .
5. Przepisz przykładowy program Cw3j jako program w języku C++/CLI u żywać
(możesz
funkcji Consol e: :ReadKey() do pobierania znaków z klawiatury).
łań cu ch znaków (jako typ St r t nq"), a następn ie analizuje znaki tego łańcucha w poszukiwaniu liczby wielkich liter, małych liter, znaków n i enal eż ącyc h do alfabetu oraz ogó lnej liczby znaków .
&. Napisz program konsolowy CLR, który definiuje
4 Tablice, łańcuchy znaków
i wskaźniki
Poznaliśmy już
wszystkie fundamentalne typy danych oraz posiadamy wiedzę na temat wykonywania obliczeń i podejmowania decyzji w programie . Rozdział ten poświęcony został szerszemu omówieniu zastosowań podstawowych technik, które poznaliśmy do tej pory . Zamiast pracować na pojedynczych danych, będziemy operować całymi ich zbiorami . W roz dziale tym dowiesz się: • Czym są tablice i jak ich używać. • Jak
deklarować
i inicjalizować tablice
• Jak
deklarow ać
tablice wielowymiarowe i jak ich
• Czym • Jak
są w skaźniki
deklarować
i jak ich
różnego
typu. używać.
używać .
i inicjalizować
wskaźniki różnego
typu.
• Co łączy tablice i wskaźniki . • Czym są referencje, jak się je deklaruje, a także poznasz kilka podstawowych
informacji na temat ich użycia.
• Jak dynamicznie przydzielać pamięć zmiennym w natywnym C++. • Jak
działa
dynamiczne przydzielanie pamięci w programach CLR.
• Czym są uchwyty i odwołania w programach CLR.
• Jak
pracować
• Czym
śledzące
oraz dlaczego potrzebujemy ich
z łańcuchami znaków i tablicami w programach w C++/CLI.
są wskaźniki wewnętrzne
i jak
się je
tworzy oraz jak
się
ich
używa.
W rozdziale tym będziemy znacznie częściej korzystali z obiektów , mimo że nie wiemy jesz cze, jak się je tworzy, ale jeśli nie wszystko jest całkiem jasne, nie musimy się tym przejmo wać. Bardziej szczegółowo na temat klas i obiektów będziemy mówić od rozdziału 7.
190
Visual C++ 2005. Od podstaw
ObslUga wielu warlości danych lego samego Iypu Wiemy już, jak zadeklarować i zainicjalizować różnego typu zmienne, z których każda prze chowuje pojedynczy fragment informacji. Fragmenty takie będę nazywał elementami danych. Potrafimy stworzyć zmienną typu cha r, przechowującą pojedynczy znak, zmienną typu snort , i nt i long, przechowującą jedną liczbę całkowitą, oraz zmienną typu f loat lub double, która przechowuje pojedynczą liczbę zmiennopozycyjną. Oczywistym krokiem naprzód jest naucze nie się operowania kilkoma elementami danych określonego typu za pomocą jednej nazwy zmiennej . Umiejętność taka umożliwiłaby nam znaczne rozszerzenie naszych możliwości. Oto przykład prezentujący, w jakiej sytuacji moglibyśmy tego potrzebować. Przypuśćmy, że mamy napisać program obsługujący listę płac. Tworzenie zmiennej o nowej nazwie dla pensji czy informacji podatkowych każdego pracownika byłoby zadaniem co najmniej żmudnym. O wiele wygodniej byłoby móc odnosić się do tego pracownika za pomocąjednej nazwy gene rycznej, takiej jak np. nazwaPracownika, oraz mieć inne nazwy generyczne dla różnego rodzaju danych każdego z pracowników, jak pensja czy podatek itd. Oczywiście przydałaby się także możliwość odnalezienia określonego pracownika wśród wielu innych pracowników oraz wydo bycia skojarzonych z nim danych generycznych, które są przechowywane w zmiennych . Tego typu potrzeba pojawia się za każdym razem , gdy mamy do czynienia ze zbiorem podmiotów, którymi chcemy zarządzać w programie - mogą to być zarówno piłkarze, jak i okręty wojenne. W C++ oczywiście dostępne są takie mechanizmy.
Tablice Podstawą rozwiązania
wymienionych problemów w C++ ISO/ANSI są tablice. Tablica to po prostu zbiór miejsc w pamięci, zwanych elementami tablicy lub prościej elementami, z któ rych każdy może przechowywać dane tego samego typu i do których odwołujemy się za po mocą tej samej nazwy zmiennej . Nazwy pracowników z listy płac można by było przechowy wać w jednej tablicy, ich płace w drugiej , a należny podatek w trzeciej. Poszczególne elementy w tablicy są określane przez wartość indeksową, która jest po prostu liczbą całkowitą i reprezentuje wszystkie elementy tablicy za pomocą kolejnych liczb, zaczy nając od zera. Wartość indeksową elementu tablicy można sobie także wyobrazić jako war tość jego przesunięcia od pierwszego elementu tablicy. Pierwszy element ma wartość prze sunięcia równą zero , a zatem jego indeks to O. Wartość indeksowa 3 odnosi się zatem do czwartego elementu w tablicy. W przypadku listy płac można by było tak zaprojektować tablice, żeby wartości indeksowe odnoszące się do nazwy pracownika, jego płac oraz informacji podat kowych miały ten sam indeks w poszczególnych tablicach. Podstawowa struktura tablicy
została przedstawiona
na rysunku 4.1.
Rysunek 4.1 przedstawia tablicę . Tablica o nazwie height zawiera sześć elementów , z których każdy przechowuje inną wartość. Mogą się one odnosić na przykład do wzrostu członków rodziny w porządku malejącym lub rosnącym. Jako że elementów jest sześć, to indeksy mają wartości od Odo 5. W celu odniesienia się do określonego elementu piszemy nazwę tablicy,
Rozdział 4.•
Rysunek 4.1
Tablice. łańcuch» znaków i wskaźniki
Warto ś ć indeksu _ _---, drugiego elementu
Nazwa
Wartość piątego
indeksu _ elementu
_
191
---,
"bl;" l
heightlO] 73
I
height[l]
heightl2]
height[3]
height[4]
heightlS]
62
51
42
41
34
Tablica height zawiera 6 elementów
po której następuje wartość indeksowa wybranego elementu otoczona nawiasami kwadra towymi. Do trzeciego elementu tablicy odwołaliby śmy s i ę za pomocą notacji hei ght[ 2]. Jeżeli przyjmiemy, że indeks stanowi warto ść przesunięcia względem pierwszego elementu, to z łatwością zauważymy , że indeks na przykład czwartego elementu wynosi 3. Ilość pamięci potrzebnej do przechowywania każdego elementu uzależniona jest od jego typu. Wszystkie elementy tablicy przechowywane są w sąsiadujących blokach pamięci .
Deklarowanie tablic Zasadniczo tablice deklaruje się tak samo jak zmienne . Jedyna różnica polega na tym, że bez pośrednio po nazwie tablicy w nawiasach kwadratowych znajduje s ię liczba jej elementów. Można na przykład zadeklarować tablicę liczb całkowitych o nazwie hei ght , którą widzieliśmy na poprzednim rysunku, za pomocą następującej instrukcji: long height [ 6J;
Jako że wartości typu l ong wymagają czterech bajtów pamięci , cała tablica zajmie ich 24. Ta blice mogą być dowolnego rozmiaru, oczywiście w granicach określonych przez ilość pamięci dostępnej na danej platformie sprzętowej . Tablice mogą być dowolnego typu . Aby na przykład utworzyć tablice do przechowywania informacji o pojemności i mocy silników, można napi sać następujące instrukcje: doubl e cubi c_i nc hes [ 10]; II Pojemność silnika.
doubl e hor sepower [ 10] ; II Moc silnika.
Jeżeli
interesujesz się samochodami, to w tych tablicach możesz przechowywać informacje o pojemności i mocy do dziesięciu silników, do których można się odnosić za pomocą warto ści indeksowych od Odo 9. Podobnie jak w przypadku innych zmiennych, w jednej instrukcji można zadeklarować kilka tablic określonego typu, ale w praktyce prawie zawsze lepiej jest każdą deklarację umieszczać w oddzielnym wierszu.
192
Visual C++ 2005. Od podslaw
~ Używanie tablic Przed przejściem do analizy kodu wyobraź sobie, że przy ka żdym tankowaniu zapisujesz i l ość zakupionej benzyny do samochodu oraz l i czbę przejechanych kilometrów. Może sz teraz napisa ć program anal i zuj ący te dane w celu sprawdzeni a, jak wygląda zużycie paliwa za każ dym razem, gdy je kupujesz: II Cw4_0i .cpp
II Oblicza nie liczby kilometrów p rzejec hanyc h na jednym baku.
#include
#include
using using usi ng using
std: .ci n:
st d: :cout:
std : :endl :
st d: :setw:
int main() {
const int MAX = za : doub le gas[ MAX J: long mi les[ MAX J: int count = o: char i ndicat or = ' t ':
II Maksym alna liczba wartości . II Jloś ć benzyny w litrach. II Wskazania liczn ika przebieg u. II Licznik pętli . II Wskaźn ik wprowadzania dany ch.
wh ile ( (i nd icat or == 't ' II indicat or == 'T') && count < MAX ) (
cout
« «
cin » cout « cin »
end l "Podaj il oś ć pa l iwa : II Wczytaj ilosć p aliwa. gas[countJ: "Podaj dane z l icznl ka przebiegu' " mi les[count J : II Wczyt aj wartoś ć z licznika przebiegu.
++count:
cout « "Czy chcesz ci n » indicat or :
i f( count <= cout
II
Wysyłan ie
są
co na jmniej dwa zest awy danych.
na wyjsc ie wyników od drugiego do ostatniego wpisu.
for( i nt i = l : l < count : i++) cout « endl « setw( Z) « i « " . " . « "Zakupiono = " « gas[i] « "których wyda j n o ś ć t o " «
dane(t l ub n)?
II count = i po ukoń czen iu wprow adzani a II p ienvszych danych. 11...musimy podać jeszcze jeden zestaw dany ch.
1)
endl "Potrzebne retu rn O: « «
po d a ć na st ęp n e
(mil es[i J - mil es[i -
II Num er sekwencji wysyłając ej. II Wysyłanie info rmacji o paliwie. Il inf ormacj a o kilometrach II p rzej echany ch na litrze. I J)/gas[ iJ « " ki lomet rów na l i t r .'" «
"
l i t rów pa l iwa."
Rozdzial4.•
Tablice.lańcuchYznaków i wskaźniki
193
cout « end l : ret urn O:
Program
za kład a, że
za każd ym razem nap ełniamy bak, a w i ęc il o ść zakupionego paliwa jest na przejechanie pod anego dystansu. Poni ż ej znajduje się przykł adowy wynik tego programu :
ilością zużytą dział ania
Podaj i l o ś ć pa l iwa: 12.8
Podaj dane z l iczni ka przebiegu: 25832
Czy chcesz pod ać n as t ępne dane (t l UD n)? t
Podaj i l o ś ć pal iwa: 14.9
Podaj dane z l iczni ka przeDiegu: 26337
Czy chcesz po d a ć nas tę p n e dane (t lub n)? t
Podaj i lo ś ć pal iwa: 11 .8
Poda j dane z l icznika przebiegu: 26598
Czy chcesz po d a ć n as t ę p ne dane (t l ub n)? n
l . Zakup i ono 14.9 l it rów pal iwa. kt órych wyd a jno ś ć t o 33.8926 kil omet rów na l i t r . 2. Zakupiono 11.8 l i t rów pali wa . których wyd ajn ość t o 22.1186 ki lomet rów na l i tr.
Jak lo działa Poniewa ż
w celu obliczenia liczby przejechanych kilometrów na zakupionym paliwie musimy dwoma wskazaniami licznika przebiegu, to z pierwszej pary wpro wadzonych danych pobieramy tylko wskazania tego licznika. Odrzucamy natomiast iloś ć zaku pionego paliwa w pierwszym przypadku, ponieważ zo stało ono zużyte wcze śniej . ob liczyć różnicę pom iędzy
Podczas drugiego okresu pokazan ego w wynikach mu si ały być straszne korki lub jeźd zil iśmy , m aj ąc zac iągn i ęty hamulec ręczny. Rozmiary dwóch tablic gas oraz mi les, użytych do prze chowywania wprowadzonych danych, określ a stała o nazwie MAX. Modyfikując wartość zmien nej MAX, zmieniamy maksymalną liczbę wprowadzanych danych . Technika ta jest często wyko rzystywana do nadawania programowi el astyczności zw iązanej z ilością obsługiwanych przez niego danych. Oczywi ście cały kod programu musi zo stać napisany z my śl ą o rozmiarach tablicy lub innych paramet rów, których rozm iary są określane za p omocą zmiennych typu const . Jest to zadanie bardzo proste i nie ma powodu unikać takiego rozw i ązania . P óźniej dowi emy si ę je szcze, jak przyd zi elać p amięć do przechowywania danych podczas działani a programu, dzięki czemu nie ma potrzeby ustalania z góry ilości pamięci przydzielonej dla prze chowywanych danych.
Wprowadzanie danych Dane wczytywan e są za pomocą pętli whi le . Jako że zmienna pętl owa count może dz iałać od O do warto ści MAX - l, użytkownik nie b ędzie mógł poda ć wi ę c ej warto ści niż tabli ca mo że pomieś ci ć . Zmienn e count i i ndi cat or inicjalizujemy odpowiednio wartościami O i t, dzięki czemu pętla wht l e wykonana zostanie co najmniej raz. Program prosi o podanie wymaganych danych, a podane wartośc i są umieszczane w odpowiednich elementach tablicy. Element użyty do przechowywania danej warto śc i jest okre śl ony przez zmi enną count, której wartość za
194
Visual C++ 2005. 011 podstaw pierwszym razem wynosi O. Element tablicy jest określony w instrukcji zmiennej count jako indeksu. Na stępnie zmienna count zostaje i jest gotowa do przyjęci a następnej warto ści .
użyciu
wejściowej
ci n przy o jeden
zwiększon a
Po wprowadzeniu każdej wartości program żąda potwierdzenia zamiaru wprowadzenia na s tępn ej. Podany znak zostaje zapisany jako wartość zmiennej i ndi cato r , a następnie spraw- . dzany w warunku pętli . Działanie pętli zakończy się, jeżeli podany znak nie jest literą "t" lub "T", lub wartość zmiennej count jest większa niż wartość zmiennej MAX. Po zakończeniu pętli wczytującej dane (bez względu na sposób) wartość zmiennej count jest o jeden w iększa niż wartoś ć indeksowa ostatniego wprowadzonego do każdej tablicy elementu (pamiętamy, że zwiększamy ją po wprowadzeniu każdego nowego elementu). Sprawdzanie to dokonywane jest w celu upewnienia się, że podane zostały co najmniej dwie pary wartości . J eżeli nie, program kończy się wyświetleniem odpowiedniej infonnacji, ponieważ do oblicze nia wartości przejechanych kilometrów potrzebne są dwa wskazania licznika przebiegu.
Tworzenie wyników Wyniki generowane są w pętli for . Zmienna kontrolna i przyjmuje wartości od 1 do count - 1, na obliczanie przebiegu jako różnicy pomiędzy b ieżącym elementem mi l es [ i ] oraz elementem poprzednim mi l es [ i -1]. Zauważmy, że wartością indeksu może być wyraże nie, które w wyniku daje liczbę całkowitą, mogącą być indeksem w omawianej tablicy, czyli od zera do pomniej szonej o jeden liczby elementów w tablicy.
pozwalając
Je żeli wartość wyrażenia
indeksowego nie należy do zbioru prawidłowych indeksów dla ele mentów tablicy, to odniesiemy się do nieprawidłowej lokalizacji danych, która może zawierać inne, niepotrzebne dane lub nawet kod programu. Jeżeli odniesienie do takiego elementu pojawi się w wyrażeniu, to w obliczeniach użyjemy przypadkowych danych, przez co uzyskamy wy nik, którego z pewnością się nie spodziewaliśmy . Je śli przechowujemy wynik w elemencie tablicy korzystającym z niedozwolonej wartości indeksu, to nadpiszemy to, co znajduje się w tej lokalizacji. Jeżeli będzie to część naszego programu, to skutek będzie katastrofalny. Nie prawidłowe wartości indeksów nie s ą bowiem zgłaszane ani przez kompilator, ani podczas działania programu. Jedynym sposobem obrony przed nimi jest takie zaprojektowanie progra mu, aby zapobiegał takim sytuacjom.
Dane wyjściowe generowane są przez pojedynczą instrukcję wyjściową dla wszystkich wpro wadzonych wartości poza pierwszą. Dla każdego wiersza wyników generowany jest także jego numer za pomocą zmiennej kontrolnej pętli i. Liczba kilometrów na litr obliczana jest bezpośrednio w wyrażeniu wyjściowym . Elementów tablic w wyrażeniach można używać dokładnie tak samo jak innych zmiennych.
Inicializacia tablic Aby zainicjalizować tablicę w miejscu jej deklaracji, należy po nazwie tej tablicy postawić znak r ówno ści , za którym podajemy rozdzielaną przecinkami listę wartości początkowych otoczonych nawiasami klamrowymi . Poniżej znajduje się przykład jednoczesnej deklaracji i inicjalizacji tablicy :
Rozdzial4.• Tablice. łańcuch' znaków i wskaźniki i nt cubic l nches[5J
=
{
195
20 0. 250 . 300 . 350 . 400 }:
Tablica nazywa się cubic_i nches i ma pięć elementów przechowujących liczby całkowite. Wartości podane podczas inicjalizacji odpowiadają n astępującym po sobie wartościom indek sów tablicy . W tym przypadku element cubic_i nches[OJ ma wartość 200, cubi c_i nches[lJ 250, cubi c_i nches[2J - 300 i tak dalej . Nie można podać więcej wartości, niż tablica może pomieścić, ale można podać mniej. Jeżeli jest ich mniej, to wartości indeksów są przydzielane na zwykłych zasadach - pierwszy ele ment ma indek s O, a następne kolejne indeksy. Elementy, dla których nie podaliśmy wartości początkowej , inicjalizowane są wartością zero . Nie jest to jednak r ównoznaczne z niepodaniem żadnej listy . Bez listy warto ści inicjalizujących elementy tablicy zawierałyby przypadkowe wartości. Jeżeli podajemy listę wartości początkowych, to musimy podać w niej co najmniej jedną wartość. W przeciwnym przypadku kompilator zgłosi błąd . Przedstawię to na poniższym raczej ograniczonym przykładzie.
~ Inicializowanie tablicy II Cw4_02.cpp
/r lnicjałizowan ie tablicy.
#include #incl ude using std: :cout :
usi ng std : :endl :
using st d: :set w:
i nt mai n( ) {
i nt va l ue[5J = { l . 2. 3 }:
int junk [5J:
cout « endl :
for (i nt i = O: i < 5: i ++ )
cout « set w(12 ) « val ue[lJ :
cout « end l :
tor r i nt i = O: i < 5: i++)
cout « set w( 12) « j unk[i J:
cout « endl : retur n O: tym deklarujemy dwie tablice. Pierwszą z nich - valu e - inicjalizujemy tylko a drugiej - j un k - nie inicjalizujemy w ogóle. Program generuje dwa wiersze, które na moim komputerze wyglądają następująco: W
Jak to działa tablicy val ue są wartoś ci a mi i n i cj a l iz ując y m i, a p ozostałe dwie m aj ą czyli zero. W tablicy j unk wszystkie warto ści są przypadkow e, ponieważ nie podaliśmy ani jednej wartości początkowej. Elementy tej tablicy zawi erają to, co z os tawi ł program, który u żywał zajmowanych przez nie obszarów pami ęc i .
Pierwsze trzy
wa rtośc i
warto ś c i domyślne ,
Aby w wygodny sposób zai n i cj a l izować tabl icę wartościami zerowymi, n ależy podczas inicjalizacji p odać jedną w arto ś ć równ ą zero. Na przykład : l ong data [l00] = {Ol ; II Nadaj wszystkim elementom wartość początko wą zero.
instrukcja deklaruje tabli cę o nazwie dat a, w której wszystkie 100 elementów będzi e zero. Pierwszy element został zainicjalizowany wartością w nawiasach kwadratowych , a pozostałe są również zainicjalizowane warto ścią zero, gdyż ich warto ści nie zos tały podane . P owyższa
mi ało wartość
Można także pom inąć
rozmiar tablicy typu numerycznego, pod warunki em że podamy wartoLiczba elementów tablicy okre śl on a jest przez l i czbę podanych wa rtośc i poNa przykład deklaracja tablicy:
ści początkowe . czątko wych.
i nt val ue[ ] = { 2. 3, 4
l;
defin iuje trzy elementową tab licę o w artościach
po czątkowy ch
2, 3 i 4.
Tablice znakowe oraz obsltma łańcuchów Tablica typu char zwana jest tablicą znakową i służy przede wszystkim do przechowywania łań cu chów znaków, Łańcuch znaków to zbiór znaków zakończo ny specjalnym znakiem okreś l aj ącym je go koniec. Sekwencja znaków oznac zająca koniec łań cucha zdefi niowana jest za pomocą kodu \ Oi czasami nazywa si ę j ą znakiem zerowym , który jest bajtem ze wszystkimi bitami zerowymi . Taka forma znaku zerowego nazywana jest czę sto ł ańcu chem w stylu C, p onieważ ten sposób definiowania ł ańcuchó w zo stał po raz pierwszy wprowadzony w języku C, od którego pochodzi C++, stworz ony przez programistę Bjame'a Strou strupa. Nie jest to jedyna m ożl iwa reprezentacj a łań cu ch a - inne poznam y troch ę p óźni ej . Innej reprezenta cji w szczegó l no ś c i u żywaj ą programy w C++/CLI, a w bibliotece MFC zdefiniowan a jest klasa CStri ng s ł u żąc a do reprezentowania łańcuchów . Reprezentacja ł ań cu cha w
pamię c i
Rysunek 4.2
w stylu C widoczna je st na rysunku 4.2. name[4]
Znak końca łańcu cha
znak w ła ń cuchu
zajmuje jeden bajt
Każdy
char name[] = "Albert Einstein";
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
197
Rysunek 4.2 przedstawia wygląd łańcuc ha w pamięci oraz poka zuje sposób dek larowania o którym za chwilę będziemy mówić .
łańcuchów, Każdy
znak w łańcuchu zaj muje j eden bajt, a więc ile ma znaków p lus j eden dla znaku zerowego.
Tablicę znakową możn a zadeklarować
Na
i
każdy łań cuch
zainicjalizować
za
zajmuj e tyle pamięci,
pomocą Iiterału łańcuchowego .
przykład :
char movi e st ar[15]
=
"Mari lyn Mon roe";
Należy nadmienić , że kończący
znak zerowy wstawiany jest automatycznie przez kompilator. taki znak samodzielnie, to po kompilacji będziemy mieli dwa takie znaki. Musimy po zw o l i ć na wstawianie znaku zerowego w liczbie elementów , które wprowadzamy do tab licy.
Jeżeli dołączymy
Można pozwolić kompilatorowi sprawdzić długość zainicjalizowanej tablicy za nas, jak na rysunku 4.1. Po niżej znajduje się je szcze jeden przykład:
widać
char presi dent[] = "U lysses Grant "; Jako że rozmiar tablicy nie został okre ś lony , komp ilator przydziela wystarczająco pamięci, aby elementy mogły pomieścić inicjalizujący łańcuch wraz z koń czącym znakiem zerowym . W tym przypadku pamięć zostaje przydzie lona dla 14 elementów tablicy preside nt. Oczywi ście , jeże li zechcemy później użyć tej tablicy do przechowywania innego łań cucha , to nie może on być dłuższy ni ż 14 bajtów (łącznie ze znakiem kończącym) . Obowiązk iem programisty jest zapewnić taki rozmiar tablicy, aby mogła ona pomieścić każdy łań cuc h , który chcemy w niej przechowywać.
Wprowadzanie łańcuchów znaków do programu Plik n agłówk owy zawiera definicje kilku funkcj i służących do wczytywania zna ków z klawiatury. Jedną z nich, o której b ędziemy teraz mówić, jes t funkcja get Ltner ), przyj mująca sekwencje znaków wprowadzonych za pomocą klawiatury i przechowująca je w tablicy znakowej jako łań cu ch zakończony znakiem \ 0. Typowe instrukcje przy użyc i u tej funkcji wyglądają n astępuj ąc o :
const i nt MAX = 80; char name[MAX]; ci n.getl i ne(name. MAX, ' \ n' );
II Maksymalna dlugość lań cucha włącznie z lO.
II Tablica do przechowywania łańcucha.
II Wczytanie danych wejś c iowyc h j ako łańcucha .
P ow yżs ze
instrukcje najpierw d ekl aruj ą tabli c ę znakową o nazwie name, która m o że pomie MAX l i czbę elementów, a n astępn i e za p o m ocą funkcj i get Li ne( ) przyj mują znaki ze stru mienia wejściowego . Jak wi dać , źródło danych (ci n) zostało zapisane z dwukropkiem oddzie l aj ącym j e od nazwy funkcji. Znaczenie argumentów funkcj i getLi ne pokazano na rysunku 4.3. ścić
Jako że ostatnim argumentem funkcji get Li ne( ) jest znak \ n (znak nowego wiersza lub końca wiersza) , a drugim argumentem je st MAX, znaki są wczytywane aż do napo tka nia znak u \ n lub wczytania liczby znaków równej MAX- l , w za leżności od tego, co pierwsze nastąpi . Mak symalna liczba wczytanych znaków wynosi MAX- l , a nie MAX, aby pozostawić miejsce dla znaku
198
Visual C++ 2005. Od pods1aw
Rysunek 4.3
Nazwa ta blicy typu char[] , w któ rej są przec howywane znaki ze str umienia wejściowego
cin
Znak m ający z a t r zy m a ć proces wprowadzania danych . Można w tym miejscu podać j eden znak, któ rego pie rwsze wystąpi enie spowoduje zatrzy manie procesu w prowadza nia danych
l l
Maksymalna liczba wczytan ych znaków. Po wczy taniu mak symalnej liczby znaków wprowadzani e danych zatrzymuje się
- --
-,
cin.getl ine( name, MAX, '\ n' );
zerowego, który zostanie dołączony do sekwencji znaków przechowywanych w tablicy. Znak \n generowany jest w momencie nac i śn i ęci a klawisza Enter i jest zazwyczaj najwygodniejszym sposobem zakończenia wprowadzanyc h danych. Zmieniając ostatni argument, można jednak okreś lić coś jeszcze. Znak \ n nie jes t przechowywany w tablicy name, ale - jak j uż mówi łem - \ 0 dodawany jest na końc u łańcuc ha wejściowego w tej tablicy. Więcej na temat tej składn i dowiemy się przy okazji omawiania klas. Na razie musisz przyj ąć do wiado mości, że takie coś istnieje, bowiem użyj e my tego w przy kładzie .
~ PrOgramowanie zzastosowaniem łańcuchów Mamy j uż teraz odpowiednio d użo wiedzy, abyśmy mogli znaków i l i czący , z ilu znaków się on składa .
napisać
prosty program
łańcuch
II Cw4_03.cpp
II Liczenie znaków w
łań cu chu.
#include
using st d:: ci n:
using std : :cout:
usi ng st d: :endl :
int mai n() {
const int MAX = 80: char buffer[ MAXJ : int count = O:
II Maksymalny rozmi ar tablicy .
II Bufor dany ch wejś cio wych.
II Licznik znak ów.
cout « "W prowadź łańcuch zna ków ci n.getl i ne(buffer . MAX. ' \n'): whi le (buffer[countJ cou nt++ : cout
« « «
1=
s k ł a d a j ą c y s ię
II Wczytuj
' \ 0' )
\ "" « buffer
zawiera " « count «
" Łań cuc h
cout « end l : return O:
niż
80 zna ków :\n" : In.
do napotkania
II Zwiększaj licznik . dopóki
II b ieżący znak nie j est ze rowy.
endl
"\ "
z mniej
łań cuch aż
"
znaków ." :
pob i eraj ący
Rozdział 4.• Przykładowy
Tablice, łańcuchy znaków i wskaźniki
199
wynik działania tego programu może być następujący:
Wprowadź łańc uch składający się
z mniej
niż
80 zna ków :
Promieni owanie zabij a geny Ła ń c u c h
"Promi eniowa ni e zabija geny" zawiera 26 znaków.
Jak to działa Program ten deklaruje tablicę znakową o nazwie buffer i przyjmuje łańcuch znaków z kla wiatury do tablicy po uprzednim wyświetleniu prośby o podanie łańcucha. Wczytywanie z klawiatury kończy się w momencie naciśnięcia przycisku Enter lub gdy liczba wczytanych znaków będzie równa MAX - 1. Pętla whi le służy do liczenia liczby wprowadzanych znaków . Jej działanie kontynuowane jest, dopóki bieżący znak, do którego odwołaniem jest bu ffer[count ], nie jest znakiem \0. Ten rodzaj sprawdzania bieżącego znaku podczas przechodzenia przez tablicę jest często spotykaną techniką w C++. Jedyną czynnością wykonywaną w pętli jest zwiększanie zmiennej count za każdym razem, gdy znak nie jest znakiem zerowym.
Istnieje też funkcja biblioteczna st rlen( l, która może zaoszczędzić nam fatygi samodzielnego pisania kodu. Jeśli chcemy jej użyć, musimy dołączyć plik nagłówkowy do programu za pomocą dyrektywy #i ncl ude, jak widać poniżej:
#include Litera c w nazwie pliku nagłówkowego oznacza, że zawiera on definicje należące do biblioteki C, który częściowo tworzy standardową bibliotekę C++. Nagłówek ten zawiera rów nież funkcję wcsnl en( l , która zwraca długość szerokiego łańcucha znaków.
języka
Za
pomocą
funkcji st rl en( l moglibyśmy
zastąpić pętlę
whi le następującą instrukcją:
count = std: :st rlen(buffer); Jako argument podaliśmy nazwę tablicy zawierającej łańcuch . Funkcja st rlen() zwraca dłu gość takiego łańcucha w postaci liczby całkowitej bez znaku typu siez_t . Wiele funkcji biblio teki standardowej zwraca wartości typu si ze_t, który jest zdefiniowany w bibliotece stan dardowej przy użyciu instrukcji typedef jako ekwiwalent jednego z typów fundamentalnych najczęściej i nt bez znaku. Powodem używania typu si ze_t zamiast bezpośrednio jednego z typów fundamentalnych jest fakt, że pozwala on na pewną elastyczność co do tego, jaki jest rzeczywisty typ w różnych implementacjach C++. Standard C++ pozwala na urozmaicenie zestawu wartości należących do typów fundamentalnych w celu maksymalnego wykorzystania architektury sprzętu , a typ si ze_t może być zdefiniowany jako ekwiwalent najodpowiedniej szego typu fundamentalnego w bieżącym środowisku sprzętowym . kodu za pomocą jednej instrukcji wyjściowej zostają wyświetlone oraz liczba znaków. Zwróć uwagę na kod znaku specjalnego , który został użyty w celu wyświetlenia podwójnego cudzysłowu.
Na
końcu przykładowego
łańcuch
200
Visual C++ 2005. Od podstaw
Tablice wielowymiarowe Tablice z jednym indeksem, które definiowaliśmy do tej pory , zwane są tablicami jednowy miarowymi. Tablica może jednak posiadać więcej indeksów niż jeden i w takim przypadku nazywa się ona tablicą wielowymiarową . Przypuśćmy, że mamy pole, na którym hodujemy fasolę w rządkach po 10 krzaków, i że pole zawiera 12 takich rządków (tak więc w sumie mamy 120 krzaków). Moglibyśmy zadeklarować tablicę, do której zapisywalibyśmy wagę fasoli ze branej z każdego krzaka za pomocą następującej instrukcji: doub le beans[12][10] : Powyższa
instrukcja deklaruje dwuwymiarową tablicę o nazwie beans . Pierwszy jej indeks odpowiada numerom rządków, a drugi numerom krzaków w tych rządkach . Aby odnieść się do określonego elementu tej tablicy, potrzebujemy dwóch indeksów. Aby na przykład ustawić wartość elementu odpowiadającego piątemu krzakowi w trzecim rządku, posłużylibyśmy się następującą instrukcją:
bean s[2][4]
~
10 .7;
Pamiętajmy, że wartości
indeksów
rozpoczynają się
od zera, dlatego indeks
rządka
to 2, a in
deks piątego krzaka to 4. Jako że jesteśmy bogatymi rolnikami, mamy jeszcze kilka takich pól fasoli. Zakładając , mamy ich osiem, moglibyśmy utworzyć tablicę trójwymiarową do zapisywania danych :
że
double beans[B ][ 12][10] : Powyższa tablica przechowuje dane dotyczące wszystkich krzaków fasoli na wszystkich polach . Pierwszy indeks z lewej odnosi się do określonego pola. Jeżeli uda nam się rozwi nąć hodowlę do rozmiarów międzynarodowych, możemy użyć tabeli czterowymiarowej . W czwartej tabeli przechowywalibyśmy nazwy poszczególnych państw. Zakładając, że jeste śmy tak samo dobrymi sprzedawcami, jak rolnikami, produkowanie tak dużych ilości fasoli w celu nadążenia za popytem może mieć niekorzystny wpływ na dziurę ozonową.
Tablice przechowywane są w pamięci w taki sposób, że wartość indeksu znajdująca się naj dalej po prawej stronie jest zmieniana najwcześniej . A zatem tablica dat a[3J [4] składa się z trzech jednowymiarowych tablic, z których każda zawiera cztery elementy. Schemat tej tablicy widoczny jest na rysunku 4.4. Elementy tej tablicy przechowywane są w przylegających blokach pamięci, jak wskazują rysunku 4.4. Pierwszy indeks odnosi się do określonego wiersza w tablicy, a drugi do określonego elementu w tym wierszu .
strzałki na
Zauważmy , że
tablica dwuwymiarowa w natywnym C++ jest w rzeczywistości tablicąjed tablice jednowymiarowe. Tablica trójwymiarowa w natywnym C++ jest w rzeczywistości jednowymiarową tablicą elementów, które stanowią jednowy miarowe tablice tablic jednowymiarowych. W większości przypadków nie trzeba się tym wszystkim przejmować, ale jak później zobaczymy, tablice w C++/CLI nie są takie same jak w natywnym C++ . Z powyższego wynika również, że wyrażenia data [OJ, dat a[lJ oraz data [2J reprezentują tablice jednowymiarowe. nowymiarową zawierającą
Rozdzial4. • Tablice. łańcuchy znaków i wskaźniki Rysunek 4.4
Elementy tabl icy przechowywane są do siebie sektorach pam ię ci
przyl egających
Inicializowanie tablic wielowymiarowych Do inicja lizacj i tablicy wielowymi arowej u żyw a s i ę rozszerzonej wersji metody u żyt ej do inicjal izacji tablic jednowymiarowych. Aby na przykład zaini cjalizować dwuwymi arową tab l i cę o nazwie dat a, mo żemy posłużyć się n astępuj ąc ą d eklaracj ą: l ong data[2 ][4]
~
{ { 1.2.3. 5 }. { 7. 11, 13 . 17 };
Jak wid ać, wartości początkowe każdego wiersza tablicy znaj duj ą się pomiędzy parą własnych nawiasów klamrowy ch. Jako że w k ażdym wierszu są cztery element y, w każdej grupie znajdują się cztery warto ś ci p o czątkowe , a ponieważ mamy dwa wiersze, to mamy też dwie grupy warto śc i w nawiasach klamr owych. Każda grup a wartości początkowy ch oddzielona jest od następn ej przecinkiem. Wartoś ci początkowe w k ażdym w ierszu można pominąć. W takim przypadku pisane wartości zerowe. Na przykład :
l ong data[2][4 ]
zo s taną im przy-
=
{ 1. 2 . 3 }. { 7. 11 } }; Za s t oso w ałe m
dodatkowe spacje pomiędzy w arto ściami po czątkowym i , aby poka za ć , gdzie Elementy data [OJ [3J, data[l J[2J oraz dat a[l J[3J nie maj ą warpo czątkowych, a więc zostają ustawione na zero.
wartoś ci zostały pominięte . tości
Gdyby śmy
chcieli
wartości
napi s ać:
l ong data [2][4]
=
{O};
wszystkich elem entów tablicy
ustaw i ć
na zero , to
m oglibyśmy
202
Visual C++ 2005. Od podstaw Inicja liz ując
tablice o większej liczbie wymiarów, należy pamię tać , że będzie potrzeba tyle grup wartości początkowych w nawiasach klamrowych, ile jest wymiarów
zag n ieżdżo nyc h
w tablicy .
~ Przechowywanie wielu łańcuchów Do przechowywania kilku łań cuchów w stylu C możemy użyć jednej dwuwymiarowej tablicy. Jak tego dokonać, prześ ledzimy na poniższym przykładzie : II Cw4_04.cpp
II Przechowywanie lańcu ch ów w tablicy .
#i nclude
usi ng std : :cout ;
using std : .ct n:
using std : :endl ;
i nt mai n() {
char sta rs[6 ][80 ] =
"Rober t Redfo rd" .
"Hopa long Cassidy".
"La ss ie" .
"Sl im Pickens",
"Bor i s Ka r loff " .
"Ol i ver Hardy "
}:
int dice = O; cout « endl
« " Wybierz szc zęś liwą gwiazdęl « " Podaj cyfrę od l do 6: «.
ci n » dice :
"
II Sprawdź poprawność wpro wadzanych danych. i f( di ce >= l && dice <= 6) cout « end l II Wyświetl dane gw iazdy. « "Twoj a szczęś l i wa gwi azda to " « st ars [di ce - l ]; el se cout « endl II Wprowadzono niepra widlowe dane. « "Przykro mi . al e nie ma sz swojej szczę śl i we j gwiazdy. ";
cout « endl ; ret urn O:
Jak to działa Poza ogromną wartością rozrywkową, najbardziej interesującą częścią tego kodu jest dekla racja tablicy stars. Jest to dwuwymiarowa tablica elementów typu char, która może prze chowywać do sześciu łańcuchów składających się maksymalnie z 80 znaków każdy (włącznie z kończącym znakiem zerowym dodawanym automatycznie przez kompi lator). Łańcuchy ini cjalizujące zostały umieszczone w nawiasach klamrowych i oddzielone od siebie przecinkami. Jedną z wad korzystania w takich przypadkach z tablicj est fakt. że zarezerwowana pamięć j est prawie n ieużywana. Wszystkie nasze łań cuchy znaków mają mniej niż 80 znaków. przez co nadwyżkowe elementy w każdym wierszu są marnowane.
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
203
Sprawdzenie liczby łańcu chów m ożemy równ ież pozostawi ć kompilatorowi, p omijaj ąc pierw szy wymiar tablicy i pisząc następującą d eklaracj ę : char st ars [J [BO J
~
{ "Robert Redford" .
"Hopalong Cassidy",
"Lassi e",
"51im Pickens".
"Bori s Karlof f ",
"Ol i ver Hardy"
}:
Taki zapis zmusi kompilator do zdefiniowania pierwszego wymiaru w taki sposób, aby m ógł on p omieści ć li czbę ł ańcuchów inicjali zujących , które określili śmy . Jak o że mam y s z eść łańcu ch ów, to wynik będ zie d okładnie taki sam, ale unikamy ryzyka wy stąpienia błędu . Nie moż emy jednak ominąć obu wymiarów tablicy . W przyp adku tablic dwu- lub wielowymia rowych ostatnia tablica po prawej zawsze musi być zdefiniowana. Zwróć uwagę m nieć,
na średn ik znajdujący się na samym końcu deklaracj i. Ła two o nim zapo kiedy podaj e s ię wartoś ci inicjalizujące tablicy.
Chcąc uzyskać dostęp do łańcucha w p oniżs zej instrukcji w celu wysłan ia go na wyj śc i e, mu simy podać tylko pierwszy indeks :
cout « endl « "TwoJa
II s z c ze ś
l iwa gwi azda t o
«
Wyś lij
dane gw iazdy.
stars[dice - 1J ;
Pojedynczy indeks wybiera określoną 80-e lem en tową podtablicę, a instrukcja wyjściowa wy syła jej zawartość do momentu napotkania znaku koń czącego . Indeks zo stał okre ślony jako dic e - l , pon ieważ wartości di ce zaw ierają si ę w zbiorze l - 6, a warto ś ci indeksów muszą należeć do zbioru l - 5.
Pośredni dostęp do danych Zmienne, o których była mowa do tej pory, s tanow i ą nazwany fragment p amięci , gdzie można przechowywać dane o kreś l on ego typu. Ich zawarto ść m oże poch odzi ć ze źródła zewnętrznego, takiego jak np. klawiatura, oraz wewnętrznego , czyli wyników ob li czeń dokonanych z udzia łem innych wprowadzonych warto ś ci . W C++ istnieje jeszcze jeden typ zmiennych, które nie przechowuj ą danych wprowadzonych lub obliczonych w normalny sposób, ale znacznie zw i ększają elastyczno ść programów i nadają im większą moc. Ten typ zmiennej zwany j est wskaźnikiem .
Czym jest wskaźnik Każda
w której przechowywane są j ak i e ś dane, ma swój adres. Za po komputerowe odnos zą s i ę do poszczególnych elementów danych . W skaźnik to zmienna przechowująca adres innej zmiennej okre ślonego typu . Ma on - po dobnie jak wszystkie inne zmienne - swoj ą nazwę, a także typ określający, do jakiego rodzaju lokalizacja w
pamięci ,
mocą adresów urząd zenia
204
Visual C++ 2005. Od podstaw danych się odnosi. Należy pamiętać, że typ zmiennej wskaźnikowej zawiera informację, że jest to wskaźnik. Zmienna, która jest wskaźnikiem mogącym zawierać adresy lokalizacji w pamięci, przechowujących wartości typu i nt, jest wskaźnikiem do i nt .
Deklarowanie wskaźników wskaźnika wygląda
Deklaracja wskaźnika
Aby na
podobnie do deklaracji zwykłej zmiennej, ale przed nazwą jeszcze gwiazdka informująca, że ta zmienna jest właśnie wskaźnikiem. zadeklarować wskaźnik typu 1ang o nazwie pnumber, można posłużyć się
znajduje
przykład
się
następującą instrukcją:
long* pnumber: Powyższa deklaracja została zapisana z gwiazdką przy nazwie typu. jak poniżej:
Możnają także napisać
l ong * pnumber :
Dla kompilatora nie stanowi to żadnej różnicy, ale typ zmiennej pnumber to wskaźnik do typu 1ono, co jest często oznaczane poprzez wstawienie gwiazdki bezpośrednio po nazwie typu. Bez względu na wybrany sposób zapisywania typu wskaźnika należy trzymać się konsekwent nie jednego. Deklaracje
zwykłych
zmiennych i
wskaźników można mieszać
w jednej instrukcji . Na
przykład:
l ong* pnumber. number = 99; Powyższa
instrukcja - tak jak poprzednio - deklaruje wskaźnik o nazwie pnumber , który jest do typu l ang, a także zmienną number typu l ang. Wskaźniki lepiej deklarować jednak oddzielnie od innych zmiennych, ponieważ można się łatwo pomylić co do typu zade klarowanych zmiennych, w szczególności gdy lubimy stawiać * przy nazwie typu. Poniższe instrukcje z pewnością są bardziej przejrzyste, a dodatkowo - dzięki umieszczeniu ich w oddzielnych wierszach - można do każdego z nich dodać oddzielny komentarz, co z kolei powoduje, że kod programu jest łatwiejszy do czytania. wskaźnikiem
l ong numbe r ~ 99; II Deklaracja i inicjalizacja zmiennej typu long.
l ong* pnumber ; II Deklaracja wskaźnika typu long.
od litery p (ang . pointer - wskaźnik) w C++ jest często temu łatwiej się zorientować , które zmienne w programie są co znacznie ułatwia rozszyfrowywanie, co robi dany kod .
Rozpoczynanie nazw
wskaźników
stosowaną konwencją. Dzięki wskaźnikami,
Prześledźmy, jak to działa, na przykładzie, nie zastanawiając się , do czego on służy . Do sposo bów użycia wskaźników przejdziemy niebawem. Przypuśćmy, że mamy zmienną typu l ong o nazwie number, którą zadeklarowaliśmy powyżej, inicjalizując wartością 99. Mamy także wskaźnik pn umber (wskaźnik do typu l ang), którego możemy używać do przechowywania adresu zmiennej number. Ale w jaki sposób uzyskać adres zmiennej?
Rozllział 4.•
Tablice. łańcuchy znaków i wskaźniki
205
Operator adresowania To, czego potrzebujemy, to operator adresowania - &. Jest to operator jednoargumentowy, który sprawdza adres zmiennej . Jest on także zwany operatorem referencji, ale o tym b ędzi emy m ów i ć troch ę p óźniej . Aby ust aw i ć w ska źnik , o którym przed c h w i lą mów ił em , m oż emy p osłu żyć s i ę następując ą in strukcją:
pnumber
=
&number ; II Zapisz adres zmiennej number do pnumber.
Rezultat tej operacji
został
przed stawiony na rysunku 4.5 .
""mb.. ~
RvslJnek4.5
Adres: 1008
l
pnumber 1008
i=jt-- - - - - - ---'
number
99
pnumber = &number;
Operatora & można u żyć do sprawdzenia adresu dowolnej zmiennej, ale do jego przecho wywania potrzebujemy wskaźn ika odpowiedniego typu. J e śli na przykład chcemy przecho w ać adres zmiennej typu doubl e, to wskaźnik musi być zadeklarowany jako typ double *, czyli wskaźnik do typu doubl e.
Używanie wskaźników Sprawdzenie adresu zmiennej i zapisanie go do w skaźnika jest zadaniem ciekawym, ale naj bardziej interesując e jest to, do czego m o żemy go użyć . P odstawą używ ani a wskaźników jest c hęć uzyskania do stępu do danych w zmiennej, na kt órą one wskazują. Dokonuje si ę tego za pomocą operatora pośredniości *.
Operator pośredniości Operatora pośredniości * używamy ze w skaźniki em do uzyskania d o st ępu do zawartości zmiennej , na którą on wskazuje . Nazwa operator pośredniości pochodzi od tego, że dostęp do danych odbywa s ię w sposó b p o średni . Inna nazwa tego operatora to operator wyłuskania, a proces uzyskiwan ia d o st ępu do dan ych w zmie nnej za pomocą ws kaź n i ka nazywa się operacją wyłuskania.
Jednym z myl ących aspektów tego operatora m oże być fakt, że mamy już kilka różnych zasto sowań tego samego symbolu *. Służy on jako operator mnożenia , operator pośredniości, a także używa si ę go w deklaracjach wskaźników . Kompilator potrafi wywni oskow a ć z kontekstu, w jakiej funkcji wystę p uje symbol *. Jeżeli na przykład mnożymy dwie zmienne A*B, to nie ma możliwoś ci zinterpretowania tego wyra żenia w jakikolwiek inny sposób .
206
VisIlai C++ 2005. Od podstaw
Do czego sluzą wskaźniki Jednym z pytań , które najczę śc iej przychodzą do głowy w tak ich chwil ach, jest: "Do czego one m ogą się przydać?" . Bo przec i eż sprawdzanie adresu zmiennej, którą znamy, i zapisywanie go do ws kaźn ika w celu później s ze go jego wyłuskania mo że wydawać si ę czynnośc ią niepotrzebn ą. Jest j ednak kilk a powod ów, dla których wskaźniki są ważne . Jak si ę niebawem przekonamy, notacji wskaźn ikowej m ożna używa ć do operowania na danych przechowywanych w tablicach - jest ona zazwyczaj szybsza ni ż tradycyjna notacja tablicowa. Kiedy dojdziemy do definiowania wła snych funkcji, dowiemy si ę , że wskaźn iki bardzo często używane są do uzyskiwania dostępu z wnęt rza funkcji do du żych bloków danych (takich jak tablice) zdefiniowanych poza nimi. I najważniejs ze - później dowiemy s ię także , że pamięć zmiennym można przydziel a ć dynamicznie podczas wykonywania programu . Ta moż liwość pozwala programowi na dostosowanie poziomu zużyc ia p ami ę ci w za leżn oś ci od iloś ci wprowadzanych do niego danych. Poni eważ nie wiemy z góry, ile zmiennych będzi emy tworzyli dynami cznie, najlepszym sposobem na zrobienie tego są wsk a źniki , a więc warto zapoznać si ę z nimi trochę lepiej.
~ Uzywanie wskaźników Różn e
aspekty operacj i z udział em
ws ka ź n ików prześledzimy
na
poni ż s zym przy kł adzie :
II Cw4_05.cpp II Ćwiczenie użycia wskaźników.
#i ncl ude using std : :cout ; usi ng std : :end l ; usi ng st d;: hex : using st d: .dec: int ma in() {
long* pnumber ~ NULL; long numberi ; 55 . number2
II Deklaracj a i inicja lizacja ~
wskaźnika.
99 ;
pnumbe r = &numbe rl; II Zap isywanie adresu do ws kaźnika. *pnumbe r +; 11 ; II Zwiększen ie zmiennej numb er1 o 11. cout « endl « "numbe rl " « numberi «" &numberl; «hex « pnumber ; pnumber ~ &number2; numberi ; *pnumbe r*l O:
II Zmiana wskaźnika na adres zmiennej number2. II Pomnożenie zmiennej number2 przez 10.
cout « end l « "numberi = " « dec « numberi « pnumber ; " « hex « pnumber « *pnumber = " « dec « *pn umber ; cout « endl : return O:
Rozdział4.•
Na moim komputerze
number l numberl
=
~
powyższy
66 990
Tablice. łańcuchy znaków i wskaźniki
program generuje
&number l pnumber
~ =
0012 FECS 0012FEBC
następujący
207
wynik :
*pnumber = 99
Jak to działa nie wprowadzamy żadnych danych . Wszystkie operacje odbywają się na zmiennych. Po zapisaniu adresu zmiennej numbe rI do wskaźnika pnumber jej wartość zostaje zwiększona pośrednio poprzez wskaźnik w poniższej instrukcji :
W tym
przykładzie
wartościach początkowych
*pnumber
+=
11:
II Zwiększen ie zmiennej numberi o II.
Zauważ, że tos cią
podczas pierwszej deklaracji wskaźnika pnumber zainicjalizowaliśmy go warNUL L. Inicjalizowanie wskaźników będziemy niebawem omawiali.
Operator pośredniości określa , że do warto ści zmiennej numberI, wskazywanej przez wskaź nik pnumber, dodajemy 11. Gdybyśmy zapomnieli w tej instrukcji wstawić operator *, to oznaczałoby to, że chcemy dodać 11 do adresu przechowywanego we wskaźniku. Warto ść
zmiennej numberI oraz jej adres przechowywany we wskaźniku pn umber zostały Manipulatora hex użyliśmy w celu wysłania na wyjście adresu w notacji szesnastkowej. Jeżeli chcemy, aby następne dane były wysłane znowu w notacji dziesiętnej, to w następnej instrukcji wyjściowej musimy skorzystać z manipulatora dec w celu przestawienia trybu wysyłania z powrotem na dziesiętny . wyświetlone.
Po pierwszym wierszu danych wyjściowych zawartość wskaźnika pnumbe r jest ustawiana na adres zmiennej number2. Następnie warto ść zmiennej nuuber l zostaje pomnożona przez 10:
numberI
=
*pnumber*lO :
II Pomno ż en ie zmiennej numberi przez la.
Obliczenie to zostaje wykonane za pomocą uzyskania dostępu do zawartości zmiennej nurnber2 pośrednio poprzez wskaźnik. Rezultat tych obliczeń widoczny jest w drugim wierszu danych wyjściowych .
Adresy, które widzimy w wysłanych na wyjście danych, mogą być za każdym razem inne, gdyż są one zależne od obszaru pamięci, w którym został załadowany program, a to z kolei uzależnion e jest od konfiguracji systemu operacyjnego. Łańcuch Ox przed adresami oznacza, że są to liczby szesnastkowe . Zauważmy , że adresy &number l oraz pnumber (zawierający &nurnber 2) różnią się o cztery bajty. Pokazuje to, że zmienne number I i number2 zajmują w pamięci miejsca obok siebie, jako że każda zmienna typu l ong zajmuje cztery bajty. Z wysłanych danych wynika , że wszystko działa tak, jak można się było tego spodziewać.
Inicjalizowanie wskaźników Używanie niezainicjalizowanych wskaźników jest bardzo ryzykowne, ponieważ możemy za ich pomocą łatwo nadpisać losowe lokalizacje pamięci. Rozmiar szkód uzależniony jest od tego, jak dużego mieliśmy pecha, a zatem inicjalizowanie wskaźników jest czymś więcej niż tylko dobrym pomysłem. Wskaźnik można łatwo zainicjalizować wartością będącą adresem
208
Visual C++ 2005. Od podstaw już istniejącej
zmiennej . W poniższym przykładzie zainicjalizowałem sem zmiennej number za pomocą operatora &z nazwą zmiennej:
int number = o: int* pnumber = &number: Inicjalizując wskaźnik adresem
rowana przed
wskaźnik
pnumber adre-
II Zain icj alizowana zmienna typu calkowit ego. II Zainicjalizowany wskaźnik.
należy pamiętać , że
zmiennej,
zmienna ta musi
być
zadekla-
deklaracją wskaźnika .
Oczywiście możemy
wcale nie chcieć inicj alizować wskaźnika adresem określonej zmiennej podczas jego deklaracji. W takim przypadku możemy go za i n i cj a l i zowa ć wartością zerową. W Visual C++ do tego celu służy symbol NULL, który został zdefiniowany jako O. A zatem wskaźnik można zadeklarować i zainicjalizować za pomocą poniższej instrukcji, zamiast uży wać sposobu z ostatniego przykładu:
int* pnumber
=
NULL :
II
Wskaźnik.
który na nic nie wskazuje.
Dzięki
temu mamy pewność, że wskaźnik nie będzie zawierał adresu, który może zosta ć uznany za prawidłowy, a wartość tego wskaźnika można sprawdzić za pomocą następującej instrukcji i f :
i f(pnumber == NULL) eout « endl « " Ws ka ź m k pnumber ma
w art o ś ć ze rową ." :
Wskaźnik można oczywiście zainicjalizować
jawnie
wartością 0,
co
również
daje nam pew-
ność, że nie zostanie mu przypisana żadna przypadkowa wartość. Żaden obiekt nie może mieć
adresu 0, a więc użycie tej wartości jako wartości wskaźnika oznacza, że na nic on nie wskazuje. Poza faktem, ż e kod może stracić nieco na czytelności , jeśli chcemy go kompilować także w innych kompilatorach, to lepiej jest stosować wartość do inicjalizacji wskaźnika, który ma na nic nie wskazywać.
°
Podejście
to j est takż e zgodne z dobrym stylem programowania w C++ fSO/ANSI. Argument za nim przemawiający to stanowisko, że każdy nazwany obiekt w C+ + powinien posiadać swój typ, a NUL L nie ma określon ego typu - j est aliasem zera. Jak przekonamy się w dalszej części rozdziału, w C++/CLf rzecz ta przedstawia się tro chę inaczej. Aby
zainicjalizować wskaźnik wartością
int* pnumber
=
o:
O, wystarczy napisać:
II Wskaźnik, który na nic nie wskazuje.
W celu sprawdzenia, czy
wskaźnik
zawiera
prawidłowy
adres, można posłużyć
instrukcją:
if(pnumber == O) eout « endl « Równie dobrze
pnumber ma
w ar t o ś ć zerową.
moglibyśmy posłużyć się instrukcją:
if ( lpnumber ) eout « endl Powyższ a
" Wsk a ź n ik
«
" Wsk a ź ni k
pnumber ma
wa rt o ś ć z er ową .
instrukcja robi dokładnie to samo co poprzednia.
się następującą
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
209
Możem y oc zyw i ś c i e zastosować nast ępuj ącą formę:
i f( pnumber II
I~
Wskaźn ik
Wskaźniki do typU
O)
ma prawidłową
wartoś ć.
a
więc użyjmy
go.
char
Wska źn ik
może być
rału
do typu char ma c i ekawą właściwość znakowego . Wskaźn ik taki możemy na przykład następuj ącej instrukcji:
zadeklarować
inicjal izowany za pomocą litei zaini cjal izować za pomocą
char* prover b = "Albo wóz. albo przewóz."; Powyższa instrukcja wygląda podobnie do inicjalizacji tablicy znakowej, ale nie jest identyczna. Tworzy literał łańcuchowy (w rzeczywistości j est to tablica typu const char) z łańcucha znaków znajdującego s ię w cudzy słowach i zakończonego znakiem /0 oraz przechowuje adres tego literału we w skaźniku proverb. Adresem literału b ędzie adre s j ego pierwszego znaku. Przedstawiono to na rysunku 4.6.
1. Utworzony zostaje
Rysunek 4.6
3. Adres łańcucha zostaje zapisany do wskaźn ik a - - - - - -- - -- - - - - . .\
w sk a źnik
o nazwie proverb
proverb 1000
Adres: 1000
2. Zostaje utworzony s t a ł y łańcuch, z a kończony znakiem \0
~ Szczęśliwe gwiazdy ze wskaźnikami Aby zobaczyć, j ak działają wskaźniki, przepiszemy przykład ze szczęś li wy m i gw iazdami, w którym używali śmy tablic, korzy stając tym razem ze wskaźników: II Cw4_ 06.cpp Il lnicj alizowanie
wskaźników
za pomocą
łańcu ch ó w
#i nc lude usi ng std: .c tn : usi ng std: :cout: usi ng std: :end l : int main( ) {
Visual C++ 2005. Od podstaw char* pstr5 - "Boris Karl off": char* pst r6 = "Ol iver Hardy"; char* pst r - "TwoJa s zc zęś l iwa gwiazda t o int dice = O: cout ci n
« « « »
end l " Wybi erz s z c zęś l iwą gwi a zdę ! " " Poda j cy frę od 1 do 6: dice :
cout « endl; switch(dice) {
case 1: cout « pstr break:
«
pstr 1;
case 2: cout « pstr break ;
«
pstr2 :
case 3; cout « pst r break;
«
pstr3:
case 4: cout « pst r break;
«
pst r4:
case 5: cout « pstr break:
«
pst r5;
case 6; cout « pstr break:
«
pstr6;
default : cout
«
"Nie masz swojej
s zc zęś liw ej
gwiazdy," ;
cout « endl : ret urn O;
Jak to działa przykładu Cw4~04,cpp została zastąpiona sześcioma wskaźnikami od pst r l do pstr6. Każdy z nich został zainicjalizowany jakimś imieniem i nazwiskiem. Zadeklarowany został również jeszcze jeden dodatkowy wskaźnik pst roZostał on zainicjalizowany zda-
Tablica z
na początku normalnego wiersza danych wyjściowych. Jako że jest wybrać odpowiednią wiadomość wyjściową za pomocą swi t ch niż za pomocą instrukcji i f, z której skorzystaliśmy w pierwszej wersji przykładu . Wszystkimi nieprawidłowymi danymi, które zostały wprowadzone do programu, zajmuje się opcja instrukcji switch defaul t . niem, które chcemy
wysłać
dysponujemy oddzielnymi
wskaźnikami, łatwiej
Wysłanie na wyjście łańcucha wskazywanego przez wskaźnik nie mogłoby już być łatwiejsze. Jak widać , wystarczy tylko podać nazwę wskaźnika . W tej chwili może zrodzić się pytanie, dlaczego w przykładzie Cw4_05.cpp, kiedy napisaliśmy nazwę wskaźnika w instrukcji wyjściowej, wyświetlony został adres, na który wskazywał ten wskaźnik . Dlaczego tutaj jest ina-
Rozdział4 .•
Tablice, łańcuchy znaków i wskaźniki
211
w sposobie traktowania wskaźników do typu cha r przez instrukcję tego typu są przez nią traktowane w specjalny sposób - jako łańcuchy (które są tablicami znaków), a więc instrukcja wysyła na wyjście sam łańcuch, a nie jego adres. czej?
Odpowiedź leży
wyjśc iową. Wskaźn iki
Zastosowanie wskaźników w powyższym przykładzie wyeliminowało problem z marno waniem pamięci, który wystąpił przy użyciu tablic. Jednak program wydaje s ię teraz tro chę rozwlekły - musi być na to jakiś lepszy sposób, i rzeczywiście jest - użycie tablicy wskaźników.
liI!lml!:.mI Tablice wskaźników W tablicy wskaźników do typu char każdy element może wskazywać na niezależny łańcuch znaków, a długość każdego z tych łańcuchów może być inna. Tablicę wskaźników deklaruje się w podobny sposób jak zwykłą tablicę. Przejdźmy teraz do przepisania poprzedniego przy kładu przy użyciu tablicy wskaźników: II Cw4_07.cpp
/r Inicjali zowanie
wskaźników łańcuchami
znaków.
#incl ude
using std : .ci n:
using std : :cout;
usi ng st d: :endl ;
i nt mai nO
{
char* ostr l l
"Robert Redford". II Inicj alizowanie tablicy "Hopal ong Cassi dy". "La ssi e" . "Sl i m Pi ckens ". "Bo ri s Karl off ". "Ol i ver Hardy " };
char* pst art = "Twoj a s z cz ęś liwa gwiaz da t o
int dic e cout « « « ci n »
~
wskaźn ików.
O:
end l
" W ybi er z s wo ją szc zęś l i wą " Podaj cy fr ę od 1 do 6: dice:
gw ia zdę l"
n •
cout « endl : if (di ce >= 1 && di ce <= 6) cout « pstart « pstr [dice
1J :
else cout «" Przykro mi . ale nie masz swoje j
II Sprawdzanie poprawności II wprowadzanych danych. II Wyświetl dane gw iazdy.
s zc zęś l iwej
gwi azdy. ": II Wprowadzono II nieprawidłowe dane.
cout « endl ;
return O:
212
VislJal C++ 2005. Od podstaw
Jak lo działa W tym przypadku stosujemy najbardziej efektywne podejśc ie . Mamy j ednowym iarową tabli cę do typu char za d ek l arow aną w taki sposób, że kompilator sam ustala j ej rozmiar na podstawie liczby łań cu ch ów inicjalizuj ących . Wykorzystanie pamięci w tym przypadku pokaz ano na rysunku 4.7.
wsk aźników
pstr[o) ~~* pstr[1] pstr[2]
pstr[3)
pstr[4)
pstr[S)
Tablica ws kaźni ków o rozmiarze 24 bajtów
Całkowity obszar
zajmowanej pamięci to 103 bajty
Rysunek 4.7 W por ównaniu z normaln ą tabl i c ą tablica wskaźn ikó w zużywa mni ej p am ię ci . W zwykł ej tablicy wszystkie wiersze mus i ałyby być takiej samej długości jak naj dłuższy łańcuch . Sześć wiers zy zaj mującyc h po 17 bajtów zaj muje ich w sumie 102. Dzięk i u życiu tablicy wskaź ników zaoszczędziliśmy cały l bajt. Co po szło nie tak? Odpowiedź jest prosta - przy niewiei kiej liczbi e w z gl ędnie krótk ich łań cuchów znaków roz miar dod atkowej tablicy ws kaźn i ków odgrywa du że znaczenie. O sz cz ędn oś ci zaczęłyby s i ę , gdyb yś my u żyw al i więcej dłużs zych łań cu chów i mieli bard ziej zróżn ico waną długoś ć zmiennych. Oszczędno ść p am ięci
to nie jedyna korzyść ze stosowania wskaźników. W wielu przypadkach czas. P omy ślmy, co by był o , gd ybyśmy chci eli prze sunąć łań cu ch "Ol i ver Hardy" na pierwszą pozycj ę , a łańcuch "Robert Redfo r d" na os tatn ią. M aj ąc tabli cę wskaźników, wystarczy tylko zm ienić miejsca wy stęp owani a wskaźników - łańcuchy pozo s taj ą tam, gdzie były. W przypadku zwykł ej tablicy, której użyli śmy w przykładzie Cw4_04.cpp, mu sielib yśmy dużo kopiow ać - łańcuch "Robert Redfo rd" trzeba by przenieś ć tymcz asowo w j aki eś inne miejsce, następnie skop iować łańcuch "Ol i ver Ha rdy" na jego miejsce i na zakoń czenie przeni eś ć łańcuch "Robert Redford" na os ta tn ią pozycję. Wykon anie tych wszystkich operacj i zaj muje znacznie więcej czasu. oszczędzamy również
Ze względu na fakt, że nasza tablica ma nazwę pst r , nazwa zmiennej prze chowując ej p oczą tek naszego komunikatu musi być inna - pst ar t. Łańcuch , który ma zo stać wy świ etl on y , wybieramy za pom oc ą bardzo prostej instrukcji warunkowej i f, podobnej do tej, której u żyli.
Rozdział4.•
Tablice, łańcuchy znaków i wskoaźniki
213
śmy w pierwszej wersji programu . Jeżeli użytkownik podał właściwą cyfrę, wyświetlana jest odpowiednia gwiazda, zaś w przeciwnym przypadku - odpowiedni komunikat.
Program ten ma tylko jedną wadę - kod zakłada, że istnieje sześć opcji, mimo że kompilator przydziela miejsce dla tablicy wskaźników na podstawie liczby podanych łańcuchów inicjali zujących . W związku z tym, dodając łańcuch do listy, musimy wprowadzić modyfikacje do odpowiednich części programu. Dobrze by było, gdybyśmy mogli dodawać łańcuchy, a pro gram automatycznie przystosowywałby się do ich liczby.
Operator sizeol Tutaj z pomocą może nam przyjść nowy operator - si zeof . Operator ten sprawdza liczbę bajtów zajmowanych przez jego operand i zwraca tę wartość jako w artość c ałkowitą typu si ze_t . Jak pamiętamy z wcześni ejszych wyjaśni eń, si ze_t jest typem zdefiniowanym w stan dardowej bibliotece i zazwyczaj odpowiada typowi unsi gned i nt. Spójrzmy na poniższą instrukcję, która odnosi się do zmiennej dice z poprzedniego cout
«
przykładu :
sizeof dice;
Wartością wyrażenia sizeof di ce jest 4, ponieważ zmienna di ce została zadeklarowana jako typ i nt , a zatem zajmuje cztery bajty pamięci. Dlatego też powyższe wyrażenie zwróci wartość 4.
Operator sizeof może być zastosowany do elementu w tablicy lub do całej tablicy. Jeżeli zosta nie zastosowany do tablicy za pomocą jej nazwy , to operator ten zwróci liczbę bajtów zaj mowanych przez wszystkie jej elementy. Gdy natomiast zastosujemy go do pojedynczego elementu za pomocą odpowiedniego indeksu lub indeksów, zwrócona zostanie liczba bajtów zajmowanych przez ten element. W związku z tym liczbę elementów w tablicy pst r mogliśmy wyświetlić za pomocą następującej instrukcji: cout
«
(sizeof pstr )/(sizeof pstr[O]);
(s izeof pst r) / (si zeof pst r[ O] ) dzieli liczbę bajtów zajmowanych przez całą przez liczbę bajtów zajmowanych przez pierwszy element tablicy. Jako że każdy ele ment tablicy zajmuje tyle samo pamięci, otrzymany wynik będzie liczbą elementów znajdują cych się w tablicy . Wyrażenie
tablicę
pstr j est tablicą wskaźników - użycie operatora siz eot dla tej tablicy lub posz czególnych jej elementów nie umożliwi nam uzyskania informacji na temat pa mięci zajmowanej przez łańcuchy znaków.
Pamiętajmy, że
Operator sizeof może być zastosowany do nazwy typu, a nie zmiennej, w którym to przy padku zwrócona wartość jest liczbą bajtów zajmowanych przez zmienną tego typu . Nazwa typu powinna się wówczas znajdować pomiędzy nawiasami okrągłymi . Na przykład po wyko naniu poniższej instrukcji size t size
=
sizeof( long) ;
214
VisIlai C++ 2005. Od pOlIslaw zmienna l on9_si ze będzie miała wartość 4. Zmienna size została zadeklarowana jako typ size_t, aby pasowała do typu wartości zwróconej przez operator si zeof. Użyc i e innego typu całko witego dla zmiennej si ze może spowodować wyge nerowa nie przez kompilator o strzeżenia.
RE!jg Używanie operatora sizeol Kod z ostatn iego listingu możem y ulepszyć , stosując operator si zeof w taki sposób, że s ię on automatycznie przystosowyw ał do dowolnej liczby łańcu ch ó w : II Cw4_08.cpp
II Elasty czne zarządza n ie
tablicą
będzi e
za p om ocą operatora sizeof
#incl ude
usi ng std : :ci n:
usi ng std : :cout ;
usi ng std : :endl :
int ma i n() (
char* pst r eJ
"Robert Redford" , "Hopal ong Cassi dy". "Lassl e". "Slim Pi ckens". "Bori s Kar loff ". "Dliver Hardy "
II Jnicjalizowanie tablicy
wskaźnikó w.
): char* pstart = "Twoj a s z c z ęś li wa gwiazda t o i nt count = (s i zeof pstr-i/ rsrzeot pstr [OJ) : II Liczba elementów tablicy . i
nt cice
cout « « « cln »
=
O:
endl
"W ybierz s wo j ą szczęś l i w ą gwi a zdęl"
" Podaj cy f rę od l do " « count « ":
dlce :
cout « endl : if(dice >= l & &dice
<~
count )
cout « pst art « pstr[d ice - 1J : el se cout « "Przykro mi . ale ni e masz swojej
II Sprawdź poprawn ość II wprowadzanych dany ch. II Wyś wietl dane gw iazdy . szcz ęś li wej
gwi azdy . ": II Wp rowadzono II n ieprawidło we dane,
cout « end l :
return O:
Jak lo działa Jak wid ać, zmiany, które trzeba było wprowadzić, są bardzo proste . Obliczamy tylko liczb ę elementów w tablicy wskaźn ików pstr i zapisujemy ją do zmiennej count. Następnie ws zędzie tam, gdz ie znaj d ują s ię odniesien ia do ogólnej liczby elementów w tablicy wynoszącej 6, używamy zmiennej count. Możemy teraz dodać kilka nazwisk do listy szczęśliwych gwiazd, a program sam s ię dostosuj e do nowej liczby łańcuchów.
Rozdzial4. • Tablice, łańcuchy znaków i wskaźniki
215
Stale wskaźniki oraz wskaźniki do stałych
Zarówno tablica pstr w ostatnim przykładzie, jak i łańcuchy, do których utworzone są wskaź niki, oraz zmienna count nie zostały przystosowane do zmieniania wartości w programie. Dobrym pomysłem byłoby wyeliminowanie możliwości przypadkowego ich zmodyfikowania w programie. Zmienną count możemy w bardzo łatwy sposób zabezpieczyć przed przypadkową modyfikacją, stosując następującą instrukcję :
const int count = (sl zeof pst r )/ (sizeof pstr [O] ) : Tablica
wskaźników
natomiast wymaga poświęcenia więcej uwagi . Tablicę
tę
zadeklarowali
śmy następująco:
cha r* ostr l l = { "Robert Redford", "Hopa long Cass idy", "Lassi e" . "Sl im Pi ckens" . "Bo ris Ka rloff" . "Oli ver Hardy"
II Inicjalizowanie tablicy
ws kaźn ików.
}; Każdy wskaźnik
w tej tablicy jest inicjalizowany adresem literału łańcuchowego "Robert Redfor d", "Hopal ong Cass i dy" itd. Typem literału łańcuchowego jest tablica const char, a więc adres tablicy typu const przechowujemy we wskaźniku typu niebędącego const. Powodem, dla którego kompilator zezwala nam na użycie literału łańcuchowego do inicjalizacji elementu tablicy typu char*, j est wsteczna kompatybilność z istniejącym kodem. Jeżeli
do tablicy znaków wprowadzimy
następujące zmiany:
*pstr [O] = "Stan Laurel" ; to programu nie
będzie można skompilować .
Gdybyśmy chcieli wartość jednego ze wskaźników w tablicy znak za pomocą następującej instrukcji:
*pstr[ O] to program
=
ustawić,
wskazywał jakiś
' X':
się
skompiluje, ale ulegnie
załamaniu
w momencie wykonywania tej instrukcji.
Oczywiście nie chcemy, aby nasz program ulegał awariom podczas zapob iec. O wiele lepiej jest zapisać tę deklarację następująco:
const cha r* pstr e]
};
aby
=
{
"Robert Redford" . II Tablica wskaźników "Hopal ong Cassidy" . li do stałych. "Lassie" . "Slim Pickens" . "Boris Ka rloff". "Ol iver Ha rdy"
działania
i możemy temu
16
Visual C++ 2005. Od podstaw W tym przypadku pojawia si ę pewna dwuznaczność, j eżeli chodzi. o stałość łańcuchów , na które wskazują wskaźniki będące elementami tablicy . Jeżeli spróbujemy je teraz zmienić, to kompilator w trakcie kompilacji zgłosi błąd. Nadal jednak moglibyśmy napisać poniższą instrukcję:
pstr [O]
~
pst r[ l ] :
Wszyscy szczęściarze, którym należałby się Robert Redford, dostaliby w zamian Hopalong Cassidy, ponieważ oba wskaźniki wskazują na to samo nazwisko. Zauważmy, że nie powo duje to zmiany wartości obiektów, na które wskazuje wskaźnik z tablicy - zmieniana jest wartość wskaźnika przechowywanego w pstr[Ol Powinniśmy zatem zapobiegać również zmianom tego typu, gdyż niektórzy mogliby odnieść wrażenie, że stary dobry Hoppy jest mniej seksowny niż Redford . Możemy tego dokonać za pomocą poniższej instrukcji: II Tablica stałych
wskaźników
do stałych.
const char* const pstr e] = { "Robert Redford",
"Hopalong Cass idy".
"Lassi e".
"Slun Pickens",
"Bo ri s Karl off ".
"Oliver Hardy"
}: Podsumowując,
sytuacje.
w odniesieniu do typu const, wskaźników oraz obiektów można wyróżnić trzy z:
Możemy się spotkać
•
wskaźnikiem
•
stałym wskaźnikiem
do obiektu ,
•
stałym wskaźnikiem
do
do
stałego
obiektu,
stałego
obiektu.
W pierwszej sytuacji nie można zmieniać obiektu, na który wskazuje ustawić wskaźnik, aby wskazywał na coś innego:
const char* pst ri ng W drugiej nie można on wskazuje , można :
=
"Jaki ~
zmienić
wskaźnik,
ale
możemy
te kst , ";
adresu przechowywanego przez
wskaźnik,
ale obiekt, na który
char* const pst ring = Jaki ~ takst" ; W trzeciej sytuacji zarówno obiekt, jak i wskaźnik na niego jako stałe, a zatem żadnego z nich nie można zmienić :
con st char* const pstring Oczywiście
~ "J a k i ~
wskazujący zostały
zdefiniowane
tekst. ":
zasady te odnoszą się do
wskaźników
do wszystkich typów.
char został tutaj użyty wyłącznie dla potrzeb przykładu.
Wskaźnik
do typu
Rozdział 4.•
Wskaźniki
Tablice, łańcuChy znaków i wskaźniki
217
i tablice
Nazwy tab lic w niektóry ch warunkach mo gą zachowywać się j ak wsk aźn iki . W wię kszośc i sytuacj i użycie samej nazwy tablicy jednowymiarowej powoduje jej au to matyczną k onw ersj ę na wskaźnik do pierwszego elementu tablic y. Należy pamiętać , że sytuacja taka nie ma miej sca, gdy nazwa tabli cy zostanie użyta jako operand operatora s i zeof. Mając p oni ż s z e
deklaracje:
double* pdata ; double dat a[5]; mo żem y napi sać n astępuj ąc e
przyp isanie :
pdata = data; Il lnicj a/izowanie wskaźnika adresem tablicy. Powyżs za
instrukcja przypisuje adres pierwszego elementu tablicy data w skaźnikow i pdat a. nazwy tablicy odnosi s ię do j ej adresu. Jeż eli użyj em y nazwy tablicy łączni e z jak imś indeksem, to będzie się ona odnos iła do zawartoś c i elementu odpowiadając ego temu indeksowi, a więc j eż eli chcemy adres tego elementu zap is ać do wsk a źnik a , musimy u żyć operatora ad resowaru a: Użyc i e
pdata = &dat a[l ]; W tym
p rzykładzie wskaźnik
pdat azawiera adres drugiego elementu tabli cy.
Arytmetyka wskaźników Z udziałem wskaźników można przeprowadzać obliczenia arytm etyczne. Zakres działań aryt metycznych na w sk aźnikach jest ograniczony do dodawania i odej mowania, ale możemy także wy ko n ywać operacj e porównywania wartośc i wskaźników w ce lu otrzyma nia wyniku w postaci wartoś ci logicznej. Arytmetyka z u życiem w skaźników niejawni e zakłada, że wskaź nik wskazuje tablic ę oraz że operacja arytme tyczn a doty czy adresu zawartego we wskaźniku. Aby w sk aźnik owi pdata przypisać adres trzecie go elementu tabli cy dat a, możem y po służyć s i ę n astępując ą i nstru kcj ą:
pdata
=
&da t a[2] :
W tym przypadku wyrażenie pdata + l odn o siłoby s i ę do adresu elementu data [3] , czyli czwartego elementu tablicy dat a. Tak więc aby wskaźnik w skazywał ten element, możemy za stosować następującą instrukcję:
pdat a
+=
l;
II Zwiększ
wskaźnik pdat a
do nas tępn ego elementu.
P owy żs za instrukcja zw i ę k s za adres zawarty we w ska źniku pdata o liczbę bajtów zajmo wanych przez j eden element tablicy data . Ogólnie wyrażeni e pdat a + n, gdzie za n możemy p odst awi ć dowolne wyrażenie zwracające liczbę całk owit ą, dodaje do adresu zawartego we ws kaźn i k u pdata warto ść wyrażenia n*s i zeof(doubl e J, p oni ewa ż został on zadeklarowany jako ws kaźn i k do typu doubl e. Z ostało to przed stawione na rysunku 4.8.
218
Vi sila IC++ 2005. 011 pOlIsław
Rysunek 4.8
double data[S];
data[l]
data[O]
data[2]
Adres
j
pdata-l
pdata+2
pdata = &data[2);
Inaczej mówiąc, zwiększanie lub zmniejszanie wskaźnika działa w kategoriach typu wskazy wanego obiektu. Zwiększenie wskaźnika typu l ong o cztery zmienia jego zaw artość do n astęp nego adresu typu l ong, a więc zwiększa adres o cztery. Również zwiększeni e w skaźnika typu short o jeden spowoduje zwiększenie adresu o dwa. Częściej spotykaną notacj ą zw i ększan ia w skaźn ika jest użycie operatora inkrementacji. Na przykład: pdat a++: II Zwiększ wskaźnik pdata do wartości nas tępnego element u.
Zapis ten jest równoznaczny (i części ej spotykany) z zapisem += . Mimo tego użyłem formy +=, aby było jasne, że - c h o ć wartość zostanie zwiększona o jeden - zw iększe n i e zazwyczaj nastąpi o li czbę w iększą niż jeden, z wyjątkiem wskaźnika typu char .
Adres powsta ły w wyniku działań arytmety cznych na wskaźnikach może być wartością z zakresu od wartości adresu p ierwszego elementu tablicy do war tości adresu leżącego p oza ostatnim jej elementem. Poza tymi granicami zachowanie ws kaźn ika nie zos tało zdefiniowane. M ożna oc zywiśc i e wyłu s kać w skaźnik,
na którym wyko na li ś my działania arytmetyczne (ina czej nie byłoby sensu ich wy kon ywać) . Zakładając na przykład , że wskaźnik pdata c ały czas wskazuje element dat a[2], poniższa instruk cja: *(pdata
+
l ) = *(pdata
+
2) :
jest równoznaczna z nast ępującą: data[ 3]
=
data [4]:
Jeżeli chcemy wyłuskać wskaźnik po uprzednim zwiększeniu adresu , który zawiera, to ko nieczne s ą nawiasy, pon ieważ operator bezpośredniości ma większy priorytet niż operatory arytme tyczne + i -. Jeśli zamiast *( pdat a + 1) napiszemy *pdata + l , to jeden zostanie do dane do wartości przechowywanej w adresie zawartym w pdata , co jest równoznaczne z obli czeniem wartoś ci wyrażenia dat a[ 2] + 1. Jako że nie jest to l va l ue, kompilator zgłosi komu nikat o błędz ie.
Nazwy tablicy m o żn a używać w taki sposób, jakby była wskaźnikiem służącym do odwo ływani a s i ę do elementów tablicy . Jeśli mamy taką samą jednowymiarową tablicę jak po przednio, zade kl arowan ą w następujący sposób :
Rozdział 4.
• Tablice. łańcuch, znaków i wskaźniki
219
l ong dat a[5] : to do jej elementów można odwoływać się za p om oc ą notacji wskaźn ikowej, np. do eleme ntu data[3] za pomocą *(data + 3). Notację tę można stosować dla wszystkich elementów tablicy, a więc zamiast dat a[ D], data[l ], dat a[ 2] można n ap i s ać *dat a, * (dat a + 1) , * (dat a+2) itd.
R!lmI!:.mI Nazwy tablic iako wskaźniki
Zaprezentowany powyżej sposób adresowania tab licowego p rzećwiczymy na programie obliczającym liczby pierwsze (liczba pierwsza dzieli się bez reszty tylko przez s amą siebie i przez jeden). II Cw4_09.cpp
II Obliczanie liczb pierwszych.
#incl ude
#i ncl ude
using std: :cout :
usi ng std :: endl :
usi ng st d: :set w:
i nt mai n( ) {
const int MAX = 100: II Liczba wymaganych liczb pierwszych.
l ong pri mes[MAX ] ~ { 2.3.5 }: II Definicja trzech pierws zych liczb p ierwszych.
l ong t ri al ~ 5; II Kandydatka na licz bę p ierwszą.
II Liczba znal ezionych liczb pierwszych.
i nt count = 3: int found = O: II Wskaźnik znal ezienia liczby pierwszej .
do (
t rial += 2: found ~ O: for (i nt i = O: i < count: i ++)
II Następna wartość do sprawdzenia.
II Ustawianie wskaźnika znalezienia.
II Spróbuj podzielić przez istniejąc e już liczby pie rwsze.
{
found = (tri al % *(pr imes + i )) i f (found) break:
nie ma reszty z dzielenia. II Jeżeli nie ma reszty z dzielenia, II to liczba nie j est liczbą pierwszą .
~= O : IITrue,jeżeli
} if (found == O)
II Mamy jedną ...
*(primes + count++) }whi le (count < MAX) : II
Wyświetlanie
for (i nt i
=
=
t ria l : II ...a więc zapisuj emy ją do tablicy liczb p ierwszych.
liczb w p ię ciu kolum nach.
O: i < MAX: i++)
{ i f ( i % 5 == O) cout « endl : cout « set w(lO) «
II Znak nowego wiersza po pierwszym i co piątym wierszu .
*(pr imes + i ) :
}
cout « end l :
retu rn O:
Po skompilowaniu i uru chomi en iu tego programu otrzymamy
nas tępuj ący
wynik:
220
VisualC++ 2005. Od podstaw 2 13 31 53 73 la l 127 151 179 199 233 263 283 317 353 383 419 443 467 503
Jak to działa Na początku znajdują się te same dyrektywy co zawsze, a więc te dołączające plik nagłówkowy , ponieważ będziemy używać mani pulatora strumienia w celu ustawienia szerokości pola dla wyświetlanych liczb. W stałej MAX określiliśmy liczbę liczb pierwszych, które mają być obliczone przez nasz program. Trzy pierw sze liczby pierwsze zostały już podane w tablicy pr i mes . Cała praca programu wykon ywana jest w dwóch pętlach: zewnętrznej pętli do-whi l e, która podaje kolejne wartości do sprawdzenia oraz dodaje znal ezione liczby pierw sze do tablicy pr imes . W ewnętrzna pętla for sprawdza, czy dana liczba jest liczbą pierwszą. Algorytm zawarty w pętli fo r jest bardzo prosty. Opiera się on na twierdzeniu, że jeżeli liczba nie jest liczbą pierwszą, to musi dać s i ę podzieli ć przez jedną z pierwszych znalezi onych do tej pory mniejszych od niej, p onieważ wszystkie liczby są albo pierwsze albo składają się z liczb pierwszych. W rzeczywistości wystarczyłoby dzielenie przez liczbę mniejszą lub równą pier wiastkowi kwadratowemu sprawdzanej liczby. A więc przykład ten można zmi enić tak, aby był jeszcz e bardziej efekty wny .
found
=
(t ri a1 %*(primes + i) )
~=
O;
II True, jeżeli nie ma reszty.
Powyższa instrukcja ustawia zmienną found na l, jeżeli nie ma reszty z dzielenia warto śc i zmiennej t r i al przez bieżącą liczb ę p ierwszą *(p r i mes + i ) (pamiętamy, że zapis ten jest równoznaczny z pr i mes[ i J), lub Ow przeciwnym przypadku. Instrukcja warunkowa i f po woduje zakończenie działania pętli, jeżeli zmienna found ma wartość l , pon ieważ kandy datka w zmiennej t r i al w tym przypadku nie może być liczbą pierwszą.
Po
zakończeniu działania pętli
d ować,
for (bez względu na pow ód) mu simy w jakiś spo sób zdecy czy liczba w zmiennej t r i a l była li czbą pierwszą, czy nie . Wskazuje na to zmienna
w skaźnikowa
found.
*(primes + count ++) = t r ial :
11 ...a
więc
zapisujemy j q do tablicy liczb pi erwszych.
Rozdział 4.•
Talllice, łańcuchy znaków i wskaźniki
221
J eż eli
zmienna tr i al zawie ra l i c zb ę pierw s zą, to p o wy żs z a instruk cja zapisze ją do tablic y pri mes[ count J, a n astępni e za p omo c ą przyro stkowego operatora inkrementacji zw iększy zmienną count . Kied y zostanie już znalezione MAX liczb pierwszych , s ą one wysyłane na wyj ś cie i umiesz czane w polach o szerokoś ci dziesięciu znaków, po pię ć w każdym wierszu. Za to odpowie dzialna j est poniższa instrukcja : i f ( i % 5 ~ ~ O) II Znak nowego wiersza po p ierwszym i co piątym wiersz u. « endl;
cout
Powy ższy
kod spowoduje
wy słan ie
znaku nowego wiersza, kiedy i ma
wart o ść
O, 5, l a itd.
~ Liczenie znaków leszcze raz Aby zob aczyć , jak działa notacja w skaźnikowa w operacjach na łańcuch ach znaków, zmody fikujemy w cze śniejszy progr am, który liczył znaki w ł ańcuchach : II Cw4_ JO. cpp
II Liczenie znaków w
łań c uch u
za pomocą
wskaźn ika .
#include
usi ng std : .ci n:
using st d: :cout :
usi ng st d: :endl:
int mainO {
const int MAX ~ SD; char buffer[MAXJ : char* pbuffer ~ buffer :
II Maksymalny rozmiar tablicy .
II Buf or danych wejś ciowych.
II Wskaźn ik tablicy bufo rowej.
cout « endl « "Podaj « M AX «
II Popro ś o wprowadzenie danych.
«
ł a n c u c h składa jący s i ę
"
z mn ie j nit "
znaków :"
endl :
cinget li ne( buff er. MAX, ' \ n' ) :
II Wczytuj łań c uch
wh il e(*pbuff er ) pbuff er++ ;
II Kontynuuj
aż
aż
do napo tkania znak u In.
do napo tkania zna ku lO.
cout « endl
« « «
" Ł a ń c uc h
ma " cout endl :
retur n O:
Poni żej
"\ "
znaj duje s i ę
v'" « buff er
«
pbuffer - buffer
«
"
znaków.
przykładowy wynik dzi ał ania
tego programu :
Podaj ł an c u ch s k ł a d aj ą cy się z mniej nit SDznaków:
Nawet na jlep sza kobiet a ma jeszcze diabelskie zebro w sobie.
Ła n c u ch "Nawet na jl epsza kobiet a ma jeszcze dia belskie że b r o w sobie." ma 60 znaków ,
222
Visual C++ 2005. Od podstaw
Jak to działa P owyższy
program operuje w skaźnikiem pbuffer zamiast n azwą tabli cy buffer. Nie ma potrzeby stosowania licznika count dla zmiennej, gdyż wskaźnik zw ię ksza ny jest w p ętli whi le aż do napotkania znaku \ 0. Kiedy do tego dojd zie, wskaźnik pbuffer będzi e zaw ierał adres tej pozycji w ł ań cuchu . A zatem liczba znaków we wprowadzonym łańcu chu stanowi różnicę pomiędzy adresem przechowywanym we w skaźniku pbuffe r a adresem początku tablicy okre ś lonym przez buffer. M ogli śmy również zwi ększyć w skaźnik
whi le (*pbuffer++ ) ;
II Konty nuuj
aż
w
pętli, zapi sując ją n astępująco:
do napo tkania znaku \0.
Teraz pętla nie zawiera ż a d nych instrukcji, tylko warunek sprawdzający. Zapis ten dzi ałałby prawi e bez zarzutów - wskaźnik b yłby zwi ększany po napotkaniu znaku \ 0, dzi ęk i czemu adres byłby o jeden większy ni ż ostatnia pozycja w łańcu chu . W zw iąz ku z tym licznik liczby znaków mu s ieliby śmy przedstawi ć jako pbuffer - buff er - 1. Zauw ażmy , że
nie m ożemy tutaj użyć nazwy tablicy w taki sam sposób jak wskaźn i ka. Wyra p onieważ nie możn a modyfikować adresu wartoś ci , która jest reprezent owana za pomocą nazwy tablicy. Mimo że nazwy tablicy mo żemy używać tak, jakby była w skaźnikiem , to jednak nim nie jes t, pon i eważ adres przez nią reprezentowany jest s tały . żenie
buff er++ jest ni eprawidłowe ,
Używanie wskaźników
z1ablicami wielowymiarowymi
Używ an i e wskaźnika
do przechowywania adresu tablicy jednowymiarowej jest względnie prost e, ale w przypadku tablic wielowymiarowych sprawy si ę tro chę komplikują. J eżeli nie planuj esz s ię tym zajm ować , to m o że sz p ominąć tę czę ś ć , poni ew aż m oże się ona wydać tro ch ę niejasna. Ale jeżeli masz trochę do świ adczenia w języku C, to warto przeczytać także tę część. Planuj ąc s ię
korzystanie ze wskaźników z tablicami wielowymiarowymi, musimy pamiętać , aby nie pogubić. Zilustruję to na przykładzie tablicy beans zadeklarowanej w następujący sposób;
double beans[3][4] : Aby
zadeklaro wać wskaźn ik
pbeans i przypisać mu wartość ,
możemy posłużyć s i ę następującą
instrukcją:
double* pbeans ; pbeans ~ &beans[O ] [O] : W powyższym kodzie ustawiamy wskaźnik na adres pierwszego elementu tablicy, który jest typu doubl e. Można również ustawi ć wskaźnik na adre s pierw szego wiersza w tablicy za pomoc ą następuj ąc ej instrukcji : pbeans
~
bean s[O] ;
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
223
Zapis ten jest równoznaczny z użyciem nazwy tablicy jednowymiarowej , która została zastą piona adresem. Użyliśmy go już wcześniej . Ponieważ jednak beansjest tablicą dwuwymiarową, nie można ustawić wskaźnika na adres za pomocą następującej instrukcji : pbeans = beans : II Ten zapis sp owoduje błąd! !
Problem leży w typie. Typ zdefiniowanego wskaźnika to doubl e*, ale tablica jest typu do uble[ 3][4J. Wskaźnik przechowujący adres tej tablicy musi być typu doub le [4J*. W języku C++ rozmiary tablicy kojarzone są z jej typem, a więc powyższa instrukcja jest prawidłowa tylko wtedy, kiedy wskaźnik zostanie zadeklarowany z wymaganym wymiarem. Robi się to za pomocą trochę bardziej skomplikowanej niż dotychczas przez nas stosowanej notacji : double (*pbeans)[4]:
Nawiasy w tym kodzie są niezbędne. Gdybyśmy ich nie zastosowali, zadekJarowalibyśmy tablicę wskaźników. Teraz poprzedni przykład jest prawidłowy, ale wskaźnik ten może być używany wyłącznie do przechowywania adresów z pokazanego wymiaru tablicy .
Notacia wskaźnikowa ztablicami wielowymiarowymi W celu odniesienia się do elementów tablicy można zastosować notację wskaźnikową. Do każ dego elementu zadeklarowanej przez nas wcześniej tablicy beans zawierąjącej trzy wiersze po cztery elementy możemy odnieść się na dwa sposoby :
W
•
używając
nazwy tablicy z dwoma indeksami,
•
używając
nazwy tablicy w notacji
związku
z tym obie
beans[i ][j ] *(*(beans + i )
+
poniższe
wskaźnikowej.
instrukcje
są równoznaczne:
j)
Spójrzmy, jak to działa. W pierwszym wierszu użyto normalnego indeksowania w celu odnie sienia się do elementu z przesunięciem j w wierszu i tablicy . Znaczenie drugiego wiersza można odgadnąć , przechodząc od wewnątrz do zewnątrz . beans odnosi s i ę do adresu pierwszego wiersza tablicy, a więc beans + i odnosi się do wiersza i ta blicy. Wyrażenie *( beans + i ) stanowi adres pierwszego elementu wiersza i, w związku z czym *(beans + i ) + j jest adresem elementu w wierszu i o przesunięciu j. Tak więc całe wyrażenie odnosi się do wartości tego elementu . Jeśli
chcemy napisać naprawdę trudny do odczytania kod - choć nie jest to zalecane dwie instrukcje, w których pomieszane zo stały notacje tablicowa i wskaźnikowa, są również poprawnymi odniesieniami do tego samego elementu tablicy: poniższe
*(beans[i] + j) (*(beans + i »[j]
Jest jeszcze jeden powód używania wskaźników, tak naprawdę najważniejszy ze wszyst kich - możliwość dynamicznego przydzielania pamięci zmiennym . Tym się teraz będziemy zajmować .
224
Visual C++ 2005. Od podstaw
Dynamiczne przydzielanie pamięci
Praca z ustaloną liczbą zmiennych w programie może być bardzo ograniczona. Podczas wyko nywania programu często zachodzi potrzeba podjęcia decyzji, ile pamięci przydzielić do prze chowywania zmiennych różnego typu, w zależności od wprowadzanych danych. W przy padku jednych danych najlepiej byłoby użyć dużej tablicy całkowitej, natomiast w innym przypadku wymagana może być tablica liczb zmiennopozycyjnych. Jako że zmienne, dla któ rych pamięć przydzielana jest dynamicznie, nie mogą być zdefiniowane w czasie kompilacji, nie można im także nadać żadnych nazw w źródle programu . Podczas ich tworzenia identyfi kowane są one za pomocą adresu w pamięci, który przechowywany jest we wskaźniku . Dzięki potędze wskaźników oraz narzędziom dynamicznego zarządzania pamięcią w Visual C++ 2005 pisanie programów o takim stopniu elastyczności jest szybkie i łatwe .
Pamięć wolna, czyli sterta przypadków podczas wykonywania programów w komputerze znajdują się zasoby pamięci . Pamięć ta w C++ zwana jest stertą lub czasami pamięcią wolną. W pamięci tej można zarezerwować obszar dla nowej zmiennej danego typu za po mocą specjalnego operatora, który zwraca adres przydzielonego obszaru pamięci . Operator ten to new, a jego odpowiednikiem o odwrotnym działaniu jest operator del et e, który usuwa przydzieloną przez new pamięć.
W
większości
nieużywane
Pamięć wolną można przydzielić
zmiennym w jednej części programu, a następnie ją zwolnić i zwrócić na stertę po zakończeniu pracy z tymi zmiennymi. W ten sposób zwalniamy pamięć do ponownego użycia przez inne zmienne z dynamicznie przydzielaną pamięcią w dalszej części programu. Pamięci
zawsze, gdy potrzebujemy przydzielić pamięć elementom, które podczas działania programu. Przykładem takiej sytuacji może być przydzielenie pamięci zmiennej do przechowywania łańcucha wprowadzonego przez użyt kownika programu. Nie ma sposobu sprawdzenia rozmiaru tego łańcucha z wyprzedzeniem, a więc pamięć przydzielimy mu w czasie działania programu za pomocą operatora new. PÓŹ niej zobaczymy przykład dynamicznego przydzielania pamięci tablicom, w którym wymiary tablicy określane będą przez użytkownika w czasie działania programu. wolnej
używamy
mogą być określone wyłącznie
Technika ta daje bardzo duże możliwości. Pozwala na bardzo efektywne używanie pamięci i w wielu przypadkach programy napisane przy jej użyciu potrafią poradzić sobie z o wiele większymi problemami związanymi z wykorzystaniem znacznie większej ilości danych , niż byłoby to możliwe w innym przypadku.
Operatory new idelete Przypuśćmy , że
potrzebujemy miejsca w pamięci dla zmiennej typu doubl e. W takim przypadku typu doubl e, a następnie zażądać przydzielenia mu pamięci podczas działania programu . Aby tego dokonać, posłużymy się operatorem new,jak poniżej: możemy zdefiniować wskaźnik
Rozdzial4.• Tablice. lańcuchv znaków i wskaźniki double* pva l ue = NULL: pva l ue = new doubl e;
225
II Wskaźnik zainicj alizowany wartością zero wą. II Żądan ie pamięci dla zmiennej typu double.
Jest to dobry moment, aby przypomnieć, ze wszystkie wskaźniki muszą zostać zainicjalizowane. Dynamiczne używanie pamięci zazwyczaj pociąga za sobą pewną liczbę oczekujących na u życie w skaźników i wa żne jest, ż e b y nie zawierały one żadnych przypadkowych wartości. Powinniśmy zadbać o to, by ka żdy wskaźnik nieprzechowujący ża dnej prawidłowej wartości zaw i e rał wartoś ć zerową.
Operator new w drugim wierszu powy ższego kodu powinien zwrócić adres pamięci na stercie przydzielonej zmiennej typu double . Adres ten będzie przechowywany we wskaźniku pvalue. Następnie mo żemy odnie ść s i ę do tej zmiennej przy użyciu tego wskaźnika za pomocą opera tora pośredniości, jak ju ż widzieliśmy. Na przykład:
*pval ue = 9999 .0; O czywiście
przydzielenie pamięci mo że nie nastąpić ze względu na fakt, ze wolna pamięć wyczerpana lub pofragmentowana w wyniku wcześniejszego używania, co oznacza, ze nie ma wystarczającej liczby występujących obok siebie bajtów, aby pomieścić zmienn ą, dla której chcemy uzyska ć miejsce. Nie musimy s i ę jednak zbyt o to martwić. W standardz ie ANSI C++ operator newspowoduj e wyjątek, je żeli pamięć nie będzie mogła zostać przydzie lona z jakiegokolwiek powodu , co z kolei spowoduje zakoń c zenie programu. Wyjątki w C++ są mechanizmem sygnalizowania błędów. Więcej na ich temat będziemy mówili w rozdziale 6. została
za pomocą operatora new możemy r ówni eż zainicjalizować. Biorąc jak o typu do ubl e, której pamięć została przyd zielona za pomocą operatora new i której adres przechowywany jest we wskaźniku pva l ue, j ej wartość moglibyśmy podczas tworzenia ustawić na 999 . Oza pomocą następującej instruk cji :
Zmienną utworzoną
przykład zmienną
pva l ue
=
new double(999.0):
II Przydziel pam ięć zmiennej double i zainicj alizuj ją.
Kiedy nie potrzebujemy już zmiennej , której przyd zi elili śm y dynamicznie pamięć, zwolnić zajmowaną przez nią pamięć za pomo c ą operatora de l et e:
delete pval ue:
II Zwolnij pamięć
wskazywan ą
prz ez
mo żemy
wskaźnik p value.
Dzięki
temu nieużywana pamięć mo że być użyta przez inną zmienną. Jeże li nie użyj emy operatora del et e i do wskaźnika pval ue zapiszemy inny adres, to nie będzie mo żliwo ści zwol nienia tej pamięc i ani tez użycia przechowywanej w niej zmiennej, gdyż dostęp do jej adresu zostanie utracony. Sytuacja taka nazywa s ię wyciekiem pamięci, zwłaszcza jeżeli powtarza si ę kilkakrotnie.
Dynamiczne przydzielanie pamięci tablicom Dynamiczne przydzielanie pamięci tablicy jest bardzo pro ste. Je że li chcem y przyd zieli ć tablicy typu char, zakładając , ze pstr je st wskaźnikiem do char, mo żemy posłuży ć s i ę
pamięć
następującą instrukcją:
pst r
~
new char[20J:
II Przydziel pamięć
łańcuchowi składają cem u się z
20 znak ów.
226
VisIlai C++ 2005. Od pOlIslaw Aby usunąć tabli c ę utworzoną przed chwilą w obszarze p ami ę ci wolnej, musimy tora del ete . Instrukcja do tego służąca wygląd a następuj ąco : delete
[J
pstr :
II
Usun ięcie
tablicy wskazywanej przez
użyć
opera
wskaź n ik pstro
w powyższym kodzie zasto sowal iśmy nawiasy kwadratow e, aby zazna usuwamy tablicę. Usuwając tabl icę z wolnej pam i ęci , należy zawsze wpi sywać kwa dratowe nawiasy, w innym przypadku wyn ik operacji b ędzie bowiem nieprzewidywalny. Należy równ ie ż zauw ażyć , że nie określamy tu żadnych wymiarów, po prostu wpisujemy [ J. Warto
zauważyć , że
czyć , że
Oczywiśc ie wskaźnik pstr zawiera teraz adres obszaru pamięci, który mógł zo stać już przy dzielony do jakiegoś innego celu, a więc z pewnoś c ią nie powinniśmy go używać. Usuwaj ąc obiekt z pam ięci za pomocą operatora del ete w celu jej zwolnienia, wartość wskaźnika zaw sze powinno s ię ponownie ustawiać na zero :
pst r = O;
II Ustaw
wskaźnik
na ze ro.
RIlmI Używanie wolnei pamięci Sposób działania dynamicznego przydzielania pamię ci prześledzimy na zmodyfikowan ej wer sji programu obliczającego określoną liczbę liczb pierwszych. Tym razem do ich przecho wywania użyjemy obszarów wolnej pamięci . II Cw4_ l1.cpp
II Obliczanie liczb pierwszy ch przy
użyciu
dynamicznego przydzielania pamięci.
#inc1ude #inc1ude usi ng std.: Ci n; usi ng std: :cout: usi ng std::endl : usi ng std : :set w: i nt ma i n() {
10ng* pp r illle = O: 10ng t ri a1 ~ 5: i nt coun t ~ 3 ; i nt foun d ~ O; i nt ma x = O; cout
« «
II Wskaźnik tablicy prime. II Kandydatka na li czb ę pi erwszą . II Licznik znalezionych liczb pi erwszych. II Wskaźnik znal ezienia liczby pi erwsz ej. II Żądan a liczba liczb pi erwszych.
endl "Podaj . i l e chcesz obl i c z yć l iczb pierwszych (co najmn1 ej 4) ;
ci n » max :
II Żąda na liczba liczb pierwszych.
i f( max < 4 ) max ~ 4:
II Sprawdź podaną liczbę . jeżeli jest to mni ej II to zwiększ ją do 4.
ppri me = new 10ng[maxJ ; *pprime = 2; *(pp rime + l) = 3: *(pprime + 2) ~ 5;
do
II Wstaw trzy
II po czątkowe liczby p ierwsze .
niż
4,
Rozdział 4.•
t rial += 2; found = o; fort i nt i = O; i
<
Tablice. łańcuchy znaków i wskaźniki
227
II Następna wartość do sp ra wdzenia.
II Ustaw wska ź nik fo und.
II Dzielenie p rzez istniejąc e liczby p ierws ze.
count : i ++ )
{
found =(tri al i f (found) break ;
% *(pprime
+ i»
=~ O ;lITru e, jeźelidzielenieniemareszty.
II Jeżeli nie ma reszty z dzielenia, li to liczba nie jest liczbą pierwszą.
} if (found -- O)
*(pprime + count ++) whil e(count < max); II
WY Śl l j p i ę ć
for( int i
=
II Mamy jedną... 11...a więc zap isuj emy ją do tablicy primes.
t ria l ;
=
llcz b pierwszych do wie rsza.
O; i < ma x; i ++)
(
i f( i % 5 ~- O) cout « endl ; cout « setw( 10) « *(ppr ime +
II Nowy wiersz dla pierwszego i co piątego wiersza. i) ;
}
delet e pprime cout « return Poniżej
[ J pprlme ;
znajduje się
3 17 37 59
wartość wskaźn ika .
endl ; O;
Podaj . i le chcesz 2 13 31 53
II Zwo lnij pamięć II i ponownie ustaw
= O;
przykładowy obl ic zy ć
wynik
działania
tego programu.
li czb plerwszych (co najmn ie j 4) ; 20
5
7
11
19 41 61
23 43
29 47
67
71
Jak to dziala W
rzeczywistości
program ten jest podobny do swojej pierwotnej wersji. Po otrzymaniu liczby liczb pierwszych w zmiennej całkowitej max za pomocą operatora new przy dzielamy pamięć z wolnego obszaru tablicy o odpowiednich rozmiarach. Dodaliśmy także zabezpieczenie na ewentualność podania przez użytkownika liczby mniejszej niż cztery . Jest to potrzebne, ponieważ program wymaga przydzielenia pamięci z wolnego obszaru co najmniej trzem inicjalizującym liczbom pierwszym plus jednej nowej. Rozmiar tablicy określamy, stawiając zmienną max pomiędzy kwadratowymi nawiasami znajdującymi się za specyfikacją typu tablicy :
żądanej
pprime = new long[maxJ; Adres obszaru pamięci przydzielonego za pomocą operatora new przechowujemy we wskaź niku pp rime. Gdyby pamięć nie mogła zostać przydzielona, program zostałby w tym momencie zamknięty.
Po pomyślnym przydzieleniu pamięci przechowującej liczby pierwsze trzem pierwszym ele mentom tablicy zostają nadane wartości będące trzema początkowymi liczbami pierwszymi :
228
Visual C++ 2005. Od podstaw *ppr ime = 2: II Wstaw trzy
*(ppri me + l ) = 3: II po czątkowe liczby p ierwsze.
*(pprime + 2) = 5;
W celu uzyskania dostępu do trzech pierwszych elementów tablicy posłużyliśmy się operatorem Jak już widzi eliśmy wcześniej , nawi asy w drugiej i trzeciej instrukcji zostały zastosowane ze względu na fakt , że priorytet operatora * jest wyższy n i ż operatora +. wyłuskania .
Nie
można poda ć wartoś ci po czątkowych
pam ięć
dynamicznie. Jeż eli chcemy ustawić my użyć j awny ch instrukcji przypisania .
elementów tablicy, dla której przydzielamy elementów tablicy, musi
wartośc i początkowe
Obliczanie liczb pierwszych odbywa s i ę identycznie jak poprzednio. Jedyna zmiana polega na tym, że nazwa wskaźnika pprime za stąpiła nazwę tablicy primes, której używali śmy w poprzed niej wer sji . Proces wys yłania danych na wyj ście j est taki sam . Dynamiczne pozyski wanie pamięci nie sprawia żadny ch problemów. Po jej przydzieleniu nie ma ona żadnego wpływu na sposób zapisu obliczeń. Po
zakończen iu używania
tablicy usuwamy ją z obszaru wolnej
pamięci
za
pomoc ą
delete, pamiętając o dodaniu nawi asów kwadratowych w celu zaznaczenia,
że
operatora usuwamy
tabli cę .
delete [ J pprime :
Mimo
że
pprime
II Zwolnij pam ięć
w tym przypadku nie jest to koni eczne, ust awiamy ~
o:
II i po nownie ustaw
wskaźnik na
zero:
wartość wskaźnika.
Cała pami ę ć
przydzielona w programie z wolnego obszaru jest zwalniana w momencie j ego ale dobrze j est wyrob i ć sobie naw yk ponownego ust awiania ws kaźn i kó w na zero, kiedy nie w s kazuj ą one ju ż prawidłowy ch ob szarów p amięci .
zako ń c zeni a ,
Dynamiczne przydzielanie pa mięci tablicom wielowymiarowym Pr zyd zielani e wolnej pam ię ci tabl icy wi elowymi arowej wymaga uży c i a opera tora new w tro chę bardziej skomplikowanej formie niż w przypadku tablic j ednowymiarowych . Za kładaj ąc , że wskaźnik pbea ns został j u ż prawidłowo zadeklarowa ny, aby uzy skać pami ęć dla tabli cy beans[3] [4 J, której u żywali ś my już wcze śni ej , moglibyśmy napi s ać następującą in strukcj ę :
pbeans
~
new double [3J[4J:
Wystarczy tylko tów tabli cy.
podać
II Przydziel p am ięć tablicy 3 x 4.
wym iary tablicy w nawiasach kwadratowych po nazwie typu elemen
Przyd zielani e p ami ę ci tabli cy trójwymi arowej wy maga pod ani a dod atk owego wymiaru za ope ratorem new, jak w idać na p oni żs zym przykład z i e : pBlgArray = new doubl e [5J[10J[10J : II Przydziel pam ięć tablicy 5 x 10 x 10.
Rozdzial4.• Tablice, łańcuchy znaków i wskaźniki Bez względu na liczbę wymiarów w utworzonej tablicy , aby ją zniszczyć i zwolnić przez nią pamięć, piszemy następującą instrukcję:
delete [J pBigAr ray:
II Zwolnij pamięć z ajmowaną prz ez
Bez względu na liczbę wymiarów w tablicy niszczymy ją zawsze za po którym umieszczamy jedną parę nawiasów kwadratowych.
229
zajmowaną
rab/icę .
pomocą
operatora del ete,
Wiemy już, że zmiennej możemy użyć jako określenia wymiaru tablicy jednowymiarowej, dla której pamięć ma zostać przydzielona dynamicznie za pomocą operatora new. Taka sama możliwość istnieje w przypadku tablic dwuwymiarowych, ale z tym ograniczeniem, że za pomocą zmiennej może być określony tylko ostatni wymiar po lewej . Wszystkie pozostałe wymiary muszą być stałymi lub wyrażeniami stałymi . W związku z tym możemy napisać:
pB igArray - new doub le[max][ l OJ [l OJ: powyższym kodzie jest zmienną. Podanie zmiennej dla innego wymiaru lewej spowoduje wygenerowanie przez kompilator komunikatu o błędzie .
max w
Używanie
niż
ostatni po
relerencii
Referencje pod wieloma względami przypominają wskaźniki (dlatego też mówię o nich dopiero teraz), ale nie są tym samym. Prawdziwe znaczenie referencji staje się oczywiste, kiedy zaczynamy używać ich z funkcj ami, a w szczególności w kontekście programowania zorientowanego obiektowo. Na pierwszy rzut oka wydają się bardzo prostą, a nawet trywialną koncepcją, ale to tylko mylące pozory . Jak zobaczymy później, referencje dają nam pewne niezwykłe możliwości , a w niektórych sytuacjach pozwalają na osiągnięcie rezultatów niemożliwych do uzyskania inną drogą.
Czym jest referencja Referencja jest aliasem zmiennej. Ma ona nazwę , której można użyć zamiast nazwy zmiennej. Jako że jest to alias zmiennej , a nie wskaźnik, to zmienna ta musi być zadeklarowana przed deklaracją referencji . Dodatkowo w przeciwieństwie do wskaźników - referencji nie można zmieniać, aby reprezentowały inne zmienne.
Deklarowanie iinicjalizowanie referencji Przypuś ćmy, że
long number Referencję
~
mamy
poniższą deklarację zmiennej :
o:
do tej zmiennej
long&rnumber - numbe r ;
możemy zadeklarować
za
pomocą następującej
II Deklaracja ref erencji do zmienn ej number.
instrukcji :
230
Visual C++ 2005. Od podstaw Znak & znajdujący się po nazwie typu l onq i przed nazwą zmiennej rnumber informuje, że deklarowanajest właśnie referencja i nazwa zmiennej (number), którą reprezentuje, znajdująca się po znaku równości, została określona jako wartość początkowa. A zatem zmienna rnurnber jest referencją do l ang. Możemy teraz użyć naszej referencj i zamiast oryginalnej nazwy zmiennej. Na przykład instrukcja:
rnumber
+=
10;
spowoduje zwiększenie zmiennej number o 10. Spójrzmy, jaka jest różnica pomiędzy referencją rnumber a rowanymi w poniższej instrukcji:
long* pnumber
=
&number ;
wskaźnikiem
pnumber, zadekla-
II Inicj alizacj a wskaźnika adresem.
Powyższa Dzięki
instrukcja deklaruje wskaźnik pn urnber i inicjalizuje go adresem zmiennej number. temu możemy zwiększyć wartość zmiennej number za pomocą poniższej instrukcji :
*pnumber
+=
10:
II Zwiększ zmienną numb er p oprzez
wskaźn ik.
Pomiędzy wskaźnikiem
a referencją jest znaczna różnica. Wskaźnik musi zostać wyłuskany i bez względu na to, jaki adres zawiera, jest używany do uzyskiwania dostępu do zmiennej, która ma być u żyta w wyrażeniu . W przypadku referencji nie ma potrzeby wyłuskiwania. Czasami referencja przypomina wskaźnik, który został już wyłuskany , jednak nie można jej zmienić, aby wskazywała inną zmienną. Referencja jest wiernym odpowiednikiem zmiennej, do której się odnosi. Może się wydawać, że referencja jest po prostu alternatywnym sposobem zapisu danej zmiennej i w tym przypadku rzeczywiście tak jest. Jednak przy omawianiu funkcji w C++ przekonamy się, że nie jest to prawda i że dostarcza ona pewnych bardzo pożytecz nych możliwości.
Programowanie wC++/CLI W CLR dynamiczne przydzielanie pamięci działa inaczej . CLR posiada własną stertę pamięci, która jest niezależna od sterty w natywnym C++. CLR automatycznie usuwa pamięć przydzieloną na stercie, kiedy nie jest już potrzebna, a więc pisząc programy CLR, nie potrzebujemy operatora del ete. CLR może również co pewien czas kompaktować stertę w celu uniknięcia jej fragmentacji. Zarządzanie stertą i czyszczenie sterty dostarczanej przez CLR nazywa się usuwaniem nieużytków (nieużytki to usunięte zmienne i obiekty), a sterta poddana takiemu procesowi to sterta poddana procesowi usuwania nieużytków (ang . garbage-collected heap). W programach w C++/CLI do przydzielania pamięci zamiast operatora new używamy operatora genew. Przedrostek ge oznacza, że przydzielamy pamięć ze sterty oczyszczonej (ang . garbage-collected heap), a nie z natywnej sterty C++, gdzie usuwanie nieużyt ków należy do naszych obowiązków . Mec hanizm usuwania nieużytków CLR potrafi u suwać obiekty i zwalniać zajmowaną przez nie pamięć, kiedy nie sąjuż potrzebne. W tym momencie rodzi się pytanie : "Skąd ten mechanizm wie, kiedy dany obiekt na stercie nie jest już potrzebny?". Odpowied ź jest bardzo prosta: CLR śledzi każdą zmienną, która wskazuje jeden z obiektów na stercie. Gdy nie ma żadnych
Rozdział 4.
• Tablice, łańcuchy znaków i wskaźniki
zmienn ych zaw i e raj ąc yc h adres dane go obiektu, nie można go u sunąć.
można się
do niego
odwołać ,
231 a
w i ęc
Ponieważ proces usuwania ni eu żytków może by ć wykonywany razem z kompaktowaniem sterty w celu u sun ięcia pofragmentowanych, nieużywanych bloków pamięci , adresy elementów danych przechowywanych na stercie mogą ulec zmianie. W związku z tym z taką stertą nie mo żemy używ ać zwykłych w skaźników z natywnego C++, ponieważ - jeżeli lokalizacja danych się zmieni - stałyby s i ę one bezużyteczne . W tym przypadku potrzebujemy sposobu uzyskiwania d o stępu do obiektów na stercie, który pozwala na uaktualnianie adresu, kiedy mechanizm usuwania ni eużytków zmieni lokalizacje danych na stercie. Możemy to osiągnąć na dwa sposoby: za pomocą uchwytu śledzącego (zwanego także po prostu uchwytem ang. tracking handle), który jest analogic zny do wskaźnika w natywn ym C++, oraz za pomocą referencji ś l e dząc ej (ang. tracking referencei , która w CLR jest odpowiednikiem referencji z natywnego C++.
Uchwyty śledzące Uchwyt ś ledzący jest pod pewnymi względami podobny do zwykłego uchwytu z natywnego C++, ale s ą też znaczne różnice . Uchwyt śledzący przechowuje adres, który jest automatycznie aktuali zowany przez mechanizm usuwania nieużytków, jeżeli obiekt przez niego wskazywany zostanie przenie siony podczas kompaktowania sterty . Nie mo żna jednak przy użyciu w skaźn ika ś ledz ącego wykonywać operacj i arytmetycznych na adresach , tak jak robiliśmy to przy uży c iu natywnych wskaźników . Rzutowanie wskaźników ś l e dzący c h jest również niedozwolone. Do wszystkich obiektów utworzonych na stercie CLR muszą być utworzone uchwyty ś l edzące . Wszystkie obiekty należące do klasy, będące odniesieniami do typów klasowych, przechowywane s ą na stercie, a zatem zmienne tworzone w celu odnoszen ia s i ę do tych obiektów muszą być uchwytami ś l e dzący m i. Na przykład typ klasowy St ri ng j est typem klasy referencji , a wi ęc zmienne odnoszące się do obiektów typu St ri ng mu szą być uchwytami ś l e dzącym i. Pamięć dla typów klas wartości przydzielana jest domyślnie na stosie, ale mo żna wybrać stertę za pomocą operatora genew. Jest to także dobry moment, aby przypomnieć sobie to, co powiedzi ałem w drugim rozdziale, że zmienne, którym została przydzielona pami ęć na stercie (czyli wszystkie typy referencyjne CLR), nie mo gą by ć deklarowane w zas i ęgu globaln ym.
Deklarowanie uchwytów śledzących Uchwyt do typu definiujemy, stawiając po jego nazwie znak (potocznie zwany daszkiem). Na przykład poniżej znajduje się deklaracja uchwytu ś l e dz ące g o o nazwie proverb , który może przechowywać adres obiektu typu St r i ng: A
St ri ng proverb: A
Powyższy kod definiuje zmienną prover b jako uchwyt ś l e d zący typu St r t nę". Podczas tworzenia uchwytu jest on automatycznie inicjalizowany wartością zerową, a więc nie odnosi się do niczego. Aby jawnie przypisać uchwytowi warto ść ze rową, należy posłużyć się słowem kluczowym nul l ptr:
232
Visual C++ 2005. Od podstaw proverb
~
null pt r;
II Ustaw
warto ś ć
uchwytu na zero.
Należy zauw a żyć, że
w tym przypadku - w przeciwieństwie do natywnych wskaźników nie można użyć wartości 0, która reprezentowałaby wartość nul l. Jeżeli użyjem y tutaj wartości 0, to zostanie ona przekonwertowana na typ obiektu, do którego odnosi się ten uchwyt, a adres tego nowego obiektu będzie przechowywany w tym uchwycie.
Oczywiście uchwyt można zainicj alizowa ć jawnie podczas deklaracji . P oniżej znajduje jeszcze jedna przykładowa instrukcja definiuj ąca uchwyt do obiektu St ri nq:
String saying ~ sam nie wi em. ": A
L " K iedy ś myś la ł em . ż e
j est em.ni ezdecydowany. ale t eraz t o
si ę
j uż
Powyższa instrukcja tworzy na stercie obiekt typu St r i ng zawierający łańcuch po prawej stronie przypisania. Adres nowe go obiektu przechowywany jest w zmiennej sayi ng. Warto zauważyć , że typem literału łańcuchowego jest const wchar _t *, a nie typ St r i nq, Sposób, w j aki zdefiniowana jest klasa St r i ng, umożliw ia u żywanie takich literałów do tworzenia obiektów typu St ri nq. Poniż ej
znajduj e s i ę
i nt value A
~
przykład
utworzenia uchwytu do typu
warto śc i ;
99;
Powyż sza
instruk cja tworzy uchwyt val ue typu i nt " , a warto ś ć , kt órą ws kazuj e na stosie, zainicjalizowana wartością 99. Pami ętajmy, że utworzyliśmy pewien rodzaj wskaźnika, a więc val ue nie może uczestniczyć w działani ach arytmetycznych bez uprzedniego wyłuskania. Do tego celu z kolei używ amy tego samego operatora * co w przypadku w skaźn ików natywnych. Na przykład pon i żej znajduj e s i ę instrukcja, w której został a użyta wartoś ć wskazywana przez uchwyt śledzący w d zi ałaniu arytmetycznym: zost ała
i nt result
~
2*(*value)+15;
Wyrażenie *va l ue znajdujące się w nawiasach uzyskuje dostęp do liczby c ałkowitej przechowywan ej w adresie umieszczonym w uchwyc ie ś le dzącym , dzięki czemu zmienna result zostaje ustawion a na wartość 213 . Zauw ażmy, że jeśli ski w ać
go jawnie -
po prawej stronie instrukcji użyjem y uchwytu , to nie ma potrzeby kompilator sam s i ę tym zaj mie. Na przykład :
wyłu
int result ~ o; resul t ~ 2*(*value)+15 ; A
W powyższym przykładzie najpierw tworzymy uchwyt result, który wskazuje na stercie warO. Warto zauważyć, że w tym momencie kompilator zgłosi ostrze żenie , ponieważ wygląda to tak, jakbyśmy chcieli zaini cjalizow ać uchwyt wartości ą zerową, a tak nie nal eży tego rob ić. Jako że w następnej instrukcji resul t znajduje się po lewej stronie przypisania, a prawa strona daje wynik, kompilator jest w stanie "do my ś lić się", że przed zapisaniem wartości trzeba najpierw wyłuskać result . Oczyw iście m o gliby śm y zapi sać to także w jawny sposó b: tość
*result
~
2*(*value)+15;
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
233
Zauważmy, że
zapis taki działa tylko wtedy, gdy result jest już zdefiniowany. Gdyby został on tylko zadeklarowany, to po uruchomieniu programu otrzymalibyśmy błąd wykonywania. Na przykład: tnt" resul t : *resul t ~ 2*(*va lue)+15 ;
II Deklara cja, ale nie defini cja. II Komunikat o błędzie - ni eobsłużony wyjątek.
Jako że w drugiej instrukcji wyłuskujemy uchwyt resul t, sugerujemy, że obiekt przez niego wskazywany już istnieje. Ale tak nie jest i dlatego spowodowaliśmy błąd wykonan ia. Pierwsza instrukcja jest deklaracją uchwytu resul t , którego wartość domyśln ie zostaje ustawiona na nu ll , a wartości tej nie można wyłuskać . Jeżeli nie wyłuskamy jawnie uchwytu result w drugiej instrukcji, to wszystko będzie w porządku, gdyż wynik wyrażenia znajdującego się po prawej stronie przypisania jest typem klasy wartości, a jego adres przechowywany j est w uchwycie result .
Tablice CLR Tablice CLR są inne niż tablice w natywnym C++, Pamięć dla tablicy CLR przydzielana jest na oczyszczonej stercie, ale to nie jedyna różnica. Tablice CLR mają wbudowane pewne funkcje (za chwilę będziemy o nich mówić), których nie mają tablice w natywnym CH. Typ zmiennej tablicowej określamy za pomocą słowa kluczowego array. W trójkątnych nawiasach, po słow ie kluczowym arr ay, musimy także podać typ dla elementów tablicy, W związku z tym ogólna forma zmiennej odnoszącej się do tablicy jednowymiarowej to array<el ement_t ype>". Jako że tablica CLR tworzona jest na stercie, zmienna tablicowa zawsze jest uchwytem śledzą cym. Poniżej znajduje się przykładowa deklaracja zmiennej tablicowej: arrayA data;
Zmienna tablicowa data może elementów typu i nt. Tablicę
CLR można operatora genew:
przechowywać wskaźnik
utworzyć równocześnie
arraYA data = gcnew array( 100);
z
do dowolnej jednowymiarowej tablicy
deklaracją
zmiennej tablicowej za
pomocą
II Utwórz tablicę przechowującą 100 liczb II całkowitych.
Powyższa instrukcja tworzy jednowymiarową tablicę o nazwie data (zauważ, że zmienna tablicowa jest uchwytem śledzącym, a więc nie można zapomnieć o znaku po typie elementu pomiędzy nawiasami trójkątnymi) . Liczba elementów znajduje się w nawiasach okrągłych po określeniu typu tablicy, a więc tablica ta zawiera 100 elementów , z których każdy może przechowywać wartości typu i nt. A
Podobnie jak w przypadku tablic w natywnym C++, elementy w tablicach CLR indeksowane są od zera, a więc wartości elementów tablicy data możemy ustawić następująco: for( i nt i = O ; i <100 ; i++) data [i ] = 2*(i+1);
234
Visual C++ 2005. Od podstaw Powyższa pętla
ustawia wartości elementów na 2, 4, 6 itd. aż do 200. Elementy w tablicach CLR są obiektami, a więc w tej tablicy przechowujemy obiekty typu Int 32. Oczywiście w działaniach arytmetycznych zachowują się one jak zwykłe liczby całkowite, tak więc fakt, że są one obiektami, jest w takich przypadkach bez znaczenia. W
powyższej pętli
liczba elementów została podana w postaci literału. Lepiej jednak która przechowuje liczbę elementów:
byłoby
użyć właściwości Lengt h tablicy,
for(i nt i = O ; i < data ->Lengt h ; i ++) data[i] = 2*( i+1), Aby uzyskać dostęp do właściwości Length, używamy operatora - > , ponieważ data jest uchwytem i zachowuje się jak wskaźnik . Właśc iwość Length zapisuje liczbę wartośc i w postaci 32-bitowej liczby całkowitej . W razie potrzeby rozmiar tablicy możemy rozszerzyć do wartości M-bitowej za pomocą właściwości LongLength. Można również przejść
przez wszystkie elementy tablicy za pomocą pętli f or each:
arrayA values ~ { 3, 5, 6. 8, 6); for each(lnt item in values ) {
i t em ~ 2*item+ l ; Conso le: :Write ("{O .5}",item) ; Zmienna i t ern znajdująca się wewnątrz pętli odnosi się do wszystkich elementów w tablicy val ues. Pierwsza instrukcja w ciele pętli zamienia bieżącą wartość elementu na jej podwójną wartość plus jeden. Druga instrukcja wysyła na wyjście nową wartość wyrównaną do prawej w polu o szerokości pięciu znaków, a więc wynik tego fragmentu kodu przedstawia się następująco:
7
11
13
17
13
Zmienna tablicowa może przechowywać adres dowolnej tablicy o takiej samej liczbie wymiarów i takim samym typie danych . Na przykład: dat a
=
gcnew array(45) ;
Powyższa instrukcja tworzy nową j ednowym i arową tablicę 45 elementów typu i nt i zapisuje j ej adres do uchwytu dat a. Oryginalna tablica zostaje usunięta.
Można również utworzyć tablicę, podając zbiór wartości początkowych
array<dou ble>Asamples
=
{
elementów:
3,4, 2,3. 6,8, 1.2. 5,5, 4,9, 7,4, 1,6};
Rozmiar tablicy określony jest przez liczbę wartości początkowych znajdujących się w nawiasach (w tym przypadku osiem). Wartości te zostają przypisane do elementów w kolejności , w jakiej zostały podane . Oczywiście
Elementy tej tablicy zostały zainicjalizowane za pomocą łańcuchów podanych w nawiasach . Liczba tych łańcuch ów określa liczbę elementów tablicy. Obiekty typu St r i ng tworzone są na stercie CLR , a więc typ elementu jest typem uchwytu śledzącego - St r i nq". Gdy nie zainicjalizujemy zadeklarowanej zmiennej tablicowej, musimy jawnie, jeśli chcemy używać listy wartości początkowych . Na przykład :
tę tablicę utworzy ć
array<St ri ngA>Anarnes : II Deklara cj a zmi ennej tablicowej. names = gcnew ar ray-St r tnq'»] "Jack". "Jane". "Joe". "Jessiea". "Jim" . "Joanna"}; Druga z powyższych instrukcji tworzy tablicę i inicjalizuje ją łańcuchami w nawiasach. Bez jawnej definicji z użyciem operatora genew, instrukcji tej nie można by skompilować. funkcji statycznej C1ear() zdefiniowanej w klasie Ar ray można ustawić na zero elementów zawierających wartości liczbowe. Funkcje statyczne wywołuje się przy użyciu nazwy klasy. Więcej na temat tych funkcji dowiemy się przy okazji szczegółowego omawiania klas. Poniżej znajduje się przykład użycia funkcji C1ea r ( ) :
Za
pomocą
dowolną sekwencję
Array: ;Clear (samples. O. samples->Lengt h);
II Ustaw wszys tkie elementy na zero.
Pierwszym argumentem funkcji C1ear( ) jest tablica, którą chcemy wyczyścić, drugi argument to indeks pierwszego elementu, który chcemy wyczyścić, a trzeci to liczba elementów do wyczyszczenia. A zatem powyższy przykład ustawia wszystkie elementy tablicy sampl es na O. O. Jeżeli funkcję C1ear () zastosujemy z tablicą uchwytów śledzących , jak np. St r tnq", to elementy zostaną ustawione na nu 11. Zastosowanie tej funkcji dla tablicy wartości logicznych spowoduje ustawienie wartości elementów na fa l se. Czas na wyczyszczenie jakiejś tablicy .
R!lml!mI Używanie tablic ClR Przykład
ten generuje
tablicę zawierającą
losowe
wartości,
a
następnie
z nich:
using namespace System ; i nt main(array<System : :St ring A> Aargs)
r array<double>Asamples II Generowanie losowych
=
gcnew array<double>(50):
wartości
dla elementów.
RandomA generat or = gcnew Random ; for(i nt i = O : i< samples->Lengt h ; i++) samples[i ] = l OO.O*generat or->Next Double( );
II WY.lylanie na
wyjście
próbek.
Console : 'WriteLi ne(L"Tabl ica zawi era n a st ę pują c e l iczby:"); for( int i = O : i< samp les->Lengt h : i++)
odnajduje
największą
236
Visual C++ 2005. Od podstaw
Conso l e: :WriteC L"{O ,10:F2}", samples[l]) : i f (Ci +l )%5 ~ ~ O) Console: :WriteLi neC) : II Szukanie największ ej wartośc i ,
double max ~ O: for eachC doub le samp le i n samo les) if Cmax < samp le) max ~ samo le ; Co nso le : :WriteLine( L "Na jw ię ksza wa rtość
w tabl icy t o (O :F2}", max) :
ret urn O; Przykładowy
wynik
działania
tego programu
widać poniżej:
Tab l lca zawiera następu jące ll czby: 63,03 66,07 83,73 12,11 76,99 89,57 83,78 80,28 25,02 86, 09 56,39 69,56 64,79 90,73 0,84 11 ,58 11,26 55,80 75, 29 75,01 88,99 26,72 57,32 95,52 77,49 43,02 28 ,21 6,97 24,58 72.85 50,23 40,12 15,13 80,63 86.40 79,60 66.04 41.69 59.03 5.86 Najw ięks za wa r tość w tab licy to 95.52
50 elementów typu doub 1e: gcnew array<do ub le>(50);
Zmienna tablicowa samp1es musi stercie oczyszczonej . Tablicę wypełniamy
losowymi
być
uchwytem
wartościami
śledzącym, gdyż
tablice CLR tworzone
są na
typu doub1e za pomocą następujących instrukcji:
Ra ndamA generator = gcnew Ra ndom: tor t i nt i = O ; i < samp les ->Length , i++) samp l es[i] = 100. 0*generato r- >Next DoubleC) :
Pierwsza instrukcja tworzy na stercie CLR obiekt typu Random. Obiekt Random zawiera funkcje, które generują liczby pseudolosowe. W tym przypadku użyliśmy funkcji Next Doub 1e( ) w pętli, która zwraca losową liczbę typu doub 1e należącą do zbioru 0,0 - 1,0. Dzięki pomnożeniu tego przez 100 otrzymujemy wartości od 0,0 do 100,0. Pętla fo r zapisuje do każdego elementu tablicy samp 1es losową liczbę .
Obiekt Random zawiera również funkcję Next( ), która zwraca losową liczbę nieujemną typu i nt. Jeżeli w wywołaniu funk cji Next ( ) podamy argument w postaci liczby całkowitej, to zwró ci ona losową, nieujemną wartość mniejszą niż podany argument. Można także
Rozdzial4.• Tablice. łańcuchy znaków i wskaźniki poda ć
dwa argumenty w p ostaci liczb całko witych, które b ędą i maksymalną zwróconych liczb losowych.
237
reprezentowały wartości
minimalną
N astępna pętla wysyła
na wyjście
zawartość
tablicy, po
pięć
elementów na wiersz :
Console: :Wr i tel i netl "Tabl ica zawi era n a st ępują c e l iczby: ") ; far (i nt i = O : i< samples->length : 1++) {
Console: ;Wnte(l "{0.10:F2}". senplesj t D : if «i +1 )%5 == O) Con sole ; 'Wrl t eli ne( ) ; Wewnątrz pętli określamy, że wartość każdego
elementu ma znajdować się w polu o szero10 i mieć dwa miejsca po przecinku . Dzięki określeniu szerokości pól warto ści zostaną
wyrównane w kolumnach . Za każdym razem, gdy wynikiem działania Ci + l) %5 jest zero, wysy' łamy znak nowego wiersza, co ma miejsce co pięć wartości , dzięki czemu w każdym wierszu mamy pięć warto ś ci . ko ści
Na
zakończenie
sprawdzamy
największą wartość:
double max ~ O; for each(doub le sample l n samples ) i f (max < samp le) max = sample;
for each, aby pokazać, że można jej tutaj użyć. max z wartością każdego elementu i za każdym razem, gdy znajdzie większą od niej, wartoś ć ma x jest ustawiana na tę właśnie wartość. W ten sposób otrzy-
W po wyższym Porównuje ona wartość
przykładzie użyłem pętli
warto ś ć
mujemy największą warto ść . Gdyby śmy to ść ,
to
chcieli jeszcze
zapisać po zycję
moglib yśm y posłuży ć się pętlą
indeksu elementu
zaw i eraj ącego n ajwiększą
war-
for. Na przykład :
doub le max = O: i nt i ndex ~ O; for (i nt i ~ O ; i < sample ->length ; i ++ ) i f( max < samples[iJ ) {
max ~ samples[ i J: i ndex = l ;
Sortowanie tablic jednowymiarowych Klasa Array w przestrzeni nazw System definiuje funkcj ę Sort() , która sortuje elementy tablicy jednowymiarowej w porządku rosnącym . Aby po sortować zawartość tablicy, wystarczy jako argument funkcji So rt O podać uchwyt do niej. Poni żej znajduje s ię przykład:
arrayA samples = { 27 . 3. 54. 11. IB. 2. 16}: Array; ;Sart(samples) ; for each(int value i n samp les) Conso le :Write(l "{O . B}" : va lue): Conso le: :Wr i t el i ne( ) ;
II Sortuj elementy tablicy . II Wyś wietl elementy tablicy .
238
VisIlai C++ 2005. Od podstaw Wywołanie kolejności.
2
3
funkcj i So rt ( ) spowodowało ustawienie elementów tablicy samp l es w Wynik wykonania powyższego fragmentu kodu jest następujący:
11
16
18
27
rosnącej
54
Podając dwa dodatkowe argumenty do funkcji So rt () , można ustawić w kolejnoś ci pewien zbiór wartości . Te argumenty to indeks pierwszego elementu ze zbioru do posortowania, a drugi to liczba kolejnych elementów. Na przykład:
arr ayA samp les = { 27. 3.54 . 11. 18. 2, 16}; Ar ray: :SortCsamp les . 2. 3): II Sortuj
elementy ze zb ioru
2 - 4.
instrukcja sortuje trzy elementy tablicy samp les, które zaczynają się od pozycji indeksowej 2. Po wykonaniu tych instrukcji elementy w tablicy będą miały następujące wartości :
Powyższa
27
3
11
18
54
2
16
Funkcja So rt () występuje w jeszcze kilku innych wersjach, o których możemy przeczytać w dokumentacji. Jedną z nich , szczególnie przydatną, wprowadzę teraz. Ta wersja funkcj i zakłada , że mamy dwie skojarzone tablice , tak ż e elementy w pierwszej z nich są kluczami odpowiadających im elementów w drugiej . Moglibyśmy na przykład przechowywać w jednej tablicy imiona osób , a w drugiej ich wagę. Funkcja So rt() sortuje tablicę names (zawierającą imiona) w kolejności rosnącej , a elementy tablicy wei ghts (zawierającej wagę) sortuje w taki sposób, aby nadal odpowiadały one kolejności pierwszej tablicy. Spójrzmy na przykład .
R!lmI!tmI Sortowanie dwóch skojarzonych ze sobą tablic Poniższy
kod tworzy tablicę imion oraz przechowuje wagę każdej osoby w elementach drugiej tablicy, odpowiadających danym osobom . Następn ie obie tablice zostają posortowane za pomocąjednej operacji:
#i nclude "st dafx.h" usi ng namespace Syst em; int mai nCarray<Syst em: :St ri ng A> Aargs) (
Ar ray; :Sort Cnames .wei ght s ) ; for eachCSt ri ngA name i n names ) Console: :Wr iteCL"{ O. 10)" . name) : Console: :WriteLinet ):
II Sortuj tablice. II Wyświetl imiona.
for eachC i nt wei ght i n we ights) Conso le: :Wr iteC L"{O. 10)". wei ghtl ; Console: :WriteL i neC) ; ret urn O;
II
Wyświetl
wagi.
Rozdział 4.•
Wynik
d ziałania powyższego
Al 176
Bill 180
Tablice, łańcuchy znaków i wskaźniki
programu przedstawia się Eve 115
J ill 103
Ma ry 128
239
następująco :
l ed 168
Jak to działa Wartości w tablicy weig hts odpowi adają wadze osoby zn aj dując ej s ię w elemencie o tym samym indeksie w tablicy names. Funkcja Sor t ( ), którą tutaj wywołuj em y, sortuje obie tablice za pomo c ą użytej jako argument pierwszej tablicy (w tym przyp adku names) w celu określenia kol ejności obu tablic. W wynikach działan ia programu widać, że każdej osobie nadal przypisana jest odpowiednia waga w drugi ej tabli cy.
PrzeszlIkiwanie lablicy jednowymiarowej W klasie Array do stępne s ą równi eż funkcje s łuż ąc e do wyszukiwania elementów w tablicach jednowymiarowych. Funkcja Bi narySearch() przeszukuje całą tablicę lub okre śloną jej czę ść w celu odnale zienia indeksu danego elementu za pom o c ą algorytmu binarnego. Alg orytm binarny wymaga, aby elementy, które mają zostać przeszukane, były posortowane, a w ięc przed u życ iem tej funkcji najpierw trzeba tablicę po sortować . Poniżs zy
kod przeszukuj e całą t ablicę :
a r ray< i nt >Ą
values = { 23. 45. 68. 94, 123, 127. 150, 203. 299}: int t oBeFound ~ 127: i nt pos i t i on = Ar ray: :Bi narySea rch(value s , to BeFound) : if ( pos it i on-D) Console : :WriteLine( L"Li czba {O} nie zo s ta ł a odnalez iona " , t oBeFound); el se Console : :Wr iteLine(L"L i czba {O} zost a ł a odnal ezio na w indeksi e ( l }. " . to BeFound, posi tion) :
Wartoś ć , którą chcemy zn a leźć, przechowywana jest w zmiennej t oBeFound. Pierwszym argumentem do funkcji BinarySearch( ) jest uchwyt do tablicy, którą chcemy prze szukać , a drugi argument okre śla, czego szukamy. Rezultat poszukiwania zwracany prze z funk cj ę BinarySearch( ) jest warto ś ci ą typu i nt . Je żeli w tablicy zostanie znaleziony drugi z podanych argument ów, to zwrócony zostanie jego indeks. W przeciwnym przypadku zwrócon a zostanie ujemna liczba całkowita. A zatem zwróconą wartoś ć trzeba sprawdzić w celu okre ś l e n i a, czy cel został odnaleziony. Jako że wartoś ci w tablicy valu es s ąjuż posortowane ro snąco , nie ma potrzeby sortować tablicy przed jej przeszuk aniem. P owyższy fragment kodu da następujący wynik :
Li czba 127
zo st a ł a
odnal ezi ona w indeksi e 5.
Aby przeszukać tylko ok reś lo ny zbiór elementów tablicy, n al eży użyć wersji funkcji Bi nary Searcht ), która przyjmuje cztery argumenty . Pierwszy argument to uchwyt tablicy do przeszukania, drugi to indeks, od którego ma s i ę rozpocząć przeszukiwanie, trzeci określa li czbę elementów, które chcemy przeszukać , a ostatni po szukiwaną wartość . Poni żej znajduje się przykład takiego przes zukiwania:
240
VisIlai C++ 2005. Od podslaw arr ay< int >A val ues = { 23, 45, 68 , 94 , 123, 127, 150, 20 3, 299}; int t oBeFound ~ 127; int posit ion = Array: :Bl narySearchCvalues , 3, 6, toBeFound): Powyż szy kod przesz ukuje wartośc i tablicy od czwartego elementu do osta tniego . Podobnie jak poprze dnia wersja, funkcja zwraca zna leziony indeks lub wartość uj em n ą, jeże li nic nie znajdzi e.
Omówmy przeszukiwan ie na przykładzie .
~ Przeszukiwanie tablic Poniżej
znajduje kiwaniem:
się
zmodyfikowa na wersja kodu z poprzedniego listingu z dodanym przeszu-
#include "stdaf x.h" using namespace Sys t em: int mai nC arr ay<Syst em : :St ri ng A> Aargs) array<St ringA/ names
{ "Jll l ", "Ted", "Ma ry", "Eve". "Bi ll ", "Al" , "Ned". "Zoe" : "Dan". "Jean"}: arrayAweight s = { 60, 112, 70 , 80, 120, 110. 125, 58 . 119. 74 }; array<StrlngA>AtoBeFound = {"Bi l l ". "Eve", "Al ". "F red" }; ~
Array. :Sort Cnames . weights);
II Sortuj tablice.
int result = O: for each CSt ringA name i n toBeFound)
II Zap isz wyni k szukania. II Szukaj wag .
{
result
=
Array: :BinarySearchCnames . name ):
II Przeszukaj
tab licę
nam es.
if Cresult
rezu ltat powyż sze go prog ramu:
Bil l wa ży 120 kg. Eve wa ży 80 kg. Al wa ży 110 kg, Waga osoby o imieniu Fred nie
z o s t ał a
odnaleziona.
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
241
Jak lo 11ziała Utworzyliśmy dwie skojarzone ze so b ą tablice - tablicę imion oraz tablicę odpowiadają cych tym osobom wag w kilogramach . Utworzyliśmy także tablicę toBeFound do przechowywania imion osób, których wagę chcemy poznać.
Tablice names i wei ghts sortujemy według tablicy names. Następnie w pętli f or each przeszukujemy tablicę names w celu odnalezienia każdego z imion znajdujących się w tablicy t oBeFound. Zmiennej pętlowej name zostaje przypisane po kolei każde imię z tablicy to BeFound. Bieżącego imienia wewnątrz pętli poszukujemy za pomocą następującej instrukcji : r esult = Arr ay: :BinarySearchCnames. name) ;
II Przeszukaj
tablicę
names.
Instrukcja ta zwraca indeks elementu z tablicy names, który zawiera imię name lub ujemną liczbę całkowitą, jeżeli imię nie zostanie odnalezione. Następnie za pomocą instrukcji warunkowej i f sprawdzamy wynik i wysyłamy odpowiedni komunikat: i f( resul t
Jako
że kolejność
w tablicy wei ght s została zmieniona w celu dopasowania do tablicy names, result możemy zindeksować tablicę wei ghts. Indeks w tym przypadku takąjak element z tablicy names, gdzie zostało znale zione imię.
zawartością zmiennej
ma
wartość
Jak
widać,
w tym, co
wygenerował
program,
imię
Fred nie
zostało
znalezione.
niepowodzeniem, to zwrócona wartość nie jest Jest ona bitowym odpowiednikiem indeksu pierwszego elementu, który jest większy od poszukiwanego obiektu, lub jest bitowym odpowiednikiem właści wości Length tablicy, jeżeli żaden element nie jest większy niż poszukiwany obiekt. Dysponując tą wiedzą, można użyć funkcji BinarySearchO do sprawdzenia, gdzie w tablicy powinniśmy wstawić nowy obiekt, nie zaburzając przy tym kolejności elementów. Przypuśćmy , że do tablicy names chcemy dodać imię Fred. Indeks miejsca, w którym powinn iśmy to imię umieścić, możemy znaleźć za pomocą poniższych instrukcji : Kiedy poszukiwanie binarne
II Sortuj ta blicę. Ar r ay; :Sor t (names) : A St r i ng name - L"Fred": i nt posit io n = Ar r ay; :Bi narySear chCnames , name) : II Jeż eli wartość ujemn a, i f Cposi t i on-D) posi t i on = -oosi t i on; II odwróć bity, aby uzyska ć indeks miej sca do wstawienia. Jeżeli
wynik wyszukiwania jest negatywny, odwrócenie wszystkich bitów daje nam indeks miejsca, w którym powinno zostać wstawione nowe imię. Jeżeli wynik jest pozytywny, imię jest identyczne z imieniem w tym miejscu, a więc możemy tego wyniku użyć bezpośrednio jako nowej pozycji , Tablicę
i użyć
names
możemy
wartości
teraz skopiować do nowej tablicy zawierającej jeden element więcej pozycji w celu wstawienia imienia w odpowiednim miejscu:
242
VisIlai C++ 2005. Od podstaw array<St ri ngA>A newNames = gcnew ar ray<St ri ngA>(names->Lengt h+l ): II Skop iuj elementy z tablicy names do newNam es
for(i nt i = O ; i <positi on : newNames[i ] = names[l]: newNames[positi on]
~
name:
i ++)
II Skopiuj nowy elem ent.
i f( posit ionLength ) II Jeż eli w tablicy nam es pozostały jakieś elementy. for(i nt i = pos i t ion : iLengt h ; i++) newNames[i +l] = names[i ] ; II skop iuj j e do tablicy new Names . Powyższy kod tworzy tablicę o jeden element większą niż stara tablica. Następnie kopiujemy wszystkie elementy ze starej tablicy do nowej do indeksu posit i on - 1. Następnie kopiujemy nowe imię, po którym umieszczone zostają pozostałe elementy starej tablicy . Aby usunąć starą tablicę, piszemy:
names = nul lpt r;
Tablice wielowymiarowe Możemy tworzyć
tablice wielowymiarowe. Maksymalnie tablica może być 32-wymiarowa , co powinno w zupełności wystarczyć do większości zasto sowań. Liczbę wymiarów tablicy podajemy w trójkątnych nawiasach bezpośrednio po typie elementu , oddzielając ją od niego przecinkiem . Domyślnie tablica ma jeden wymiar (dlatego do tej pory nie musieliśmy podawać liczby wymiarów). Poniżej znajduje się przykład utworzenia dwuwymiarowej tablicy elementów typu całkowitego:
arr ay
2> Ą
val ues
~
gcnew array( 4. 5):
Powyższa instrukcja tworzy dwuwymiarową tablicę z czterema wierszami i pięcioma kolumnami, a więc w sumie złożoną z 20 elementów. Aby uzyskać dostęp do tablicy wielowymiarowej, należy podać odpowiednią liczbę indeksów - po jednym dla każdego wymiaru. Indeksy podaje s i ę w nawiasach kwadratowych , oddzielanych przecinkami i umieszczanych po nazwie tablicy. Poniższy przykładowy kod ustawia wartości elementów dwuwymiarowej tablicy liczb całkowitych :
i nt nrows ~ 4: i nt ncol s = 5; arrayA va lues = gcnew array(nrows . ncols) : for(i nt i ~ O : i
Jak nietrudno zauważyć, użyta tutaj notacja dotycząca uzyskiwania dostępu do elementu tablicy dwuwymiarowej jest inna niż notacja użyta w natywnyrn C++. Nie jest to przypadek. Tablica w C++/CLI nie jest tablicą tablic , takjak tablica w natywnym C++, ale jest prawdziwąta-
Rozdział 4.•
Tablice. łańcUChy znaków i wskaźniki
blicą dw uwymiarową.
243
Do tab lic dwuwymiarowych w C++/CLI nie można stosować pojedynczyc h indeksów, gdyż nie mają one tutaj znaczenia - tablica jest prawdziwą tablicą dwuwymiarową, a nie tab l icą tablic. Jak j uż wcześniej mówiłem , liczba wymiarów tablicy określana jest mianem jej poziomu, a więc poziom tab licy va l ues w poprzednim fragmencie kod u to 2. W C++/C LI możn a oczywiście definiować tab lice na poziomach 3. i wyższych , aż do 32 . W przeciwieństwie do C++/CLI , tablice w natywnym C++ są zawsze na poziomie l ., ponieważ są one dwiema lub większą liczbą tablic tab lic . Jak przekonamy się później w C++/CLI również można definiować tablice tab lic. Użyjmy
tablicy wielowymiarowej w
przykładzie.
~ Używanie tablic wielowymiarowych Poniższy
program CLR two rzy
tab l iczkę mnożen ia
l2 x 12 w dw uwymiarowej tablicy:
#inclu de "st daf x.h" usi ng namespace Syst em: i nt mainCa rray <Syste m: :St r i ng A> Aargs )
I const i nt SIlE = 12: arrayA products = gcnew arr ayCS IlE.SIlE): for Ci nt i = O : i < SIl E: i++) fo rC i nt j = O : j < SIl E : j ++) product s[ i .j] = Ci +1)*C j +1): Console: :Wr itel ineC l "Ot o tab l i czka II Rysuj poziomą
krawędź
linię oddzielającą
Wyślij pozos ta łe
Wyślij
znak nowego wiersza.
II
Wyś lij
znak nowego wiersza.
z kreskami pionowymi.
forC i nt i - O : i <= SIlE : i ++ ) Consol e: :WriteCl "_ _ I" ) : Consol e : :Wr i tel i neC) : II
II
tabeli.
Console : :WriteC l " I ") : forCint i = l : i <= SIlE : i ++ ) Console : :WriteC l "{O.3 } I". i): Console : :Wr itel tner ): II Rysuj poziomą
do (O}: " . SIl E):
linię oddzielającą.
for Ci nt i = O : i <- SIl E : i++) Conso le : :WriteCl " " ): Console : :Writel t net ) : II Ry suj górną
m no że n i a
wiersze.
fo rCint i = O : i<SIl E i++) { Console: :Wr ite Cl"{O .3} I". t -I ) : for Cin t j = O : j <SIlE .: j ++ )
II
Wyś lij
znak nowego wiersza .
244
Visual C++ 2005. Od podstaw Console: ;WriteC L"{ 0.3} I". products[i ,jJ ) : Console: :WriteLine C) ; II Rysuj poziomą linię forCint i = O ; i
II
Wyślij
znak nowego wiersza .
II
Wyślij
znak nowego wiersza.
oddzielającą,
<= SIlE; i ++ ) Conso!e: :WriteCL" ") ; Console; :Wri te Li neC ) ; ret urn O;
Wynik
d z i ałani a powyż szego
Oto t abli czka
mn ożen i a
programu powinien by ć
n astępuj ący :
do 12:
1 1 I 2 I 3 1 4 1 5 I 6 1 7 I 8 1 9 I 10 I 11 1 12 I _ 1_1_ 1_ 1_ 1_ 1_ 1_ 1_ 1_ 1_ 1_ 1_ 1 1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4 5 6 7 8 9 10 11 12
2 4 6 8 10 12 14 16 18 20 22 24
3 I 4 1 5 1 6 6 1 8 1 10 1 12 9 1 12 1 15 I 18 12 I 16 I 20 I 24 15 1 20 I 25 I 30 18 I 24 1 30 I 36 21 1 28 1 35 I 42 24 I 32 1 40 1 48 27 36 45 54 30 40 50 1 60 33 44 55 1 66 36 48 60 72 1
Jak to działa Kodu wydaje się d o ść dużo , ale większa jego część dotyczy sterowania wyg lądem nych danych. Za p omo c ą poniższej instrukcj i tworzymy tabli c ę dwuwym i arow ą:
wysyła
const int SIl E = 12; arrayA products = gcnew arrayCSIl E,SIlE) ;
Pierwszy wiersz definiuje s tałą całkowi tą przechowującą l i czbę elementów każdego wymiaru tablicy. W drugim wierszu zdefiniowa li śmy dwuwym i arową tab licę , która zawiera po dwanaście elementów w dwóch wierszach. Zapis uje ona wyniki do tabe li o wymiarac h 12x12. Wartości
elementów tej tablicy obliczamy za
pomocą zagn ieżdżonej pętl i :
for Ci nt i = O ; i < SIlE ; i++) for Cint j = O ; j < SIlE ; j++) product s[ i ,jJ = Ci +1l*Cj +1) ; Pęt la zew nętrzna
iteruje przez wiersze, a wew nętrz na przez kolum ny. Wartością każdego elementu jest iloczyn wartości indeksowych wiersza i kolumny po uprzednim zwiększeniu ich o jeden. Reszta kodu w ciele funkcj i mai nO dotyczy już wyłącznie prezentacji wys yłanych danych.
Rozdział4.•
Po wysłaniu ści tabeli:
nagłówka
tabeli tworzymy
rząd
Tablice, łańcuchy znaków i wskaźniki
pionowych kresek w celu oznaczenia górnej
245 czę
farli nt i - O : i <- SIl E: i ++) Cansal e : :Write lL" ") : Cansal e : :WriteLi ne l ) : II Wyślij znak nowego wiersza. Każda
iteracja pętli dodaje pięć znaków poziomej kreski . Jako że górny limit w pętli także wlicza, rysujemy 13 zestawów pięciu kresek w celu utworzenia miejsca dla etykiet wierszy tabeli i dwunastu kolumn. się
Następnie,
za pomocąjeszcze jednej
II Rysuj
gó rną krawędź
pętli,
piszemy wiersz etykiet kolumn tabeli :
tabeli.
Cansal e : :Wr i t e l L" I" ) : fa rl i nt i - l : i <- SIl E : i++) Cansal e : :Write l L"{O.3} I". i): Cansal e : :Wr iteL i ne i ) :
II
Wyślij
znak nowego wiersza.
Miejsce nad etykietą wiersza musimy utworzyć oddzielnie, gdyż jest ono wyjątkowe i nie zawiera żadnej wartości . Wszystkie etykiety kolumn pisane są za pomocą pętli. Następnie wysyłamy znak nowego wiersza, przygotowując się na kolejne wiersze danych. Dane w wierszach tworzone
są w zagnieżdżonej pętli :
farlin t i - O : i <SIZE : i ++) { Cansale : :Writel L"{O .3 } I". i-n : farlin t j - O : j <SIlE : j ++) Cansale: :Wr itel L"{O .3 } I". praducts [i .rn : Cansal e : :Wr iteLi ne O : II Wyślij znak no wego wiersza.
Pętla zewnętrzna iteruje przez wiersze, a kod w niej zawarty tworzy kompletny wiersz, włącznie z etykietą wiersza po lewej stronie. Pętla wewnętrzna wstawia wartości z tablicy products, które odpowiadają wierszom o numerze i . Wartości oddzielane są pionowymi kreskami. Pozostały
kod
wysyła
na
wyjście
dodatkowe linie poziome,
wykańczając dół
tabeli .
Tablice tablic Elementy tablic mogą być dowolnego typu, a więc można utworzyć taką tablicę, w której elementami będą uchwyty śledzące do tablic. W ten sposób można utworzyć struktury zwane tablicami postrzępionymi, ponieważ każdy uchwyt do tablicy może zawierać inną liczbę elementów. Najłatwiej to zrozumieć na przykładzie. Przypuśćmy, że chcemy przyporządkować dzieci w klasie do odpowiednich grup w zależności od otrzymanej oceny w skali od l do 5. Najpierw utworzymy tablicę pięciu elementów, z których każdy przechowuje tablicę imion . Poniżej znajduje się potrzebna do tego instrukcja: array< array<
St r i n g
Ą
>Ą >Ą
grades - gcnew array< array<
Str in g
Ą
>Ą
>(5) :
Na pierwszy rzut oka kod z taką dużą liczbą daszków może wydawać się bardzo skomplikowany, ale to tylko pozory . Zmienna tablicowa grades jest uchwytem typu array-type>".
246
Visual C++ 2005. Od podstaw Każd y element tablicy również jest uchwytem do tablicy, a więc typ elementów tablicy jest taki sam (a r rayct ype>"), w związku z czym mu si zos tać podany w nawiasach trójkątnych w specyfikacji typu oryginalnej tablic y i z tego powodu mamy zapis ar ray< ar rayA >A. Elementy przechowywane w tablicach są także obiektami typu St ri ng, a więc w ostatnim wyrażeniu musimy zamienić typ na St r i nq". Dzięki temu otrzymujemy tablicę typu array< ar ray < St r i ngA >A >A. Mając już tablicę przykładowy
tablic, możem y przejść do tworzenia tablic imion. kod do wykonania tego zadania:
Poniżej
przedstawiam
grades[O] = gcnew ar ray<Str ingA>{"Loui se " , "Jack"}: II Ocena 5. grades D ] = gcnew array<Str i ngA>{"Bi ll" , "Mary" , "Ben" , "Joan"}: II Ocena 4. grades[2] ~ gcnew arra Y<String A>{ "J i 11 " , oWi11 ", "Phi l "}: II Ocena 3. grades[3] = gcnew arraY<StringA>{" Ned". "Fred" . "Ted" , "Jed" : "Ed"} : II Ocena 2. grades[4 ] = gcnew ar ray<String A>{ "Dan " , "Ann"}: II Ocena l. Wyrażenie
gr ades [ n] uzyskuje do stęp do n-tego elementu tablicy gr ades i jest to oczywiza każdym razem uchwyt do tablicy uchwytów typu St r i nq". A zatem każdy z powyż szych pięciu wierszy tworzy tablicę uchwytów do obiektów typu St r i ng i zapisuje adres do jednego z elementów tablicy gr ades. Jak widać, tablice łańcuchów mają różne rozmiary , a więc w ten sposób możemy zarządzać zbiorem tablic o dowolnych rozmiarach. ście
Całą tablicę
tablic
można utworzyć
i za i n i cj a l i zo w ać za
pomocą pojedynczej
instrukcji:
array< array < String A >A >A grades = gcnew array< ar ray< Str i ng A >A > { gcnew ar ray<StringA>{ "Louise " , "Jack"}. II Ocena 5. gcnew array<StringA>{"Bill" , "Mary" , "Ben". "Joan"}. II Ocena 4. gcnew array<StringA>{"Jill ". "Wi ll " , "Phil" }, II Ocena 3. gcnew array<Str ingA>{" Ned". "Fred" . "Ted" , "Jed" : "Ed"}. II Ocena 2. gcnew array<Str i ngA>{" Dan ". "Ann" } II Ocena l . }: Wartości początkowe
Spójrzmy teraz na tablic.
elementów podane
przykładowy
są w
nawiasach klamrowych.
program, w którym
zaprezentuję
sposób przetwarzania tablic
~ Używanie tablic tablic Utwórz program konsolowy CLR i umieść w nim
następujący
kod
źró dłowy :
#i nclude "st dafx.h" usi ng namespace System: int mai n(array<System: :St ri ng A> Aargs ) ar ray< array< Stri ng A >A >A grades {
~
gcnew array < array< Stri ng A >A >
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
gcnew array<StringA>{" lo uise" . "Jack"}. gcnew array<St ringA>{"Bill ". "Mary" . "Ben" . "Joan"}. gcnew arraY<String A>{"Jill" . "Will" . "Ph il "}. gcnew array<Str i ngA>{"Ned" . "Fred" . "Ted " . "Jed" . "Ed "}. gcnew ar ray<Str i ngA>{"Dan". "Ann"}
247
II Ocena 5. II Ocena 4. II Ocena 3. II Ocena 2. II Ocena l .
}:
wchar_t gradel et t er = ' 5' : for each(array< St r i ngA >A grade in grades) {
Consol e : :Writeli ne( "Uczni owi e z
oce ną
{O} : " , gradeletter - - ):
for each( St rin gA st udent in grade) Conso l e : :Write( " {O.12} " . student ) :
II
Wyś lij b ieżące im ię.
Consol e : :Writel i net ) :
II
Wyś lij
nolry wiersz.
retur n O: Poni żej
znajduje się wynik
Uczniow ie z oceną 5 : l oui se Jack Uczniowie z oceną 4 : Bil l Mary Uczni owi e z oce ną 3: J i ll Will Uczniowie z o ce ną 2: Ned Fred Uczni owi e z oc en ą l : Dan Ann
dzi ałani a powyższego
Ben
programu:
Joan
Phil led
Ed
Jed
Jak lo działa Definicja tablicy jest identyczna, jak w idzieli śmy wcześniej . Nas t ęp n i e definiujemy zmienn ą typu wchar_t o nazwie gradeLetter o wartości początkowej 1. Po służy nam ona do prezentacji ocen na ekranie. Imiona uczniów oraz ich oceny wy świetlone zostały za p om oc ą for each iteruje przez elementy tablicy grades:
pętli zagn i eżdżonych .
Ze-
wnętrzn a pętla
for each(ar ray< St ri ngA >A grade in grades) { II Przetwarzaj uczniów z
b ieżącą oceną...
}
Zmienna pętlowa grade jest typu array< Stri ng >A, ponieważ taki jest typ elementów w tablicy grades. Zmienna grade wskazuje po kolei każdą tablicę uchwytów typu Str i nq", a więc w pierwszej iteracj i wskazuje tablicę uczniów z oceną 5, w drugiej tablicę uczniów z oceną 4 i tak dalej aż do tablicy przechowuj ącej imiona uczniów z oceną l . A
Przy każdej iteracji pętli
zewnętrznej
Console : :Wri te l i net "Uczni owi e z
wykonywany jest następujący kod :
o c eną
{O}: ". gradeletter- - ) :
248
Visual C++ 2005. Od podstaw A
for each( Stri ng student i n gr ade ) Consol e : :Write( "{O .12}".st udent) :
II
Wyślij bieżące imię.
Console : :Wr it eLt ner ):
II
Wyślij n 0 lry
wiersz.
Pierwsza instrukcja wysyła wiersz zawierający bieżącą wartość zmiennej gra del etter, która początkowo ma wartość 5. Wyrażenie to zmniejsza także wartość zmiennej gradelette r , dzięki czemu w kolejnych powtórzeniach przyjmuje ona po kolei wartośc i 4, 3, 2 i 1. Następn ie wewnętrzna pętla
fo r eac h iteruje po kolei przez wszystkie imiona bieżącej tablicy ocen. Instrukcja wyjściowa użyta w tym przypadku to Conso l e : :Write O , a więc wszystkie imiona pojawią się w tym samym wierszu. Imiona wyrównane są do prawej w polach o szerokości 12 znaków . Po pętli funkcja Wr i t el i ne () wysyła nowy wiersz w celu przenie sienia danych dotyczących następnej oceny do następnego wiersza.
Jako
pętli wewnętrznej mogli śmy również użyć pętli
for (i nt i = O : i < grade- >Length : i++) Console: :Write( "{O.12}".qrade l i l ) : Pętla
ograniczona jest przez gra de.
właściwość
II
le ngt h
fo r : Wyślij b ieżące im ię.
bieżąc ej
tablicy imion wska zywanej przez
zmienną
Jako pętli zewnętrznej również mogliśmy użyć pętli fo r . W tym przypadku potrzebne by dalsze zmiany w wewnętrznej pętli i wyglądałaby ona następująco:
były
for (int j = O : j < grades ->Lengt h : j ++) { Consol e : :Wri t eLi ne( "Uczni owe z oc e n ą (O} : ". gr adeLette r+j) : fo r (i nt i = O : i < grades[ j ] ->Lengt h : i ++) Consol e : :Wr i ter "{ O. 12}".grades [j] l i D: II Wyślij b ieżące imię. Consol e: :WriteLi ne ( ) :
Teraz gra des [ j ] wskazuje j w j tablicy imion .
tablicę
elementów, a więc
wyrażenie
grad es [ j] [ i ] wskazuje i
imię
~ańcuchY Jak już wiemy, typ klasowy St r i ng, który jest zdefiniowany w przestrzeni nazw System, w języku C++/CLI reprezentuje łańcuch znaków (w rzeczywisto ści łańcuch składa si ę ze znaków Unicode) . Mówi ąc dokładniej , reprezentuje łańcuch składający s i ę z sekwencji znaków typu System: :Char. Obiekty klasy St r i ng mają bardzo dużą funkcjonalno ść , dzięki czemu przetwarzanie ł ańcuchów jest niezwykle proste. Zacznijmy od tworzenia łańcucha. Obiekt klasy St r i ng można
utworzyć
w
System::S tri ng saying
L"Co dwi e
g łowy .
A
=
następujący
sposób:
to nie j edna. " :
Zmienna say i ng je st uchwytem śledzącym do obiektu klasy St r i ng, który zo stał zainicjalizowany łańcuchem po prawej stron ie znaku =. Wskaźn iki do obiektów klasy St r i ng zawsze przechowuje się w uchwytach śledzących. Podany w powyższym przykładzie l iterał łańcu-
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
249
chowy jest typu wi de characte r, ponieważ przed nim stoi litera L. Jeżel i nie podamy litery L, to otrzymamy literał łańcuchowy zaw ierający znaki ośm iobitowe , ale kompilator przekonwertujeje do łańcuchów typu wi de-character. Dostęp do poszczególnych znaków można uzyskać za pomocą indeksów, tak jak w tablicy . Pierws zy znak w łańcuchu ma indeks O. Poni ż sza instrukcja wysyła na wyjście trzeci znak
sayi ng:
łańcucha
Console: :WriteLi ne( "Trzecim znaki emw ła ń cu c hu jest {O)". sayi ng [2]); Pamiętajmy, że choć za pomocą wartości indeksowych można uzyskać dostęp do określonego znaku w łańcuchu , to nie mo żna jednak zmienić jego zaw arto ś ci . Obiekty klasy St ri ng są stałe i nie można ich modyfikować . Liczbę
znaków danego
gość łańcucha
sayi ng
za pomocąjego właściwośc i Length. Dłu za pomocą następującej instrukcji :
łańcucha można sprawdzić
możemy wyświetlić
Consol e : : W riteLi ne( " Ł a ńcuch ma {O} znaków . ". sayi ng->Length);
Jako że sayi ng jest uchwytem śledzącym (który - jak wiemy - jest rodzajem wskaźnika) , aby uzyskać dostęp do właściwości Lengt h (lub jakiejkolwiek innej składowej obiektu) , musimy użyć operatora - >. Więcej na temat właściwości dowiemy się przy okazji szczegółowego omawiania klas w C++/CLI.
~ączenie łańcuchów Do łączenia łańcuchów znaków i tworzenia nowych obiektów klasy Stri ng operatora +. Na przykład :
mo żemy używać
St ri ng namel = L"Beth": St ri ng name2 = L"Betty" ; St ri ng name3 = namel + L" i " + name2: A A A
Po wykonaniu tych instrukcji zmienna na me3 zawiera łańcuch Bet h i Betty. Zauważ, w jaki sposób można łączyć obiekty klasy Stri ng z literałami łańcuchowymi za pomocą operatora +. Łączyć można również obiekty klasy Stri ng z wartościami liczbowymi i logicznymi , które zostaną automatycznie przekonwertowane do łańcuchów przed operacją łąc zenia . Poniżej znajdują się instrukcje ilustrujące to zj awisko: Stri ng St ri ng St ri ng St ri ng
A A
A
A
st r = L"War t o ś ć : ": st rl = st r + 2.5 : II Nowy łańcu ch st r2 = st r + 25; II Nowy łańcu ch st r3 = st r + t rue: II Nowy łańcu ch
Można również połączyć łańcuch
"Wartość: "Wartość: "Wartość:
2.5". 25 ". True ".
typu Str ing ze znakiem, ale wynik
znaku: char ch = ' Z' : wchar t wch = ' Z' Stri ng st r4 = st r + ch: St ri ngA st r5 = st r + wch; A
II Nowy II Nowy
łańcuch "Wartość:
łańcuch "Wartość:
90 ". Z".
będzie zal eżny
od typu
250
Visual C++ 2005. Od podstaw W komentarzach podane zo stały wyniki ka żdej instrukcji. Znak typu char traktowany jest jako wartość liczbowa i dlatego do łańcucha zos tała dołączona liczba . Znak typu w_char jest tego samego typu co znaki w obiekcie klasy Stri ng (typ Char), a więc do łańcucha dołączony został znak . Pamiętajmy, że obiekty łańcuchowe są stałe . Nie można zmien iać ich zawartości po ich utworzeniu . Oznac za to, że w wyniku wszelkich operacji mających na celu z m i anę zawarto ści obiektów klasy St r i ng zawsze otrzymujemy nowy obiekt klasy Stri ng.
W klasie Str i ng zdefiniowana jest również funkcja jo i nt ), która służy do łączenia w jeden kilku łańcuchów przechowywanych w tablicy z uwzględnieniem znaków oddzielających poszczególne łańcuchy . Poniż sza instrukcja łączy w jeden łańcuch imiona, oddzielając je przecinkami: . ar ray- St r t nq" >" names = { "J i ll ". "Ted". "Ma ry". "Eve". "Bi ll "}: Stri ng A separat or = ". ": St ri ngA joined = St ri ng: :Joi nCseparator. names ) :
Po wykonaniu powyższych instrukcji zmienna joi ned zawiera łańcuch "J i 11 . Ted. Mary . Eve. Bi 11 ". Łańcuch separ ator został wstawiony pomiędzy każdą parą łańcuchów z tablicy names. Oczywiście łańcuch ten może być dowolny - może to być na przykład i i wtedy otrzymamy wynik "J ill i Ted i Mary i Eve i Bi ll ". Spójrzmy teraz na przykładowy program z zastosowaniem obiektów klasy St r i ng.
~ Praca złańcuchami Przypuśćmy , że mamy tablicę liczb całkowitych , której wartości chcemy zaprezentować wyrównane w kolumnach . Chcemy, aby wartości były wyrównane i aby kolumny były wystarczająco szerokie, ponieważ zależy nam na zmieszczeniu największych wartości tablicy włącz nie z przestrzenią pomiędzy kolumnami. Poniższy program odpowiada tym wymaganiom.
II Cw4 17.cpp: main projectfile. II Tworzenie własnego łańcucha formatu.
#i ncl ude "stdafx.h" using namespace Syst em: int mai nCa rray<System : :St ri ng A> Aargs) {
arrayA values = { 2. 456. 23. -46, 34211, 456, 5609, 112098. 234, -76504 . 341, 6788, -909121, 99, l O}; String A formatStrI = "{O,"; II Pierwsza poło wa łań cu cha fo rmatu. II Druga poło wa łańcucha fo rmatu. St ri ng A format Str2 = T ' : St ri ngA number : II Przechowywanie liczby jako łańcu cha. II Sprawdź długość
najdłuższego łańcucha.
i nt maxLengt h = O: for each Cint value i n va lues )
II Przechowuje największą znalezioną
{
number = "" + value: i f CmaxLengt hLengt h)
II Utwórz
łańcu ch
z
wa rtości.
liczbę.
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
251
maxLength = number->Length: II Utwórz
lańcuch formatu
do
użycia
przy
wysyłaniu
danych na
wyjście.
St ring format = formatSt rI + (maxLength+1) + forma tSt r2: A
II
Wyślij wartości.
int numberPerLine = 3: for(in t i = O : i< values ->Length : i++) (
Rezultat działania tego programu jest następujący : 2 456 -46 34211 5609 112098 -76504 341 -909121 99
23 456 234 6788 10
Jak lo działa Celem tego programu jest utworzenie łańcucha formatującego wyrównującego liczby całko wite z tablicy val ues w kolumnach o szerokości wystarczającej dla najdłuższej z nich. Łańcuch formatujący zaczynamy tworzyć w dwóch częściach: St ri ng formatSt rI = "(O. " : St ri ng formatSt r2 = "}" : A
A
II Pierwsza polowa lańcuchaformatującego. II Druga polowa lańcuchaformatującego.
Powyższe dwa łańcuchy stanowią początek
i koniec łańcucha formatującego, który chcemy na koniec. Aby go uzupełnić, musimy pomiędzy dwiema połowami formatStr I oraz format Str 2 umieścić długość najdłuższego łańcucha reprezentującego liczbę .
otrzymać
Tę wartość
odszukujemy za pomocą poniższego kodu:
int maxLength = O: for each( int value in values)
II Przechowuje najdluższą znalezioną
liczbę.
(
numbe r = "" + value: i f (maxLengt hLengt h) maxLength = numbe r->Length:
II Utwórz
lańcuch
z
wartości.
Wewnątrz pętli każda liczbakonwertowana jest do typu łańcuchowego poprzez dołączenie jej do pustego łańcucha. Właściwość Length każdego łańcucha porównujemy ze zmienną ma xLength i jeżeli dana wartość jest od niej większa, to zmienna maxLength przyjmuje tę właśnie wartość .
Tworzenie łańcucha St ri ng format A
~
formatującego
jest bardzo proste:
formatSt rI + (maxLengt h+1) + formatSt r2;
252
Visual C++ 2005. Od podstaw Do zmiennej maxLengt h musimy dodać l w celu utworzenia dodatkowego pola, kiedy wyświe tlany jest naj dłuższy łańcuch . Umieszczenie wyrażenia maxLength + l w nawiasach daje gwarancję, że zostanie ono obliczone jako wyrażenie arytmetyczne przed operacją łączenia łańcuchów.
Na
zakończenie wysyłamy
na
wyjście wartości
z tablicy za
pomocą łańcucha
format:
int numberPerLine ~ 3; for(int i = O ; i< va l ues->Lengt h : i++) {
Conso le : ;Write (format, val ues l t j):
i f « i+l )%numberPerLine == O) Consol e; ;WriteLine( ): Instrukcja wyjściowa w pętli używa łańcucha forma t jako łańcucha do wysyłania. Dzięki zmiennej maxLengt h w łańcuchu format dane są umieszczone w kolumnach o szerokości o jeden większej niż długość naj dłuższej z wysyłanych wartości . Zmienna numberPerl i ne określa, ile wartości pojawia się w jednym wierszu, dzięki czemu pętla jest dość elastyczna, gdyż pozwala na zmianę liczby kolumn poprzez zmianę wartości zmiennej numberPerli ne.
Modyfikowanie łańcuchów Najczęściej spotykaną operacją
iz
tyłu .
Do tego celu
służy
na łańcuchach jest obcinanie spacji funkcja Tr i m():
Str ing st r = {" Nie szat a zdobi Str ing newStr = st r->Trim(): A
cz łowi e k a. "
znajdujących się
z przodu
"l ;
A
Funkcja Tri m( ) w drugiej instrukcji usuwa wszystkie spacje z przodu i z tyłu łańcucha st r i zwraca wynik w postaci nowego obiektu klasy St ri ng przechowywanego w zmiennej newStr. Oczywiście, jeżeli nie chcemy zachowywać oryginalnego łańcucha, możemy wynik zapisać z powrotem do zmiennej st r. Istnieje także inna wersja funkcji Tri m(), która pozwala na określenie znaków do usunięcia z początku i końca łańcucha . Funkcja ta jest bardzo elastyczna, ponieważ umożliwia określe nie znaków do usunięcia na więcej niż jeden sposób. Znaki te można zapisać do tablicy i uchwyt do niej przekazać jako argument do funkcji :
Str ing toBeTrimmed = L " we ł n a we łna owca owca w eł n a we łna wełna" :
array<wchar_t / notWanted = {L 'w' ,L 'e ' .rr. L' n'. ia '. L' ' l : Console ; :Wr iteLine(to BeTrimmed->Trim(not Wanted» : A
mamy łańcuch o nazwie t oBeTri mmed, który zawiera owcę "przyTablica znaków do usunięcia z łańcucha została zdefiniowana pod nazwą notWante d, a więc przekazanie jej do funkcji Tri m( ) zastosowanej dla łańcucha spowoduje usunięcie z jego końca i początku wszystkich znaków w niej podanych. Pamiętaj, że obiekty klasy Stri ng są stałe , a więc oryginalny łańcuch nie zostanie w żaden sposób zmieniony - w wy. niku działania funkcji ' Tr i m( ) tworzony jest nowy łańcuch, który jest następnie zwracany. Wykonanie powyższego fragmentu kodu da następujący rezultat: W
powyższym przykładzie
krytą" wełną.
owca owca
Rozdzial4.• Tablice, łańcuchy znaków i wskaźniki
253
Jeżeli literały znakowe podalibyśmy bez towarzy szącego im przedrostka L, to byłyby one typu char (który odpowiada typowi klasy wartości SByte). Mimo to kompilator sam zadba o ich konwersję do typu wcha r_t .
Znaki do usunięcia przez funkcję Tri m() można także podać wprost jako argumenty tej funkcji. W związku z tym ostatni wiersz poprzedniego fragmentu kodu moglibyśmy zapisać następująco : Console: :W riteLineCtoBeTrimmed->TrimCL 'w' . i :«:
.L'ł'.
L'n '. L'a '. L' ' l ) ;
Kod ten da taki sam wynik jak poprzednia wersja tej instrukcji. Liczba argumentów typu wcha r_t jest dowolna, choć jeśli jest ich bardzo dużo , to lepiej zdefiniować je w tablicy . Jeżeli
chcemy usunąć znaki tylko z jednej strony łańcucha, to możemy użyć funkcji Tri mEnd() lub Tri mSta rtO. Funkcje te występują w takich samych wersjach jak funkcja Tr imt ), a więc jeżeli nie podamy żadnych argumentów, to usunięte zostaną spacje. Jeżeli jako argument podamy uchwyt do tablicy , to usunięte zostaną znaki w niej zdefiniowane. Znaki do usunięcia można również podać wprost jako argumenty typu wchar_t funkcji . Działaniem przeciwnym do usuwania znaków z łańcucha jest jego dopełnianie z obu stron spacjami lub innymi znakami. Dostępne są funkcje PadLeft O i PadRi ght O, które dopełniają łańcuch odpowiednio z lewej i prawej strony . Głównym zastosowaniem tych funkcji jest formatowanie wysyłanych na wyjście danych, gdy chcemy je wyrównać do prawej lub lewej strony w polach o ustalonej szerokości . Prostsze wersje funkcji PadLeft () oraz PadRi ght () akceptują pojedyncze argumenty określające długość łańcucha powstałego w wyniku operacji . Na przykład :
St ring value = L"3.142 " : St ri nq" l eftPadded ~ val ue->PadLeft 00) ; II Wynik lo" 3. 142 ". St ri ng r i ghtPadded ~ val ue->PadRi ght 00): II Wynik lo "3.142 n A
A
Jeżeli długość łańcucha,
oryginalnego
łańcucha,
podana jako argument funkcji, jest równa lub mniejsza niż długość to obie funkcje zwrócą nowy obiekt klasy Stri ng identyczny z ory-
ginałem .
Aby
dopełnić łańcuch
Poniżej
znajduje
St ring
A
value
się
znakiem innym niż spacja, należy go podać jako drugi argument funkcji. kilka przykładów takiego dopełniania :
L"3 .142": = val ue->PadLeftOO. L' * '); II Wynik lo "*****3.142". r i ghtPadded = value-> PadRightOO, L'# '): II Wynik lo "3.142##### ". ~
Strinq" leftPadded
St ring
A
Oczywiście w każdym z powyższych przykładów moglibyśmy zapisać powstały łańcuch z powrotem do uchwytu do oryginalnego łańcucha, co spowodowałoby usunięcie oryginalnego łańcucha.
W klasie Stri ng dostępne są także funkcje ToUpper() oraz ToLower() zamieniające wszystkie litery w łańcuchu na wielkie lub małe . Spójrzmy na przykładowy kod z ich wykorzystaniem : St r tnq" proverb ~ L "~.~ dwi e głowy. to nie je dna. ": St ring upper ~ prover b-c-Tolo oer j ): II Wynik: "CO DWIE GŁOWY, TO NIE JEDNA ". A
254
Visual C++ 2005. Od podstaw Funkcja t oUpper ( ) zwraca nowy literami wielkimi . Funkcji I nser t() łańcuchu. Poniżej
używamy do
znajduje
łańcuch,
który jest
kopią
oryginalnego, ale z wszystkimi
wstawiania łańcuchów w określone miejsce w zastosowania tej funkcji :
istniejącym j uż
się przykład
String proverb = L" Co dwie głowy. to nie jedna . '" Str ing newProverb = proverb ->Insert( B. L" mą d r e ") : A
A
Funkcja ta wstawia łańcuch podany jako drugi argument w miejscu, którego początek zostal w starym łańcuchu przez pierwszy argument. W wyniku działania tego kodu powstanie nowy łańcuch : określony
Co dwie
mą d r e g łowy.
to nie jedna.
Mo żna także
wszystkie wystąpienia jednego znaku zastąpić innym znakiem lub wszystkie jednego fragmentu łańcucha zastąpić innym fragmentem . W poniższym przykła dzie wykonywane są oba rodzaje operacji :
wystąpienia
Str ing proverb = L" Co dwie głowy . to nie jedna.": Console: :WriteLine(prove rb->Replace( L' '. L'*' » : Console : :WriteLi ne(proverb->Replace(L"Co dwie". L"Co t rzy"»: A
Wykonanie powyższego fragmentu kodu da
następujący
rezultat:
Co*dw ie*głowy *to * ni e* je dna.
Co t rzy gł owy. t o nie j edna. Pierwszy argument funkcji Repl ace() określa znak lub fragment łańcucha, który ma zastąpiony, a drugi argument to, co ma zostać wstawione w zamian .
zostać
Przeszukiwanie łańcuchów Jedną
z najprostszych operacji przeszukiwania łańcucha jest sprawdzenie, czy na jego końcu lub początku znajduje si ę określony fragment łańcucha . Służą do tego funkcje Start sWit h() oraz EndsW i th ( ). Do każdej z tych funkcji należy przekazać uchwyt do poszukiwanego łań cucha. Funkcje zwrócą wartość logiczną określającą, czy łańcuch został odnaleziony , czy nie. Poniżej znajduje się przykład użycia funkcji St ar t sW ith():
String sentence = L"Krowy to m ił e z w i erzę t a . ":
i f( sentence ->Sta rtsWi t h(L"Krowy"» Consol e: :WriteLine( "Zdanie rozpoczyna się s ło wem ' Krowy' ."): A
Wykonanie powyższego fragmentu kodu da następujący rezultat:
Zdanie rozpoczyna Oczywiście
si ę s łowem
do tego samego
' Krowy' .
łańcucha możemy zastosować funkcję
Console: :WriteLi ne( "Zdan i e {Oj
k o ń c zy si ę słowem
EndsWi t h( ) :
' zw i e r z ę t a '. ".
sentence->EndsWith(L"zwi e rz ę t a " ) ? L"" : L" nie"):
Rozdział 4.•
Tablice, łańcuchy znaków i wskaźniki
255
Do łańcucha wyjściowego wstawiony został wynik wyrażenia z u życiem operatora warunkowego. Jeżeli funkcja EndsWit h( ) zwróci warto ść true, to wstawiony zostanie pusty łańcuch , a jeżeli fa l se, to wstawiony zostanie łańcuch ni e. W tym przypadku funkcja zwróci wartość fa l se (ponieważ na końcu łań cucha znajduje s i ę jeszcze kropka) . Funkcja IndexOf( ) przeszukuje łańcuch w celu odnalezienia pierwszego wystąpienia określo nego znaku lub łańcucha oraz zwraca indeks, jeżeli go odnajdzie, lub - I w przeciwnym przypadku . Poszukiwany znak lub łańcuch określamy jako argument funkcji . Na przykład :
String sentence = L"Krowy to faj ne zw i er z ęt a . " : i nt ePos it ion = sent ence ->IndexOf< L'w' ): II Zwraca 3. int thePosition = sentence- >IndexOf
Pierwsze wyszukiwanie dotyczy litery w, a drugie IndexOf ( ) podane zostały w komentarzach.
słowa
t o. Wartości zwrócone przez
funkcję
Częściej jednak chcemy znaleźć wszystkie miejsca wystąpienia danego znaku lub łańcucha . Do tego celu służy inna wersja funkcji IndexOf ( ), której można używać wielokrotnie. W tym przypadku podajemy drugi argument, określający miejsce, od którego ma s ię rozpocząć poszukiwanie. Poniżej znajduje się przykład użycia tej funkcji:
Str i nq" words = "weł na weł n a owca owca weł n a weł na String word = " we ł n a " ; int index = Q: int count = Q: while ((index = words->IndexOf (word.index)) >= Q)
we ł n a" :
A
{
i ndex += word->Length ; ++count : } Cons o l e :; W riteLin e( L "S ł owo
' {Q}'
zosta ło
znalezione {l} razy w:\ n{2}". word. count.
words) ; Powyższy fragment kodu liczy, ile razy w łańcuchu words wystąpiło słowo weł na. Operacja poszukiwania znajduje się w warunku pętli whi l e, a wynik przechowywany jest w zmiennej i ndex. Pętla powtarza się , dopóki zmienna i ndex nie ma wartości ujemnej, czyli aż do momentu, kiedy funkcja IndexOf () zwróci -l . Wartość zmiennej i ndex zw iększana jest wewnątrz ciała pętli o długość słowa wo rd, co powoduje przesunięcie pozycji indeksu do znaku znajdującego się po znalezionym słowie i pętla gotowa jest do nowej iteracji. Zmienna count wewnątrz pętli jest zwiększana za każdym razem, kied y odnajdywane jest szukane słowo , dzięki czemu przechowuje ona liczbę odnalezionych słów word w łańcuchu words. Wykonanie powyższego fragmentu kodu da następujący rezultat: S łowo 'weł n a ' zosta ło weł n a wełna
owca owca
zna lezione 5 razy w: we łn a we łna wełn a
Funkcja LastlndexOf ( ) jest podobna do funkcji IndexOf ( ), z tym wyjątkiem , ż e rozpoczyna przeszukiwanie od tyłu lub wstecz od okre ślonego indeksu. Poniższy kod wykonuje tę samą czynność co poprzedni za pomocą funkcj i LastI ndexOf ( ):
int i ndex = words->Length - l : int count = Q; whil e(index >= Q && (i ndex ~ words->La st lndexOf (word.index)) >= Q)
256
Visual C++ 2005. Od podslaw
--in dex: ++count : Przy użyciu tych samych łańcuchów word i wo r ds co wcześniej , kod da identyczny rezultat. Jako że funkcja LastI ndexOf( l przeszukuje wstecz , to indeks , od którego ma zacząć przeszukiwanie, określa ostatni znak łańcucha - wo rds->Length-1. Kiedy zostaje znalezione słowo word, zmniejszamy i ndex o jeden, dzięki czemu następne poszukiwanie rozpocznie się od znaku poprzedzającego bieżące słowo wo rd. Jeżeli poszukiwane słowo znajduje się na samym początku łańcucha wo rds (w pozycji indeksowej O), zmniejszenie wartości zmiennej i ndex spowoduje ustawienie jej na wartość -1. Taka wartość nie jest prawidłowym argumentem funkcji LastI ndexOf, ponieważ przeszukiwanie zawsze musi rozpoczynać się od jakiegoś miejsca w łańcuchu . Dodatkowe sprawdzanie wartości ujemnych zmiennej i ndex w warunku pętli zapobiega wystąpieniu takiej sytuacji . Jeżeli prawy operand operatora && ma wartość f al se, to wartość lewego nie jest sprawdzana. Ostatnia funkcja przeszukiwania , o której chcę napisać, to funkcja IndexOfAny(). Przeszukuje ona łańcuch w celu odnalezienia pierwszego miejsca wystąpienia jakiegokolwiek znaku w tablicy typu array<wchar_t >, którą podajemy jako argument. Podobnie jak funkcja I ndexOf ( l, funkcja IndexOfAny( l może rozpocząć przeszukiwanie łańcucha od samego początku lub od określonego miejsca. Poniżej znajduje się przykładowy program z wykorzystaniem funkcji IndexOfAny ( l .
RImmJI Poszukiwanie jednego zkilku znaków . Poniższy program poszukuje znaków interpunkcyjnych w
ciągu :
#include "st dafx.h" using namespace System: int main(ar ray<System: :String A> Aargs) {
array<wchar_t / punctuation = {L "". L'v'. L'. ' , L' , ' , L' :'. L': ' , L' ! ', L'? ' j; StringA sentence ~ L"\ "Zimno tu\ ", chłodno powied ziała matka do synka.": II Utwórz
tablicę
spa cji o takiej samej
długości jak zdani e.
array<wcha r_t>A indicators = gcnew ar ray<wcha r_t>(sentence->Length){L' 'j; i nt i ndex ~ O: II Liczba znalezionych znaków . i nt count ~ O: II Liczba znaków interpunkcyjnych. whi le((index = sentence->IndexOfAny(punctuation, index)) >= O) {
indicators[index] = L' A' ; ++index; ++count:
II Ustaw znacznik. II Zwiększ do następnego znaku. II Zwiększ licznik.
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
257
Console : :WriteL i ne(L"W ła ń c u c hu s ą {O} znaki inte rpunkcyj ne:" , count): Consol e: :WriteL i ner L" \n{O}\n{l }", sentence. gcnew Stri ng (i ndicators )) ; ret urn O; Wyn ik działania powyższego programu powinien
być następujący:
Wł a ńc uch u są 4 znaki i nterpunkcyjne : "Zimno tu", chło d no powie dzi ał a matka do synka, A
Jak to działa Najpierw tworzymy
tablicę
znaków do odnalezienia oraz łańcuch, który chcemy
przeszukać :
array<wchar_t / punctuatio n ~ {L"" , L'\", L' , ', L' ,' , L' ; ' , L';' , L' ! ' , L'? '}; Stri ng A sentence = L"\ "Zimno tu\ ", ch ło d no powiedzi a ł a matka do synk a,"; Zauważmy , że
znak pojedynczego cudzysłowu musieliśmy zapisać za pomocą kodu znaku specjalnego, gdyż pełni on rolę ogranicznika literałów znakowych. W literałach znakowych możemy stosować podwójne cudzysłowy bez kodowania , ponieważ nie ma ryzyka, że w tym kontekście zostaną one zinterpretowane jako znaki oddzielające . Następnie
definiujemy tablicę znaków, której elementy zostały zainicjalizowane znakiem spacji:
array<wchar_t>A i ndicators = gcnew arr ay<wcha r_t >(sent ence->Lengt h){L , ' }; Tablica ta ma tyle elementów, ile znaków jest w łańcuchu sent ence. Tablicy tej w danych wyjściowych będziemy używać do zaznaczania, gdzie w łańcuchu sent ence znajdują się znaki interpunkcyjne. Za każdym razem, gdy zostanie odnaleziony znak interpunkcyjny, odpowiedni element tablicy przyjmuje wartość Zauważ, że pojedynczy inicjalizator umieszczony w nawiasach klamrowych po specyfikacji tablicy może być używany do inicjalizowania wszystkich elementów tablicy . A .
Poszukiwanie odbywa
się
w pętli whi l e:
while((i ndex = sente nce ->IndexOfAny(punctuation , i ndex)) >= O) {
indicators[i ndex] = L' A'; ++i ndex; ++count ;
II Ustaw znacznik. II Zwiększ do następn ego znaku. II Zwiększ licznik.
Warunek pętli jest w zasadzie taki sam jak w poprzednich fragmentach kodu. Wewnątrz pętli elementu znajdującego się w miejscu i ndex w tablicy i ndi cat ors jest zmieniana na przed zw i ększen i em indeksu przed następną iteracją. Po zakończeniu pętli zmienna count zawiera liczbę znalezionych znaków interpunkcyjnych, a tablica i ndi cat ors zawiera znaki w tych miejscach, gdzie zostały one znalezione.
wartość
A
A
Dane na wyjśc ie
wysyłane są za pomocą następujących
instrukcji :
Console : :Wri t eLine(L" Wł a ńcuc hu są {O} zna ki i nte rpunkcyj ne:", count) ; Console; ;Wri t eLinerL"\n{O} \ n{ l }" sente nce , gcnew St ri ng(i nd i cators )) ;
258
Visual C++ 2005. Od podstaw Druga z powyższych instrukcji tworzy nowy obiekt klasy St ri ng na stercie z tablicy i ndicatars poprzez przekazanie tablicy do konstruktora klasy St ri nq. Konstruktor klasy to funkcja tworząca nowy obiekt klasy w momencie jego wywołania. Więcej na temat konstruktorów dowiesz się , kiedy dojdziemy do definiowania własnych klas .
Relerencie Śledzące Referencje śledzące są podobne do referencji w natywnym c ++ pod tym względem , że stanowią alias czego ś znajdującego się na stercie. Można je tworzyć do typów wartości na stosie i do uchwytów na oczyszczonej stercie. Same referencje śledzące zawsze tworzone są na stosie, a ich zawartość jest automatycznie uaktualniana, gdy obiekt przez nie wskazywany zostanie przesunięty przez mechanizm usuwający nieużytki. Referencję śledzącą definiujemy
śledząc ą do
typu
za
pomocą
operatora %.
Poniższy przykład
tworzy
referencj ę
wartości :
int value = lO: int% trac kValue = value: Druga z powyższych instrukcji definiuje referencję ś led zącą o nazwie t rackVa l ue do zmiennej va l ue, która zo stała utworzona na stosie. Za pomocą referencj i trackVal ue możemy teraz modyfikować zmienną va l ue:
trac kValue *= 5: Consol e: :WriteL i ne( va l ue): Jako że referencja śledząca trackVa l ue je st aliasem zmiennej va l ue, druga z strukcji zwróci warto ś ć 50.
powyższych
in-
Wskaźniki wewnętrzne Mimo że adresów przechowywanych przez uchwyty śledzące nie można używać w działaniach arytmetycznych, w języku C++/CLl istnieje rodz aj wskaźnika , który do tego celu może być używany . Nazywa się on wskaźnikiem wewnętrznym i definiuje się go za pomocą słowa kluczowego i nteri ar_pt r. Adres przez niego przechowywany może być w razie potrzebyautomatycznie aktualizowany przez system zbierający nieużytki. Wskaźnikiem wewnętrznym jest zawsze zmienna automatyczna o zasięgu lokalnym w funkcji . Poniżej znajduje się przykładowa definicja pierwszego elementu tablicy:
wskaźnika wewnętrznego zawierającego
adres
array<double>Adata = {l.5 . 3.5. 6.7. 4.2. 2.l }: interior ptr<doub le> pstart = &data [OJ: Obiekt wskazywany przez wskaźnik wewnętrzny podajemy w nawiasach trójkątnych po słowie kluczowym i nteri ar_pt r. W drugiej z powyższych instrukcji wskaźnik wewnętrzny inicjalizujemy adresem pierwszego elementu tablicy za pomocą operatora &, podobnie jak w przypadku wskaźników w natywnym C++. Jeżeli nie podamy wartości początkowej wskaźnika,
Rozdział 4.•
Tablice. łańcuchy znaków i wskaźniki
259
to domyślnie zostanie on zainicjalizowany wartością nu ll ptr. Pamięć dla tablicy jest zawsze przydzielana na stercie , a więc jest to sytuacja, w której system usuwania nieużytków może dostosować adres zawarty we wskaźniku wewnętrznym . Istnieją
typu wskaźnika wewnętrznego . Wskaźnik weadres obiektu klasy wartości na stosie lub adres uchwytu do obiektu na stercie CLR. Nie może zawierać adresu całego obiektu na stercie CLR. Wskaźnik wewnętrz ny może także wskazywać natywny obiekt klasy lub wskaźnik natywny. ograniczenia
dotyczące okreś lania
wnętrzny może zawierać
Wskaźnika wewnętrznego można również użyć
do przechowywania adres u obiektu klasy na stercie, takiego j ak np. elementu tab licy CLR . W ten sposób możemy utworzyć wskaźnik wewnętrzny przec h owuj ący adres uchwytu ś l edzące go do obiektu System: :Stri ng, ale nie możemy utworzyć wskaźnika wewnętrznego do przechowywania adresu samego obiektu klasy Stri ng. Na przykład : wartości ,
który jest
częścią obiektu
inte ri ar_pt r<St ri nq" > pst r-l : interiar ptr<Stri nq> pst r2;
II OK - wskaźn ik do uchwytu . II Nie skomp iluje s ię - wskaźn ik obiektu String.
Z zasto sowaniem wskaźnika wewnętrznego można wykonywać takie same działania arytmetyczne co ze wskaźnikami natywnymi. Można także użyć operatora inkrementacji lub dekrementacji w celu zmiany zawartego w nim adres u na poprzedni lub następny element. Moż na również dodawać i odejmować liczby całkowite oraz porównywać w skaźniki wewnętrzne . Spójrzmy na przykład z zasto sowaniem tego , co zosta ło opisane.
liIIlmmUI Tworzenie i używanie wskaźników wewnętrznych Poniższy program jest ćwiczeniem zastosowania liczbowymi i łań cuch am i .
wskaźników wewnętrznych
#include "st dafx.h" using namespace Syste m; int main(arr ay<Syst em ; ;St ring A> Aargs ) { II Uzyskaj
dos tęp
do elementów tablicy za pomocą
wskaźn ika .
arr ayA data = {1 .5. 3.5. 6.7. 4.2. 2.l} ; inter iar_ptr pstart = &da t a[O J; inte ria r_pt r pend ~ &data[dat a->Length lJ; dauble sum= 0.0; whi le (pstart <= pend) sum+= *pstart++ ; Can s a l e ; ;W rite L i n e( L"Łą cz na
suma elementów t ablicy data = {O} \ n". sum);
II W celu pokazania, żejes t to możliwe, uzyskaj II za pomocą wskaźn ika wewnętrznego.
dostęp
do
łań cuchó w
array<St ri ngA>Ast rings = { L"Ahaj . widać l ą d ' '' .
L"Wychyl kielich a'''.
z
warto ściami
260
Visual C++ 2005. Od podstaw L"Nie trzę ś S i ę l ".
L" Nie rzucaj s łów na wiatr l "
l: for(interior_ptr<String pstrings = &strings[OJ pst rings- &strings[OJ < st ri ngs->Lengt h Consol e; ;WriteL i ne(*pstri ngs l; A>
++pstringsl
return O; Rezultat
działania tego
programu jest
następujący:
Łączna sum a elementów tabli cy dat a = 18 Ahoj , wid a ć lą d!
W ychyl kie l icha l Nie t r z ęś S i ę l
Nie rzucaj s łów na wiatr !
Jak to działa Po utworzeniu tablicy data
zawierającej
elementy typu doubl e definiujemy dwa
wskaźniki
wewnętrzne :
interior_ptr<doub le> pstart = &dat a[OJ; interior_ptr<double> pend ~ &dat a[dat a->Lengt h - 1J; Pierwsza z powyższych instrukcji tworzy wskaźnik pstart do typu doubl e i inicjalizuje go adresem pierwszego elementu tablicy - data[O]. Wskaźnik pend został zainicjalizowany adresem ostatniego elementu tablicy - dat a[dat a->Lengt h - 1]. Jako że wyrażenie dat a->Length oznacza liczbę elementów tablicy, odjęcie jeden od jego wartości daje w wyniku indeks ostatniego jej elementu. Pętla
whi le oblicza sumę elementów tablicy:
whil eCpstart <= pendl sum+= *pstart ++; Pętla będzie się powtarzać tak długo, aż wskaźnik wewnętrzny pstart będzie zawierał adres nie większy od adresu we wskaźniku pend. Warunek pętli mogliśmy równie dobrze zapisać w następujący sposób: ! pstart > pend.
pst art zawiera adres pierwszego elementu tablicy. Warpierwszego elementu uzyskujemy poprzez wyłuskanie wskaźnika za pomocą wyrażeni a *pstart, a jego wynik dodajemy do zmiennej sum. Następnie adres we wskaźniku jest zwięk szany o jeden za pomocą operatora H . Przy ostatniej iteracji pętli wskaźnik pstart zawiera adres ostatniego elementu, czyli taki sam jak wskaźnik pend. W związku z tym zwiększenie wskaźnika pstart sprawia, że warunek pętli daje wynik fa l se, ponieważ wskaźnik pst art stał się większy niż pend. Po zakończeniu działania pętli wartość zmiennej sumwysłana zostaje na wyjście, dzięki czemu mamy potwierdzenie, że pętla whi l e działa tak, jak powinna. Na
początku działania pętli wskaźnik
tość
Rozdział 4.• Następnie
tworzymy
tablicę
czterech
Tablice. łańcuchy znaków i wskaźniki
261
łańcuchów :
array<Stri ng A>A stri ngs = { l "Ahoj , w id a ć ląd ! ",
l "Wychyl kielicha !", LO Nie trzęś Sięl ",
LONie rzucaj słów na wiatr !" }; Pętla
z tych łańcuchów do wiersza poleceń : forCi nterior_ptr<String A> pstrings ~ &st ri ngs[ O] ; pst ri ngs-&strings [O] < stri ngs->length , ++pstrings) Consol e; :Wri tel i neC*pstri ngs); fo r
wysyła każdy
Pierwsze wyrażenie w warunku pętli for deklaruje wskaźnik wewnętrzny pst ri ngs i inicjaIizuje go adresem pierwszego elementu tablicy st ri ngs. Drugie wyrażenie , które decyduje , czy pętla kontynuuje dz iałanie , to: pstri ngs-&strings[O] < stri ngs ->length
Dopóki wskaźnik pst ri ngs zawiera adres prawidłowego elementu tablicy, różnica pomiędzy tym adresem a adresem pierwszego elementu w tablicy jest mniejs za ni ż liczba elementów w tablicy zwrócona przez wyrażen ie st ri ngs- >Length. Kiedy różnica ta jest równa liczbie elementów w tablicy, działanie pętli zostaje zakoń czone . Z danych wyjściowych wynika, że wszystko działa zgodnie z przewidywaniami. Wskaźników wewnętrznych naj c zęściej używa s i ę
obiektów na stercie CLR. Więcej na ten temat
do wskazywania obiektów, które są c zęścią w dalszej częśc i ks iążk i .
będz iemy mówić
Podsumowanie Znamy już wszystkie podstawowe typy wartości w C++, potrafimy tworzyć tablice tych typów i ich używać oraz tworzyć wskaźn ik i i z nich korzystać . Wspomnieli śmy także o referencjach. Oczywiście , nie powiedzieliśmy jeszcze wszystkiego na te tematy. Do tematów tablic, wskaźników i referencji wrócimy jeszcze w dalszej części książki . Poniżej znajduje się lista najważniejszych zagadnień poruszonych w tym rozdziale : •
Dzięki
zbiorem danych tego samego typu, posługując wymiar tablicy definiowany jest pomiędzy nawiasami kwadratowymi po nazwie tablicy w jej deklaracji . tablicom
możemy zarządzać
się prostą pojedynczą nazwą. Każdy
•
Każdy wymiar tablicy indeksowany jest od zera. W zwi ązku z tym tablicy ma indeks cztery .
piąty
•
Tablice można inicjalizować, wstaw i aj ąc w deklaracji umieszczone pom iędzy nawiasami klamrowymi .
•
Wskaźnik je st typem zmiennej , która zawiera adres innej zmiennej . Wskaźniki deklaruje się jako "wskaźn i k i do typu " i mo żna im przypisywać tylko adresy zmiennych o podanym typie.
element
wartości początkowe
262
Visual C++ 2005. Od podstaw •
Wskaźnik może wskazywać obiekt s tały . Można go ponownie przypisać do innego obiektu. Wskaźnik można również zdefiniować jako eonst i w takim przypadku nie można go ponownie przypisać .
•
Referencja jest aliasem zmiennej i może być używana w tych samychmiejscach co zmienna, którą wskazuje. Referencja musi zostać zainicjalizowana w momencie deklaracji .
•
Raz przypisanej referencji nie
•
Operator s i zeof zwraca liczbę bajtów zajmowanych przez obiekt podany jako jego argument. Jego argumentem może być zmienna lub nazwa typu otoczona nawiasami okrągłymi.
•
Operator new dynamicznie przydziela pamięć w wolnej przestrzeni w programach w natywnym C++. Po przydzieleniu pamięci zwraca wskaźnik do początku przydzielonego obszaru. Jeżeli pamięć z jakiegoś powodu nie może zostać przydz ielona, powstaje wyjątek i program zostaje zamknięty .
można przypisać
do innej zmiennej .
Mechanizm działania wskaźnika może być czasami trudny do zrozumienia, gdyż operuje on na różnych poziomach w jednym programie. Czasami działa jako adres, a czasami może działać z warto ściami przechowywanymi pod danym adresem . Bardzo ważne jest, aby dobrze zrozumieć i stotę tego mechanizmu, a więc mając jakiekolwiek problemy ze zrozumieniem sposobu działania w skaźników , należy przećwiczyć ich użycie na kilku przykładach , aby sprawnie się nimi posług iwać . Najważniejsze
zagadnienia, których
nauczyliśmy się
o programowaniu dla CLR, to:
•
W programach dla CLR pamięć przydzielana jest na oczyszczonej stercie za pomocą operatora genew.
•
Obiektom klasy referencji , a w szczególności obiektom klasy St ri ng pamięć
przydzielana jest zawsze na stercie CLR .
•
Pracując
•
CLR posiada własne typy tablicowe po siadające tablicowe w natywnym C++.
•
Tablice CLR zawsze
•
Uchwyt śledzący jest typem wskaźnika używanego do wskazywania zmiennych zdefiniowanych na stercie CLR. Uchwyt śledzący jest automatycznie aktualizowany, jeżeli to, do czego się odwołuje , zo stało przenie sione przez mechanizm usuwania
w programach CLR, używamy obiektów klasy Str i ng.
są tworzone
większą funkcjonalność niż
typy
na stercie CLR.
nieużytków .
•
Zmienne
odnoszące s i ę
do obiektów i tablic na stercie
są
zaw sze uchw ytami
śledzącymi .
•
Referencja śledząca podobna jest do referencji w natywnyrn C++, z tym wyjątkiem, że zawarty w niej adres jest automatycznie aktualizowany, j eż e l i obiekt przez nią wskazywany zostanie przeniesiony przez mechanizm usuwania nieużytków .
•
Wskaźnik wewnętrzny to typ wska źnika -C f-f-/Cl.L Można go stosować
do wykonywania tych samych operacji, które wykonuje się za pomocą
wskaźnika natywnego.
Rozdział 4.•
•
Tablice. łańcuchy znaków i wskaźniki
263
Adres zawarty we wskaźniku wewnętrznym można modyfikować za pomocą działań arytmetycznych i nadal utrzymać poprawny adres, nawet o dnosząc się do czego ś przechowywanego na sterc ie CLR .
Ćwiczenia Kod źródłowy wszystkich przykładów w tej książce oraz rozwiązania do ćwiczeń ze strony www.helion.pl.
można pobrać
1. Napisz program w natywn ym C++ pozwalający na podanie dowolnego zbioru liczb , które będą przechowywane w tab licy ulokowanej w obszarze pamięci wolnej . Program powinien wysyłać na wyjście po p i ę ć z podanych liczb, a na końcu podawać" ich średnią. Początkowo tablica powinna mieć rozmiar pięciu elementów. W razie potrzeby program powinien tworzyć nową tab licę z dodatkowymi pięcioma elementami oraz ko piować wartośc i ze starej tablicy do nowej .
2. Powtórz poprzednie
a.
Zadeklaruj
tab licę
pętli zamień
co
ćwiczenie,
ale z użyciem notacji
wskaźnikowej
znaków i zainicjalizuj ją do odpowiedniego na wie lką.
zamiast tablic.
łańcucha .
Za pomocą
drugą l i terę
Wskazówka: w zestawie znaków ASC II wartości wielkich liter są o 32 mniejsze niż ich małych odpowiedników .
4. Napisz program w C++/CLI tworzący tablicę zawierającą l osową l i czbę elementów typu i nt . Tablica powinna zawierać nie mniej niż 10 i nie więcej niż 20 elementów. Wartości elementów powinny być ta k że losowe i zawierać się w zbiorze od 100 do 1000. Zawartość eleme ntów wyświet l w porządku ma lejącym bez sortowania tablicy. Na przykład znaj dź najmniej szy element i go wyświet l, następnie kolejny najmniejszy itd.
Il. Napisz program w C++/CLI
generujący losową li czbę całkowitą większą od 10 000. na ekranie, a n astęp ni e wyświet l słowne odpowiedniki poszczególnych cyfr. Je że li na przy kład program wygenerował liczbę 345 678 , to wynik powinien być następujący : Wyświet l tę liczbę
Wartość w ygenerowa na to: 345678 t rzy cztery pi ęć s ześć sie dem osiem
8. Napisz program w C++/CLI, który stworzy tablicę
zawierającą następujące łańcuchy :
" Ko by ł a ma ma ł y bok ." "Kawa i wuzetka to zestaw obo wi ązko wy.
" Jeż leje lwa. paw leje l ż e j . "
"Ala ma kota. kot ma A lę. "
" P ę t a k a pętaj. a tępaka tęp . "
Program powinien przeanalizować po kolei wszystkie ł ań cu ch y i wyświetlić je z i nformacją, czy są one palindromami, czy nie (tzn. czy nie ma różnicy , czy s ię je czyta od przodu, czy od ty łu, pomij ając znaki interpu nkcyj ne).
264
Visual C++ 2005. Od podstaw
5 Wprowadzanie struktury do programu Do tej pory nie mieliśmy możliwości nadania naszym programom struktury modularnej, kod zawierał s i ę w pojedynczej funkcji mai n(), choć używali śmy j uż różnego rodzaju funkcj i bibliotecznych, a także funkcji należących do obiektów. Pisząc program w C++, należy za każdym razem od samego początku myśleć o jego modularnej budowie. Jak się niebawem przekon asz, dobre opanowanie technik imple mentacji funkcji jest niezbędne w programowaniu zorientowanym obiek towo w C++. W rozdziale tym nauczysz się:
ponieważ cały
•
Dekl arować
i pisać
własne
•
Definiować
argument y funkcji i ich
•
Przekazywać
tablice do i z funkcji .
•
Przekazywać
przez
•
Jak
•
Jak używ a ć referencj i jako argumentów funkcji oraz co oznacza przekazywanie przez wartość .
•
Jaki wpływ ma modyfikator const na argumenty funkcji .
•
Jak
•
S tosować rekurencję .
funkcje w C++. używać .
wartość .
przekazywać wskaźn iki
zwracać wartości
do funkcji .
z funkcj i.
Tematyka struktury programów w C++ jest bardzo szeroka, a więc aby nie n abawić się niestrawności , nie będziemy próbowa li połknąć wszystkiego za jednym razem. Po zapoznaniu się z podstawowymi zagadnieniami przejdziemy do n a s tępne go ro zdziału , w którym zagłębimy się w tę tematykę jeszcze bardziej . .
266
Visual C++ 2005. Od podstaw
Zrozumieć
lunkcje
Spójrzmy najpi erw na ogó l ną zas adę dział an i a funkcji . Funkcja jest blokiem kodu przeznaczonymdo wyko nywania okreś lo nego zadania. Ma ona nazwę , która j ą identyfikuje, a zarazem s łuży do j ej wywo ływan ia w programie. Nazwa funk cji jest globalna, ale nie musi być unikalna, o czym przekonamy s ię w n astępnym rozdziale. Ogólnie mó wi ąc , funkcje wykonuj ąc e ró żn e czyn nośc i powinn y mi e ć inne nazwy. Nazew nictwem funkcji rządzą te same zasady co nazewnictwem zmiennych. Tak więc n azwę funkcji stanowi sekwencja liter i cyfr. Pierwsza musi być litera, z tym że znak podkreśl en i a ró wni eż uznawany jest za lit erę. Nazwa funkcji powinna o dzwie rc ie d lać jej przeznaczenie. Na przykł ad fu n kcję licząc ą fasolki m ożna by był o n azw ać l i cz_fasol ki (). Informacje do funkcji przekazujemy za pomocą argumentów podawanych przy wywoływani u funkcji . Argumenty mu s zą odpow i adać parametrom, które z naj d ują się w defini cji funkcji. Podane argumenty zas tęp ują parametry używane w definicji funkcji , kiedy jest ona wykonywana. Wtedy kod takiej funkcji wykon ywany jest w taki sposób, jakby został napisany przy u życiu naszych argumentów . Na rysunku 5.1 widać , jaki jest zw iązek p om iędzy argumentami wywo ła nia funkcji a param etrami w jej defini cji .
Rysunek 5.1
Argument y
(out « add _ints( 2 , 3 );
II Warto ści
argum ent ów zastę p ują w definicji funkcji o d powi a dając e im paramet ry
lI
int add_ints( int i, int j ) Definiqa funkcji
'-----
-
Zwrócona
t
~
} T ----.. .,I return i + j ;
{
Parametry
I
wa rtość ~ 5
W powyższym przykładzi e funkcja zwraca sumę obu przekazanych do niej argumentów. Funkcja zwraca poj edynczą warto ś ć w miej scu, w którym zos tała wywołan a, lub nic nie zwraca, w zależności od tego, jak zos tała zdefiniowana. M oże s ię wydawać, że zwracanie pojedynczej wa rtośc i jest dużym ograniczeniem , ale wa rto ść ta może być na przykład ws kaźn i kiem zawieraj ącym adres tablicy. Więcej na temat zwracania danych z funkcji powiemy w dalszej częśc i tego rozdziału .
Rozdzial5.• Wprowadzanie struktury do programu
267
Do czego potrzebne są funkcje Jedną z głównych zalet funkcji jest to, że mogą one być wykonywane dowolną liczbę razy w różnych miejscach programu. Gdyby nie mo żliwo ść pakowania bloków kodu do funkcji , programy byłyby znacznie dłuższe, gdyż ten sam fragment kodu trzeba by było wielokrotnie powtarzać. Ale prawdziwym powodem tworzenia funkcji jest rozbicie programu na czę ści, którymi można z łatwości ą zarządzać , rozwijać je i testować . Wyobraźmy sobie naprawdę duży program - złożo ny z miliona wierszy kodu . Na pisanie programu takich rozmiarów bez funkcji byłoby niemożliwe. Funkcje pozwalają na tworzenie segmentów programu, a każda częś ć może być pisana i testowana oddzielnie przed połącze niem wszystkich w jedną cało ść . Pozwala to także na podzielenie pracy pomi ędzy kilka zespołów programistów , w których każdy programista odpowiada za ściśle okre ś loną czę ść projektu z dobrze zdefiniowanym funkcjonalnym interfejsem do reszty kod u.
Struktura funkcji Jak j uż
widzie liśm y, pisząc funkcję
ma i nO , funkcja składa się z nagłówka funkcji ją identypo którym na stępuje cia ło funkcji . Jest ono otoczone nawi asami klamrowymi i zawiera kod wykonywalny funkcji. Spójrzmy na przykład . Możemy napisać funkcję podnoszącą liczbę do danej potęgi, która oblicza wynik mnożenia liczby x przez siebie s a m ą n razy, to jest x", fi kującego,
II Funkcja podnoszą ca x do potęgi n. gdzie n j est większe lub równe O.
double powerC double x. int n)
li Nagłó wek fun kcj i .
{
II Cialo f unkcj i rozpo czyna s ię tutaj. .. II Wynik przechowywany jest tutaj .
double resul t ~ 1. O; for Ci nt i ~ 1; i <= n; i ++ ) result *= x: retu rn res ul t:
II Ciało funkcji kończy się tutaj.
Naglówek funkcji Przeanalizujmy nagłówek funkcji na przykładz ie . dou ble powe rCdouble x. int n) Składa się
on z trzech
Po ni żej
się
pierwszy wiersz funkcji.
II Nagłowek fu nkcj i.
części : wartości (w
tym przypadku doubl e),
•
typu zwracanej
•
nazwy funkcji (w tym przypadku powe r),_
•
parametrów funkcji pomiędzy nawiasami o typach odpowiednio doubl e oraz i nt ).
Wartość
znajduje
okrągłymi
(x i n w tym prz ypadku
zwracana jest przekazywana do funkcji wywołuj ącej po jej wykonaniu. A naszej funkcji w wyrażeniu, w którym się pojawia, da wynik typu doubl e.
wywo łanie
więc
268
Visual C++ 2005. Od podstaw Nasz a funkcja ma dwa param etry: x - liczba typu doubl e, któ rą chcemy podn i e ś ć do danej pot ę gi, oraz w arto ś ć potę gi n, która jest li czbą całkow i tą. Obl iczen ia wykonywane przez tę funkcję zapisane są przy u życiu tych zmie nnych parametrowy ch razem z i n ną zm ien ną res ul t zadeklarowaną w ciele funkcji . Nazwy parametrów oraz wszelkie zmienne zdefiniowane w ciele funkcj i maj ą w niej zasięg lokalny. Zauważ, że
na
ko ń cu
naglówka f unkcj i lub po
zamykającej
klamrze ciala f unkcj i nie ma
srednika.
Dgólna struktura naglówka lunkcii Ogó l n ą s truk tu rę n agłó wk a
funkcji
może my za p isać n a st ępuj ąc o :
zwracany_typ nazwa_funkcj7(li sta-parametrów) Typ zwracany przez fu nkcję m oż e być dowolnym praw i dłowym typem. Jeż eli funkcja nie zwraca żadnej wartości , to j ej typ okreś la się s łowe m kluczowym voi d. Słowo to używ ane jest równi eż do informowa nia o braku parametrów . Tak więc funkcja n i epo si ad aj ąca żadnych parametrów i n i e zwracaj ąc a żad nyc h wartości może mi eć n astępuj ąc y n a gł ów e k :
voi d moja_funkcja(void) Pusta lista parametrów oznacza równ ież, że funkcja nie przyjmuje kluczowe voi d w nawiasach m ożem y po mi nąć:
żadnych
argumentów, a więc
słowo
void moja_funkcja() Funkcja z type m zwracanym okreś lonym jako void nie powinna być używan a w wyraprogramie wywolującym . Jako że nie zwraca ona wartości, nie m oże być częścią żadnego wyrażenia. Jeż eli j ednak użyjemy jej w taki sposób, to kompila tor zglosi komunikat o b lędzie . żeniu w
Ciało funkcii Obliczenia, które wykonuje funkcja, wykonywane są przez instrukcje zawarte w jej ciele znajdującym się po nagłówku . Pierwsza z tych instrukcji w naszym przykładzie deklaruje zmienną result , która jes t inicj alizowana wartośc ią l .O. Zmienna re sult ma zasięg lokalny w funkcji, jak wszystkie deklarowane w jej ciele zmienne automatycz ne. Oznacza to, że zmienna result przestaje istn ieć w momencie zakończen ia funkcji. Skoro zmienna przestaje istnieć natychmiast po zakończeniu wykonywania funkcji, to jak jest ona zwraca na? Po prostu automatycznie tworzona jest kopia zwracanej wartości, d o stępna w miejscu, w którym program zwraca wartość. Ob liczen ia wykonywa ne są w pę t li f or . Ko ntro lna zm ienna pęt lowa i jest zadek larowana w p ętl i f or, która przybiera kolejne wartości od l do n. Zmienna result mno żon a jes t przez x jeden raz przy każdym powtórzeniu pętli , a więc wygenerowanie żąd anej wartości wymaga n powtó rzeń . Jeżeli n równe jest O, to wyrażenie w pętl i nie zostanie wykonane ani razu, ponieważ warunek kontynuacji nie zostanie spełniony, a w ięc wartość zmiennej resu lt pozostanie bez zmian, czy li l . O.
Rozdział 5.•
WprowalJzanie struktury do programu
269
Jak już mówiłem, parametry i wszystkie zmienne deklarowane w ciele funkcji mają w niej zasięg lokalny. Nie ma żadnych przeszkód, by używać tych samych nazw zmiennych służą cych do czegoś innego w różnych funkcjach. Jest to możliwe ze względu na fakt, że wymyślenie unikalnych nazw dla wszystkich zmiennych w rozbudowanych programach byłoby bardzo trudn e, zwłaszcza jeśli nazwy zmiennych nie były pisane przez jedną osobę. Zasięg
zmiennych deklarowanych wewnątrz funkcji określany jest w taki sam sposób, jak Zmienna tworzona jest w miejscu jej zdefiniowania i przestaje istnieć wraz z zakończeniem bloku ją zawierającego. Istnieje jeden typ zmiennych, który jest wyjątkiem od tej reguły - zmienne zadeklarowane jako typ st at i c. Więcej na temat zmiennych statycznych powiem w dalszej części rozdziału.
już mówiłem.
Należy uważać
przy maskowaniu zmiennych globalnych zmiennymi lokalnymi o tej samej nazwie. Pierwszy raz z taką sytuacją spotkaliśmy się w rozdziale 2., gdzie uczyliśmy się używać operatora zasięgu : : do uzyskiwania dostępu do zmiennych globalnych.
Instrukcia return Instrukcja retu rn zwraca wartość zmiennej result w miejscu, gdzie została wywołana funkcja. Ogólna forma instrukcji r eturn przedstawia się następująco : ret urn wyrazen7 e : Wartość wyrażenia musi być tego samego typu, jaki został określony w nagłówku funkcji dla wartości zwracanej. Wyrażeniem może być dowolne wyrażenie, jeżeli tylko jego wynik jest takiego samego typu jak żądany. Może ono zawierać wywołanie funkcji - nawet tej samej, w której się znajduje . Przekonamy się o tym w dalszej części rozdziału . Jeżeli
typem zwracanym przez zapisujemy po prostu:
funkcję
jest typ voi d, to wyrażenie musi
być
puste.
Instrukcję
ret urn:
Używanie funkcji W momencie, w którym używamy funkcji w programie, kompilator musi coś o niej wiedzieć w celu skompilowania jej wywołania. Potrzebuje on informacji identyfikujących funkcję oraz sprawdzających, czy jest ona używana zgodnie z zasadami. Jeżeli definicja funkcji, której chcemy użyć, nie znajduje się nigdzie wcześniej w tym samym pliku źródłowym, musimy ją zadeklarować za pomocą wyrażenia zwanego prototypem funkcji.
Prototypy lunkcii Prototyp funkcji dostarcza podstawowych informacji, których potrzebuje kompilator do sprawdzenia, czy funkcja jest używana zgodnie z zasadami. Określa się w nim parametry funkcji, jej nazwę oraz typ zwracanej wartości - w zasadzie prototyp zawiera te same informacje co
270
Visual C++ 2005. Od podstaw nagłówek być
funkcji z dodatkiem ś re dn i ka . Oczywi ście liczba parametrów oraz ich typy takie same zarówno w prototypie funkcji, jak i w nagłówku w defini cji funkcj i.
mu szą
Prototyp funkcji wyw o ływa n ej wewnątrz innej funkcji mus i znajdować s i ę przed instrukcjami odpowiedzialnym i za to wywołanie i zazwyczaj jest umieszczany na samym poc zątku pliku źródłowego programu . Pliki nagłówkowe dołączane w celu korzystania z funkcji biblioteki standardowej zaw ierają między innym i prototypy funkcji w niej dostępny ch. Prototyp funkcji power ()
double power (doub le
może wyglądać następująco:
wart o ś ć .
i nt i ndeks) :
Nie zapo mnij, że na końcu prototypu funkcji wymagany jest średnik. Bez niego kompilator komunikat o błędzie .
zgłos i
Zauważ , że
nazwy parametrów, które podałem w prototypie funkcji, są inne niż te, których w nagłówku funkcji podczas jej definiowania. Zrobiłem to tylko po to, aby pokazać , że jest to możliwe . Zazwyczaj zarówno w prototypie funkcji, jak i w jej nagłówku w definicji funkcji używa się tych samych nazw, ale nie jest to konieczne. W prototypie funkcji można używać dłuższych , bardziej opisowych nazw parametrów, aby było łatwiej zrozumieć ich znaczenie, a w definicji funkcji nazw króts zych, ponieważ gdyby były zbyt długie, to kod ciała funkcj i stałby się trudny do odczytania . użyłem
Jeśli
chcemy, to
możemy
nawet
całkowicie pominąć
nazwy w prototypie i napisać tylko:
double power( double. tnt i: Takie informacje w zupełności wystarczą kompilatorowi , choć dobrym zwyczajem jest stosowanie nazw w prototypach funkcj i, jako że poprawiają one czytelność kodu oraz, w niektórych przypadkach , sprawi aj ą, że całkow ic ie niezrozumiały kod staje się łatwy do odczytania. Jeżeli mamy funkcję z dwoma parametrami tego samego typu (przypuśćmy, że w naszej funkcji power ( ) zmienna inde x też była typu doubl e), użycie odpowiednich nazw pozwala zorientować się , który parametr jest pierws zy, a który drugi .
R!lmmjI Stosowanie funkcji Omawiane powyżej zagadnienia cji powe r().
przedstawię
teraz na
przykładzie
II Cw5_01.cpp II Deklarowanie. defi niowani e oraz stoso wanie funkcji.
#include usi ng std : :cout: usi ng st d: :endl: doubl e power( double x. i nt nI:
II Prototyp funkcji.
int main (void) {
int i ndex = 3: doubl e x = 3.0: doub le y = 0.0:
II Podni esienie do podanej potęgi.
II x innego typu niż w funkcji power.
programu z
użyciem
funk-
Rozdział
5.• Wprowadzanie strukturv do programu
271
y = power (5.O. 3); II Przekazywanie s tałych j ako argumentów. cout « endl
« "5.0 do p otęg i trzeciej = " « y:
cout « endl « "3 .0 do potę gi trzecie j = " « power( 3. O, i ndex) : II Wysyłanie na
wyjście
zwróconej wartoś ci .
x = power tx. power (2. O. 2. O)) ; II Użyc ie funkcji j ako argumentu cout « endl II z autokonwersją drugiego parametru. «
"x
~
" « x;
cout « endl ; ret urn O;
Powyższy
program prezentuje kilka sposobów użycia funkcji power t ), określając jej argu menty na kilka sposobów. Po uruchomieniu tego programu otrzymamy następujący rezultat: 5, 0 do po t ęgi trzeci ej 3, 0 do pot ęgi trzeci ej x = 81
~
=
125
27
Jak lo działa Po dyrektywie #i ncl ude dla instrukcji wejścia-wyjścia i deklaracji us i ng mamy prototyp funk cji power( ). Gdybyśmy go usunęli i spróbowali ponownie skompilować program, to kompi lator nie mógłby przetworzyć wywołań tej funkcji wewnątrz funkcji main O i wygenerowałby całą masę komunikatów o błędach: er ror C3861; .power , : ident i f ier not found error C2365: 'power' : redef i nit ion: previous defi nit ion was ' forme r ly unknown ident H i er ' Zmianą w stosunku do poprzednich przykładów jest użycie nowego słowa kluczowego void jako parametru funkcji mai n( ) w miejscu, gdzie normalnie powinna pojawić się lista parame trów. Słowo to oznacza, że żadne parametry nie zostaną podane. Wcześniej zostawialiśmy miejsce pomiędzy nawiasami przeznaczone puste, co w C++ interpretowane jest jako de klaracja, że nie ma żadnych parametrów, ale lepiej jest fakt ten oznaczyć za pomocą słowa kluczowego voi d. Jak już widzieliśmy, słowo kluczowe void może być używane jako wartość zwracana funkcji w celu określenia, że żadne wartości nie będą zwracane. Jeżeli typ zwracany przez funkcję określimy jako void, to nie możemy umieszczać żadnych wartości w instrukcji ret ur n wewnątrz funkcji. W przeciwnym przypadku kompilator zgłosi komunikat o błędzie .
Z powyższych przykładów dowiedzieliśmy się, że stosowanie funkcji jest bardzo proste . Aby za pomocą funkcji power ( ) obliczyć wartość wyrażenia 5,03,a wynik zapisać w zmiennej y, wystarczy poniższa instrukcja: y = power(5.0, 3);
272
Visual C++ 2005. Od podstaw Wartości 5. Ooraz 3 są argumentami funkcji. Są one stałymi, ale jako argument można stosować dowolne wyrażenie, jeżeli tylko jego wartość jest spodziewanego przez funkcję typu. Argu menty funkcji power ( ) zostają wstawione w miejsce parametrów x i n, które zostały użyte w definicji funkcji . Obliczenia wykonywane są przy użyciu tych wartości , a kopia wyniku (125) zwrócona zostaje do funkcji wywołującej main O i następnie zapisana w zmiennej y. O funkcji tej możemy myśleć jak o mającej tę wartość w instrukcji lub wyrażeniu, w którym się ona pojawia. Następnie wysyłamy na wyjście zawartość zmiennej y:
cout
« «
end l
"5.0 do
potęgi
Następne wywołanie
cout
« « «
trzec i ej
~
" «
y :
funkcji zawiera się w instrukcji:
end l
"3.0 do po t ęg l trzecie j
powerC3 .0. index) ;
II
Wysyłanie
na
wyjście
zw róconej
war/ości.
W tym przypadku w artość zwrócona przez funkcję jest przesyłana bezpośrednio do stru mienia wyjściowego . Jako że zwróconej wartości nigdzie nie zapisaliśmy, jest ona niedostępna w żaden inny sposób . Pierwszym argumentem w tym wywołaniu funkcji jest stała, a drugim zmienna. N astępnie w instrukcj i tej
x
~
została użyta
power (x. power (2.0, 2.0)) :
funkcja power ( ): II
Użyc ie funk cji jako
argumentu,
Tutaj funkcja power( ) wywoływana jest dwa razy . Pierwsze jej wywołanie znajduje się po prawej stronie wyrażenia, a wynik jest przekazywany jako wartość drugiego argumentu wywo łania po lewej stronie. Mimo że typy argumentów w pod wyrażeniu power(2 . O, 2 . O) zostały określone jako literał typu doubl e 2,O, funkcja jest w rzeczywistości wywoływana z pierw szym argumentem w postaci 2. O i drugim w postaci literału reprezentującego liczbę całkowitą 2. Kompilator konwertuje wartość double podaną jako drugi argument do typu tnt, ponie waż z prototypu funkcji (jeszcze raz pokazanego poniżej) wie, że typ drugiego argumentu to właśnie 'j nt .
double powerC double x. l nt n):
II Prototyp funk cji.
Wynik w postaci liczby typu doubl e 4 . Ozostaje zwrócony przez pierwsze wywołanie funkcji power (), a po konwersji do typu i nt wartość 4 zostaje przekazana jako drugi argument w na st ępnym wywołaniu funkcji z x stanowiącym pierwszy argument. Jako że x ma wartość 3,0, obliczona zostaje wartość wyrażenia 3.0 4, a wynik zapisany w zmiennej x. Ta kolejność dzia łań została przedstawiona na rysunku 5.2. Instrukcja ta zawiera dwa niejawne rzutowania typu doubl e na typ i nt , które zostały wyko nane przez kompilator. Przy konwersji z typu doubl e na typ int istnieje możliwość utraty części danych, a więc kompilator zgłasza ostrzeżenie, gdy ma ona miejsce, mimo że sam ją zastosował. Poleganie na automatycznej konwersji typów, gdzie istnieje możliwość utraty danych, jest niebezpieczne, a z kodu nie wynika w sposób oczywisty, czy będzie ona miała miejsce. O wiele lepiej , jeżeli jest to konieczne, stosować konwersję jawną za pomocą ope ratora st at i c_cast. Instrukcję z przykładu o wiele lepiej byłoby zapisać następująco:
x = power Cx. st at ic castC powerC2.0 . 2)) ) ; '
Rozdzial5.• Wprowallzanie slruklury do programu Rysunek 5.2
J(
273
=power( x , power( 2.0 , 2.0 ));
I
Wa rto ść p oczątkowa
I
CD Przeko nwert owana
3.0
na typ int
1
@ Wynik z powr otem zapisany do zmiennej x
<J) power( 2.0 • 2 ) \~ 4.0 (ty p double)
I
® Przeko nwertow ana na typ in!
ł
® power( 3.0 • 4 ) '---y---I 81.0 (ty p double)
Instrukcja w takiej postaci pomaga unikn ąć os trzeże ń kompilatora, które pojawiały si ę w po przedniej wersji. Użycie rzutowania statycznego nie oznacza, że podczas konwersj i z jednego typu na inny nie zostaną utracone żadne dane. Ale skoro określil i śmy jąjawnie , oznacza to, że świadom i e chcemy jej d okon ać , wiedząc , że grozi nam utrata danych.
Przekazywanie argumentów do funkcji jest, aby rozum ie ć sposób , w j aki argum enty s ą przekazywan e do funkcji , ma to wpływ na to, jak je piszemy, oraz na to, jak one d ziałaj ą. Z cz yn n oś c i am i tymi zwi ązan ych jest kilka pułapek, a wi ę c przyjrzymy si ę temu mechan izmowi z bliska .
Bard zo
w a żne
poni eważ
Argumenty podawane do funkcji podczas jej wywoływani a zazwyczaj powinny odpowi adać typowi i kolejności, w jakiej zos tały podane parametry w jej definicji. Jak widzieli śmy wostat nim przykładzie , jeżel i typ argumentu podanego w wywołaniu funkcji nie odpowiada typowi parametrów w tej funkcji, to (je żeli jest to m ożliwe) są one konwertowane do odpowiedniego typu przy zachowaniu zasad obowiązujących przy rzutowan iu operandów, o którym mowa w rozdziale 2. Jeżeli okaże się to niemożliwe , kompilator zgło si błąd. Jednak nawet jeżeli jest to możliwe i kompilacja zakończy się pomyślnie, to zawsze narażamy się na n iebezpieczeń stwo utraty pewnej części danych (na przykład przy konwersji z typu l ong na typ short ) i dla tego powinni śmy unikać konwersji typów. W C++ dostępne są dwa mechanizmy słu ż ące do przekazyw ania argumentów do funkcji. Pierwszy z nich stosujemy, gdy parametry w definicji funkcji określamy jako zwykłe zmienne (nie referencje) . Metoda ta nazywa się przekazywaniem przez wartość danych do funkcji i zajmiem y s i ę niąjako p ierwszą.
274
Visual C++ 2005. Od podstaw
Mechanizm przekazywania przez wartość W przypadku tego mechanizmu zmienne lub stałe określone jako argumenty nie są w ogóle przekazywane do funkcji. W zamian tworzone są kopie argumentów i to one są używane jako wartości do przekazania. Rysunek 5.3 ilustruje ten mechanizm na przykładzie naszej funk cji power O .
Rysunek 5.3
int index = 2; double value =10.0; double result =power(value, index);
index
2 Robione są ty mczasowe kopie argumentów do użycia w funkcji
value
10.0
'OPYOf~
'[0 double power ( double X
Int n )
{ Oryginalne argumenty są tutaj niedost ępne. tylko ich kop ie
Za każdym razem, gdy wywołujemy funkcję power ( ), kompilator tworzy kopie podanych argumentów i przechowuje je w tymczasowej lokalizacji w pamięci. Podczas wykonywania funkcji wszystkie odniesienia do parametrów funkcji są odwzorowywane z tych tymczaso wych kopii argumentów .
~
Przekazywanie przez wartość
Jedną
z konsekwencji mechanizmu przekazywania przez wartość jest fakt, że funkcja nie może przekazanych do niej argumentów. Można to sprawdzić, próbując dokonać tego celowo w poniższym przykładzie: bezpośrednio modyfikować
II Cw5_ 02.cpp
II Nieudana próba zmodyfikowania argument ów wywolania.
#include using std : :cout : using std : .endl :
Rozdział 5.•
i nt incrl O(i nt num ) ;
Wprowadzanie struktury do programu
275
II Prot otyp fu nkcji.
int mat nt void)
{
i nt num = 3;
cout
«
endl
"l ncrlO(num) « l ncrl O(num)
endl
« "num = " « num ;
« «
cout « end1;
ret urn O;
II Funkcj a zwiększająca zm ienną o / O.
i nt i ncrl O(i nt num) II
Użycie
tak iej samej naz wy
może pomóc...
{
num += 10 ;
II Zwiększ enie argum entu wyw alania - mam
ret urn num;
II Zwrócenie zwiększonej
nadzieję.
wartoś c i .
Jak to dziala Dane na wyj ściu potwierdzają, szona została jej kopia, która funkcji usunięta.
że
oryginalna wartość zmiennej numpozostała nietknięta . Zwięk wygenerowana, a następnie przy koń czeniu dz iałania
została
Mechani zm przekazywania przez warto ść bardzo dobrze chroni argumenty wywołania przed ich niechci an ą mod yfikacją przez funkcj ę . C zę s to się jednak zdarza, że chcemy, aby funkcja zmodyfikowała nasze argumenty wywołania . Oczywi ście jest na to sposób. Czy nie mówiłem wcze śniej , że wskaźniki mog ą by ć bardzo pożyteczne ?
Wskaźniki iako argumenty lunkcii Kiedy jako argumenty do funkcji przekazujemy w skaźniki , mechanizm przekazywania przez tak samo jak wcześniej. Ale wskaźnik jest adresem innej zmiennej i j eżeli zro bimy kopi ę tego adresu, to nadal wskazuje on tę samą zm ienną. W ten sposób podanie wskaź nikajako parametru pozw ala funkcji na dostęp do argum entów wyw ołan i a .
warto ś ć dzi ała
mI!mI!mI Przekazywanie przez wskaźnik Aby zad e m o n s tro w ać, jak to działa, zmodyfikujemy poprzedni II CwS_03.cpp
II Udana p róba zm odyfi kowania argumentów wywalania.
#i ncl ude
usi ng S td : :cout ;
usi ng std; 'endl;
przykład
z użyci em
wskaźnika:
276
Visual C++ 2005. Od podstaw i nt incrl O(int* num) ;
II Prototyp funkcji .
i nt mai n(void)
{
int num = 3;
int* pn um = #
II
Wskaźnik
do zmiennej num.
cout « endl
« "P rzekazany adres = " « pnum ;
cout « endl « "incrl O(pnum) " « i ncrl OCpnum)_:
L
------J
cout « endl
« "num ~ " « num;
cout « endl ;
return O;
II Funkcja zwiększająca z m ienną 0 10.
int i ncrl O(l nt * num)
II Funkcj a z argumentem
IV
postaci
wskaźn ika .
(
cout « end l
« "Otrzymany adres =
«
*num += 10; ret urn *num;
num;
II Zwiększ argument wy walania.
II Zwróć zwiększoną wartość.
Wyn ik działania tego progr amu jest
n astępujący :
Przekazany ad res = 00 12FF6C
Otrzymany adres = 0012FF6C
i ncrl OCpnum) ~ 13
num = 13
Adresy na każdym komputerze powinny być identyczne.
mogą być
inne
ni ż
te, które
widać powyżej ,
ale obie
warto ści
Jak lo działa Najw ażniejs ze
zmiany w stosunku do poprzedniego kodu w tym przykładzi e dotyczą przeka zywania wskaźnika pnumzamiast oryginalnej zmiennej num. W prototypie funkcji znalazł się teraz typ wskaźn i kowy do typu i nt , zaś dla funkcji mai nO zos tał zadeklar owany i zainicjali zowany adresem zmiennej numwskaźnik pnum. Funkcje mai nO i in cr l OO wysyłaj ą na wyj ście odpowiednio wysłany i otrzymany adres w celu sprawdzenia, czy w obu miejscach używany j est ten sam adre s. Z danych na w yjściu wynik a, że zm ienna numzo stała tym razem identyczną z t ą, która zos tała zwróco na przez funkcj ę .
zw iększo n a
i ma
wa rtość
Rozdzial5.• Wprowadzanie struktury do programu
277
W zmodyfikowanej wersji funkcji incrlO () zarówno instrukcja zwiększająca warto ś ć przeka do funkcj i, jak i instrukcja retu rn wyłusk ują wskaźni k w celu użyc i a przechowywanej przez niego wartości.
zaną
Przekazywanie tablic do lunkcji Do funkcj i m o żn a także p rzekaza ć ta bl i cę , ale w tym przypadku tablica nie jest kopiowana, mimo że metoda przekazyw ania przez wart o ś ć nadal ma tu zastosowanie. Nazwa tabl icy zostaje p rz ekształcona na ws kaź n i k i kopia tego ws kaźnika do początku tablicy zostaje przekazana do funkcji metodą przez warto ś ć. Jest to zaleta, gdyż kopiowanie d użych tablic zaj muje mnóstwo czasu. Jak możn a s ię domyś lić, elementy tablicy mogą być zmieniane wewnątrz funkcji i w ten sposó b tablica jest jedyny m typem, który nie m oże b yć przekazywany przez wartość .
~ PrzekaZywanie lablic Zilustrujemy to na przykładz ie funkcji ob liczającej w postaci tablicy.
end l " ~ redn i a = " average(values . (si zeof val ues)/(s i zeof values[O]) );
cout « endl ;
retu rn O;
II Funkcj a
o bl iczająca średn ią .
double average(double array[]. int count) {
do uble sum = 0.0; for(i nt i = O; i < count : i++ )
sum += array[i] ;
II Sumowanie elementów tablicy.
ret urn sum/count;
II Zwracanie średn iej.
P on i żej
widoczny jest rezultat
~ red n ia
= 5.5
II Łączna suma wszystkich liczb.
po wyższe go
programu ;
278
Visual C++ 2005. Od podstaw
Jak to llziała Funkcja aver age() została przystosowana do pracy z tablicami o dowolnych rozmiarach . Jak z jej prototypu, przyjmuje ona dwa argumenty: tablicę oraz liczbę elementów. Jako że chcemy , aby działała ona z tablicami dowolnego rozmiaru, parametr tablicy pozostawiliśmy bez określenia wymiaru .
widać
wywoływana jest
Funkcja ta
cout
« « «
w funkcji main () za pomocą następującej instrukcji :
Funkcja zostaje wywołana z pierwszym argumentem w postaci nazwy tablicy val ues oraz drugim argumentem będącym wyrażeniem obliczającym liczbę elementów tej tablicy. Wyrażenie mówiliśmy
to (z użyciem operatora sizeof) przypominamy sobie z rozdziału 4., w którym o tablicach.
funkcji wyrażone są w spodziewany sposób. Nie ma żadnej zna tym sposobem a sposobem, który zastosowalibyśmy w przypadku ich implementacji bezpośrednio w funkcji ma i nt ).
Obliczenia
wewnątrz ciała
czącej różnicy pomiędzy
Rezultat
działania
programu potwierdza, że wszystko
działa
tak, jak przewidywaliśmy.
~ Przekazywanie tablic za pomocą notacii wskaźnikowej Nie wyczerpaliśmy jeszcze wszystkich możliwości. Jak powiedziałem na samym początku, nazwa tablicy przekazywana jest jako wskaźnik (mówiąc dokładniej, jako kopia wskaźnika), a więc wewnątrz funkcji nic musimy traktować danych jako tablicy. Naszą funkcję w przykła dzie możemy zmodyfikować w taki sposób, aby w całości pracowała z notacją wskaźnikową, pomimo że używamy tablicy: II Cw5_05.cpp
average(values. (sizeof va l ues )/( si zeof va lues[O ] )) ;
cout « endl :
ret urn O:
Rozdzial5.• Wprowadzanie struktur, do programu II Funkcja
279
obliczająca średnią.
double average(doub le* array. i nt count ) doub le sum = 0.0; for( i nt i = O; i < count : i ++ ) sum+= *array++:
II Łączna suma wszystkich liczb.
return sum/count;
II Zwracanie ś redn iej.
Wynik
działania
II Sum owanie element ów tablicy .
tego programu jest identyczny z poprzednią jego wersją.
Jak to działa Jak widać, program wymagał niewielu zmian, aby pracował z tablicąjako wskaźnikiem . Zmie nione zostały prototyp i nagłówek funkcji, choć żadna z tych zmian nie jest konieczna . Jeżeli przywrócimy je z powrotem do poprzedniego stanu, gdzie pierwszy parametr był określony jako tablica elementów typu doubl e, i ciało funkcji pozostawimy z notacją wskaźnikową, to program będzie działał równie dobrze. Najbardziej interesującą częścią tego programu jest instrukcja w pętli for :
sum+= *array++;
II Sumowanie elementów tablicy.
W instrukcji tej wydaje się , że łamiemy zasadę, iż nie można modyfikować adresu określonego jako nazwa tablicy, ponieważ zwiększamy adres przechowywany w zmiennej array. W rze czywistości nie łamiemy tej zasady. Pamiętajmy, że mechanizm przekazywania przez wartość robi kopię oryginalnego adresu tablicy i go przekazuje - w ten sposób modyfikujemy tylko kopię (oryginalny adres pozostaje nienaruszony). W związku z tym za każdym razem, gdy przekazujemy do funkcji tablicę jednowymiarową, możemy przekazaną wartość traktować jako wskaźnik, a także dowolnie zmieniać adres.
Przekazywanie 110 lunkcii lab lic wielowymiarowych Przekazywanie do funkcji tablicy wielowymiarowej jest bardzo proste. deklaruje tablicę dwuwymiarową o nazwie bean s:
Poniższa
instrukcja
double beans[2][4] ; Następnie możemy utworzyć
prototyp hipotetycznej funkcji o nazwie yi el d( ):
double yield(double beans[2][4] ); Możliwe, że
zastanawiasz się, skąd kompilator wie, że to definicja argumentujako tablicy o pokazan ej liczbie orymiarów, a nie pojedynczego elementu tablicy. Odpowiedź j est pro sta - pojedynczego elementu tablicy nie można zastosować jako parametru w definicji lub prototypie funkcji, choć można go przekaza ć jako argument podczas "yworywania funkcji. Parametr przyjmujący jako argument pojedynczy element tablicy musiałby mieć tylko nazwę zmiennej. Kontekst tablicowy nie ma tutaj zastosowania.
280
Visual C++ 2005. Od podstaw Definiuj ąc tabli c ę wielowym i arowąjako parametr, można także pominąć wartość pierwszego wymiaru . Oc zyw iście funkcja musi w pewien spo sób dowiedzie ć s ię, jakiego rozmi aru jest pierwszy wymi ar. Na przykład m oglibyśmy nap isa ć c o ś takiego:
doubl e yield(double beans[ ][4] . i nt i ndexl : Tutaj drugi parametr dostarc zyłby potrzebnych informacji o pierw szym wymiarze. Funkcja może działać z tablicą dwuwymi arową z wartośc i ą pierwszego wymiaru okre ś l on ą przez drugi argument funkcj i oraz drugim wymiarem tablicy ustawionym na 4.
m!lmI!mI Przekazywanie tablicy wielowymiarowej Funkcję taką
zdefiniujemy w
p on iższym przykład owym
kodzie .
II Cw5_06.cpp
II Przekazywa nie do funkcji tablicy dwu wymiarowej .
#incl ude
usi ng st d: :cout:
using st d: :endl ;
double yield( doubl e ar ray[][ 4]. i nt n) : i nt main (void)
double sum = 0.0 : for ti nt i = O: l < count : i ++ ) for (i nt j = O: j < 4; j++1 sum +~ beans[i ][ J] ; ret urn sum:
Rezultat
Pl on
=
dzi ałania
II Pętla przechodząca przez liczbę wierszy. II Pęt la przechodząca przez elementy w wierszu.
tego programu jest następujący:
78
Jak to działa W nagłówku funkcji zastosowałem inne nazwy argumentów niż nazwy parametrów w jej pro totypie po to, by przypomnieć , ż e jest to dozwolone (chociaż w tym przypadku w niczym nie pomaga). Pierwszy parametr zo s tał zdefini owany jako tabl ica zaw ie raj ąc a dow olną liczbę
Rozdział 5.•
Wprowadzanie struktury do programu
281
wierszy, z których każdy zawiera cztery elementy . Funkcję wywołujemy z tablicą beans zawie rającą trzy wiersze. Drugi argument jest określany poprzez dzielenie całkowit ego rozmiaru tablicy w bajtach przez rozmiar pierwszego jej wiersza. W wyniku otrzymujemy liczbę wier szy w tablicy. Obliczenia w funkcji dokonywane są w zagnieżdżonej pętli f or, gdzie pętla wewnętrzna su muje elementy jednego wiersza , a zewnętrzna powtarza tę czynność dla wszystkich wierszy. Użycie w tym przypadku wskaźnika zamiast tablicy wielowymiarowej jako argumentu funk cji nie byłoby zbyt dobrym rozwiązaniem. Kiedy przekazywana jest tablica, to przekazuje ona adres wskazujący tablicę czterech elementów (w każdym wierszu). Nie łatwo tego dokonać za pomocą operacji ze wskaźnikiem wewnątrz funkcji. Musielibyśmy zmodyfikować instruk cję zagnieżdżonej pętli fo r w następujący sposób:
sum += *(*(beans
+ 1) +
j):
W notacji tablicowej kod jest bardziej przejrzysty.
Referencje jako argumenty funkcji Przejdziemy teraz do drugiego mechanizmu przekazywania argumentów do funkcji. Określe nie parametru funkcji jako referencji zmienia sposób przekazywania danych dla tego para metru . Nie używamy wtedy metody przekazywania przez wartość, w której argument przed przekazaniem do funkcji jest kopiowany, ale metody przekazywania przez referencję, w któ rej parametr działa jako alias przekazywanego argumentu. Wyklucza to konieczność kopio wania czegokolwiek oraz daje funkcji bezpośredni dostęp do argumentu wywołania. Oznacza to także, że wyłuskiwani e, któr e jest konieczne przy przekazywaniu i używaniu wskaźnika wartości, jest również niepotrzebne.
~
Przekazywanie przez referencję
Wrócimy na chwilę do kodu z programu Cw5_03.cpp, aby użyjemy w nim parametrów w postaci referencji: II Cw5_07.cpp
II Modyfikowanie argum entów
wywołan ia
za pomocą referencji.
#i ncl ude
using std: :cout:
usi ng std:: endl :
i nt incrlO(i nt &num):
II Prototyp funkcji.
i nt mai n(void)
{
i nt num ~ 3;
i nt va l ue ~ 6;
cout « endl
« "i ncrlO( numl
~
"
«
i ncrl O(num l :
zobaczyć ,
jak
będzie działał,
gdy
282
Visnal C++ 2005. Od podstaw cout « end l
« "num ~ "
«
num:
cout « endl
« "i ncr10(val ue) cou t « end l
« "va l ue = "
«
=
"
«
i ncr10(val ue):
value:
cout « endl : ret urn O; II Funkcja zwi ększająca zmienną 0 10
i nt i ncr10(i nt &num )
II Funkcja z argumentem w p ostaci ref erencj i.
(
cout « endl
« "Ot rzymana
w ar t oś ć
num += 10 : ret urn num; Wynik
działania powyższego
= " « num:
II Zwiększen ie argumentu wywolania. II Zwrócenie zwiększonej wartoś c i .
prog ramu j est
n astępujący :
Otrzymana wa r to ś ć = 3
i ncr10(num) = 13
num = 13
Ot rzymana wa r t o ś ć = 6
i ncr10(val ue) = 16
va l ue = 16
Jak to działa Sposób działan i a tego programu jest d oś ć niezwykły. Jest on w zasadzie taki sam jak program Cw5_03.cpp, z tym wyjątkiem , że jako parametr funkcji mamy re fe re n cj ę . Aby to odzwier ciedlić , zm ien iony z ostał prototyp tej funkcji. Kiedy funkcja jest wywoływana, argument jest określany tak jak w przypadku operacj i przekazywania przez wartość , a więc jest on uży wany podobnie ja k w poprzedniej wersji. Wartość argumentu nie jest przekazywana do funkcj i. Parametr funkcji zostaje zainicjalizowany adresem argumentu, a więc za każdym razem, gdy pa rametr num użyt y jest w funkcji, uzyskuje on bezpośredni dostęp do argumentu wywołującego . Aby zapewnić , że nie ma nic podejrzan ego w u życiu identyfikatora numw funkcji mai m ), jak również w samej funkcji, wywołuje my ją ponownie ze zmienną val ue ja ko argumentem. Na pierwszy rzut oka może wydawać si ę , że je st to sprzeczne z tym, co mówiłem na temat podstawowych właści wo ści referencji - że po zadeklarowaniu i zainicjalizowaniu nie można jej przypi sać do innej zmiennej. Powodem , dla którego nie jest to sprzeczne ze wspomn ian ą zasadą, je st fakt, że referencja będąca parametrem funkcji jest tworz ona i inicjal izowana przy każdym wyw oł aniu funkcji, a następnie, kiedy funkcja się kończy, jest niszczona. W ten spo sób za każdym razem , gdy używamy funkcj i, otrzymuj emy całkiem n ową referencję .
Rozdzial5.• Wprowadzanie struktury do programu
283
Wewnątrz funkcji wartość otrzymana z programu wywołującego wyświ etl ona zostaje na ekranie. Mimo że instrukcja jest w zasadzie taka sama jak ta użyta do wysłan ia na wyj ście adresu przechowywanego we w skaźniku , ponieważ zmienna numjest teraz referencją, zamiast adresu uzyskujemy wartość danych .
Widać tu bardzo dobrze różnicę pomiędzy wskaźnikiem a referencją. Ref erencja j est aliasem innej zmiennej, dz ięki czemu moż e być używan a j ako alternatywny sp osób odwoły wania s ię do niej. Jej użycie j est równoznaczn e z użyciem oryginalnej nazwy zmiennej.
Z danych na wyjściu widać, ż e funkcja i nc rlOO zanąjako argument wywołania.
bezpo ś rednio
modyfikuje
zmienną
przeka-
jako argument funkcji i ncrlO ( ) podamy waność liczbow ą, np. 20, to kompilator zgłosi komunikat o błędzie. Dzieje się tak, ponieważ kompilat or uznaje, że referencja jako parametr może być modyfikowana wewn ątrz funkcj i, a ostatni ą rzeczą, której byśmy chcieli, jest zmiana wartośc i stałych . Wprowadziłoby to trochę zamieszania w naszych programach, którego wole-
Jeżeli
libyśmy uniknąć .
Zabezpi eczenie to jest bardzo dobre, ale gdyby funkcja nie modyfikowała wartości, to wolelibyśmy , by kompilator nie zgłaszał błędu za każdym razem, gdy przekazujemy referencję jako argument, który był s tałą. Powini en być na to j aki ś sposób i oczywiście je st.
Zastosowanie modyfikatora const Modyfikator const można zastosować do parametru funkcji w celu poinformowania kompilatora, że nie zamierzamy go w żaden sposób modyfikować. Powoduje to sprawdzenie przez kompilator, czy rzeczyw i ście argument nie jest nigdzie w kodzie modyfikowany. Przy zastosowaniu modyfikatora const kompilator nie zgł asza już żadnych komunikatów o błęd ach .
~ Przekazywanie modyfikatora const W celu zaprezentowania spo sobu poprzedniego programu.
działania
modyfikatora const
użyjemy
zmienionej wersji
IICw5_08cpp II Modyfi kowani e argum entów wywalania za pomocą ref erencji.
#i ncl ude using st d: :caut : using st d: :endl : int i ncrl O(canst i nt&num):
II Prototyp funkcji.
i nt mai n(vaid) canst int num = 3: i nt value
=
6:
II Deklaracja z operatorem const w celu spra wdzenia II tworzenia tym czaso wego.
6 ostrukturze programu
-
ciąg
dalszy
W popr zednim rozdziale nauc zyliśmy s i ę podstaw definio wania funkcji oraz ró żnych spo sobów przekazywania do nich danych. Zobaczyli śmy także, jak rezultat dzi ałan i a funkcji prze kazywany je st do progr amu wywołującego . W tym rozdziale zagłę b i my się jeszcze bardziej w Oto lista poruszanych temató w:
tem a tykę
robienia dobrego
użytku
z funkcji.
• Czy m jest w skaźni k do funk cji. • Jak
defini ować
i używa ć wskaźników do funk cji .
• Jak
defini ować
i u żywa ć tablic w skaźników do funk cji.
• Czym jest • Jak
• Czym
p isać
procedu ry
o bs ług i wyj ątków .
kilka funk cji o jednej nazw ie w ce lu aut omatycznej typu.
są
szablony funkcji oraz jak
nap is ać
• Czym • Jak
i jak
napi s ać
ró żnego
• Jak
wyj ątek
są
się je
ob sług i
defin iuje i ich używ a .
solidny program w natywnym C++ z zastosow aniem wielu funkcji .
funkcje generyczne w C++/C LI.
nap i s ać
danych
solidny program w C++/CLI z zas tosowa niem wielu funkcji.
306
Visual C++ 2005. Od podstaw
Wskaźniki
do lunkcji
Wskaźnik
przechowuje adres, który do tej pory zawsze odnosił się do j akiej ś zmiennej o takim wskaźnik typie podstawowym. Dawało to dość dużą elastyczność, umożliwiając używanie różnych zmiennych w różnym czasie poprzez jeden wskaźnik. Wskaźnik może także wskazywać adres funkcji. Pozwala to na wywoływanie tej funkcji poprzez wskaźnik, który zajmuje pamięć o adresie ostatnio przypisanym do wskaźnika . samym co
Oczywiście wskaźnik
do funkcji musi zawierać adres obszaru pamięci funkcji , którą chcemy Aby jednak wskaźnik mógł prawidłowo działać, musi także przechowywać infor macje o liście parametrów funkcji, którą wskazuje, oraz zwracanym przez nią typie. W związku z tym , definiując wskaźnik do funkcji, poza nazwą, musimy podać typy parametrów oraz typ wartości zwracanej przez funkcje, które może wskazywać . Zasady te ograniczają oczywiście w pewnym stopniu zakres tego, co można przechowywać w danym wskaźniku do funkcji. Jeżeli zdefiniujemy wskaźnik do funkcji przyjmujących jeden argument typu i nt i zwracają cych wartość typu doubl e, to możemy przechowywać tylko adres funkcji, która ma dokładnie taką samą formę. Jeżeli chcemy przechować adres funkcji przyjmującej dwa argumenty typu in t i zwracającej typ char, to musimy zdefiniować inny wskaźnik o takich właściwościach. wywołać .
Deklarowanie wskaźników do funkcji Możemy zadeklarować wskaźnik o nazwie pfun, którego będzie można używać do wskazy wania funkcji przyjmujących dwa argumenty typu char* oraz int i zwracających warto ści typu doubl e. Deklaracja takiego wskaźnika wygląda następująco:
double (*pfun)(char*. i nt ) :
II
Wskaźnik
do deklara cji funkcji.
Z początku może się wydawać , że kod ten wygląda dość dziwnie z powodu tych nawiasów. lnstrukcja ta deklaruje wskaźnik o nazwie pfun, który może wskazywać funkcje przyjmujące dwa argumenty - typu wskaźnikowego do char i typu int - oraz zwracające wartości typu doubl e. Nawiasy otaczające nazwę wskaźnika pf un oraz gwiazdka są niezbędne. Bez nich instrukcja ta byłaby deklaracją funkcji, a nie deklaracją wskaźnika. W takim przypadku wyglą dałaby ona następująco :
double *pfun (char*. int );
II Prototyp funkcji II zwracają cej typ double".
Powyższa instrukcja jest prototypem funkcji pf un z dwoma parametrami, zwracającej wskaź nik do typu doubl e. A jako że chcieliśmy zadeklarować wskaźnik, powyższa instrukcja niejest tym, czego potrzebujemy.
Ogólna forma deklaracji
typ_zwracany Wskaźnik
ten
wskaźnika do
funkcji
wygląda następująco:
(*nazwa_wsk a źni k a ) ( l i s t a_ typó w-p a rame tró w ) :
moż e wskazywać
tylko funkcje o takim samym typie zwracanym i z taką .
samą listą typów parametrów określonymi w deklaracji.
Rozdział 6.•
Jak
w idać p owyżej ,
deklaracja w s kaźni ka do funkcji
skład a s ię
z trzech
307
s kład n ikó w :
funkcj ę, którą wskaźnik może wskazywać ,
•
typu zwr acanego przez
•
nazwy
•
typów parametrów funkcj i, które
wskaźnika
ciąg dalszy
OstrukUJrze programu -
poprzed zonej
gwiazdką ozna czającą, że jest
to
w skaźnik,
wskaźnik mo że wskazywać.
Jeżeli
spróbujemy przypisać do wskaźn ika funkcję, która nie odpowiada typom w jego deklaracji, kompilator zgłosi komunikat o błędzi e .
Wskaźnik
znajduje
można zaini cj alizować nazwą
do funkcji
się przykład
takiego
funkcji w obrębie jeg o deklaracji .
Poniżej
podejścia:
lo ng sum( lo ng numl. l ong num2) : l ong (*pfun)( l one . l ong ) = sum:
II Pro totyp funkcji . II Wskaźn ik do funkcji wskazuje fun kcję sumt).
zadeklarowany tu wskaźn ik pfun m ożemy ustawi ć tak, aby wskazywał dwa argumenty typu long oraz zwracając ą wartoś ć typu l ong. W pierwszym przypadku zainicjalizowaliśmy go adresem funkcji sum(), której prototyp został zdefiniowany w pierwszej z powyższych instrukcji.
Ogólnie rzecz
biorąc,
dowolną funkcję przyjmującą
Oczywi ś cie wskaźnik
do funkcj i m ożemy również zai nicj al i zować za pomoc ą instrukcji przy pf un został zadekl arowan y tak jak powyż ej , jego wartoś ć moglibyśmy ustawiać tak, aby wskazywała różn e funkcje za pomo cą p oni ższych instrukcj i:
pisania.
Zakładając , że w sk aźn ik
l ong produet ( l ong. l ong) :
II Prototyp fu nkcji.
pf un
II Ustaw
=
produet ;
wskaźn ik
na funkcj ę producu) .
Podobn ie jak w przypadku w skaźników do zmiennych, musimy upewnić się , że wskaźnik do funkcji został zainicjalizowany przed użyciem go do jej wywołania . Bez wykonania tej czyn ności mamy pewność, że program ulegnie katastrofalnej awari i.
lmIiD Wskaźnikido IImkcii Aby dokładnie szy listing :
zobaczyć ,
jak
dzi ałają wskaźn iki
do funkcji w programie , spójrzmy na
II Cw6 Ol.cpp
II Ćwic-;enie wskaźników do fun kcji.
#i nel ude
using st d: :eout :
us i ng st d: :endl ;
l ong S um(l ong a . l ong b) ; l ong i l oczyn (l ong a . l ong b);
II Prototyp f unkcji. II Prototyp f unkcji.
i nt ma i n(voi d)
{
long (*pdo_i t )(l ong. l onq) :
II
Wskaźn ik
do deklara cji funkcji.
poniż
308
Visual C++ 2005. Od podstaw pdo_i t = i locz yn: cout « endl « "3 * 5 ~
« pdo_it (3 . 5) :
II Wynik
wywo łania
przez
wskaźnik.
pdo i t = sum ; II Ponown e przyp isani e ws kaźnika do fun kcj i sumi). cout « endl « "3 * ( 4 + 5 ) + 6 - " « pdo_i t ( i l oczyn (3 . pdo_i t (4 . 5 )). 6) ; II Dwukrotne wywołan ie poprzez wskaźnik. cout « endl ;
ret urn O;
II Funkcj a
mnożą ca
dwie liczby.
l ong iloczyn(l ong a. l ong b)
{
ret urn a*b:
II Funkcj a dodając a dwie liczby.
l ong sum( l ong a. lo ng b)
{
re t ur n a + b;
}
Poniżej
znajduje
się
rezultat
powyższego programu:
3 * 5 - 15
3 * ( 4
+
5 )
+
6
~
33
.Jak to działa Program ten nie jest zbyt użyteczny, ale dobrze służy jako przykład deklaracji wskaźnika do funkcji, przypisywania wartości do wskaźnika i wreszcie użycia go do wywołania funkcji. Po tradycyjnym wstępie deklarujemy wskaźnik do funkcji pdo_ i t , mogący wskazywać każdą z pozostałych dwóch funkcji, które zdefiniowaliśmy : sum( ) oraz i l oczynt ), Za pomocą poniż szej instrukcji przypisania wskaźnikowi przypisany został adres funkcji i l oezyn() : pdo_i t
~
i l oczyn;
Podaliśmy
tylko nazwę funkcji jako początkową wartość wskaźnika. Niepotrzebne są tutaj nawiasy i inne dekoracje. Nazwa funkcji zostaje automatycznie przekonwertowana na adres, który jest przechowywany we wskaźniku . żadne
Funkcja i l oezyn( ) została wywołana pośrednio poprzez wskaźnik pdo_i t w poniższej instrukcji: eout « endl « "3 * 5 = " « pdo_it (3 . 5) :
II
Wywołanie f unkcj i
iloczyn Opoprzez
Nazwy w ska źnika używamy tak jak zwykłej nazwy funkcji, po której menty w nawiasach , dokładnie tak samo jak w przypadku bezpośredniego nej nazwy funkcji.
wskaźnik.
znajdują się
argu
używania oryginał
Rozdzial6. •
ostrukturze programu -
W celu pokazania , że jest to mo żliwe, zmieniamy
wskaźnik ,
II Ponowne przypisanie Następni e dz iałania
cout « « «
wykorzystuj emy go w absurdalni e arytmetycznego:
aby
wskaźn ika
ciąg
dalszJ
wskazywał funkcj ę
309 sum( ).
do fu nkcji sumt).
zawiłym wyrażeniu
w celu wykonan ia prostego
endl
"3
* (
4
+
5 )
+
6 - "
pdo_ i t ( i l oczyn(3. pdo_ i t (4. 5) ) . 6) ;
II Dwukrotne
wywo łan ie
poprz ez
wskaźn ik.
Z powyższe go kodu wynik a, że wskaźnika do funkcji można używać w dokładnie taki sam sposób jak samej funkcji przez niego wskazywanej. Kolejność czynności w tym wyrażeniu pokazana jest na rysunku 6.1. pdo_it ( iloczyn ( 3 , pdo_it ( 4,5) ) • 6)
Rysunek 6.1
~ rów noznaczne z
,-1--, sum(4,5)
I r -----A_ d_a_., j' "['"Ok"
iloczyn ( 3 , 9 ) ~) daje w wyniku
ł
pdo_it ( 27 • 6 )
'-I równoznaczne z
~
sum ( 27 • 6) -
Wskaźnik
ma warto ś ć _
33
do funkcji iako argument
na fakt, że wskaźnik do funkcji jest całkiem rozsądnym typem , funkcja może parametr będący wskaźnikiem do funkcji. Funkcja ta następnie może wywoły wać funkcję wskazywaną przez ten argument. Jako że wskaźnik może być tak zaprojektowany, aby wskazyw ał różne funkcje w różnych warunkach, pozwala to na określenie tej funkcji , która ma być wywołana z wnętrza funkcji w program ie wywołuj ącym. W takim przypadku funkcj ę możemy przekazać jawnie jako argument.
Ze
względu
również m ieć
310
Visual C++ 2005. Od podstaw
~ Przekazywanie wskaźnika funkcji potrzebujemy funkcj i przetw arz ając ej tablicę liczb kwadratów każdej z nich w jednej sy tuacj i oraz sumę ich trzecich pot ę g w innej sytuacji. Jednym ze sposobów os iągn i ęc ia tego celu je st użycie jako argumentu wskaź nika do funkcji. Spójrzmy na
przykład . Przypuśćmy, ż e
obli czającą sum ę
/ICw6_02.cpp
II Ws kaźnik do funkcji jako argument.
#lncl ude
using std: :cout:
using st drendl:
II Prototypy f unkcji.
double squared(dou ble) ;
double cubed(doublel;
double suma rray(doub le arr ay[]. int len. double (*pfun)( do ub le l);
« "Suma l iczb podnies ionych do po tęg i trzeci ej
« sumarray(array. len. cubed) ;
cout « end l ;
return O:
II Funkcja
podnosząca
liczby do kwadratu.
doub le squared(double xl
{
ret urn x*x:
II Funkcj a podnosząca liczby do potęgi trzeciej.
doub le cubed(doub le xl
{
ret urn x*x*x;
}
II Funkcja
s umują c a fun kcje
elementów tabli c.
double sumarray(double array[]. i nt len. doubl e (*pfunl(double)) (
doub le total
=
0.0;
II Suma ogólna.
rort mt i = O; i < len; i ++l
t ot al +~ pfun(arraY[l ] ) ;
ret urn total ;
Po skompilowaniu i uruchomieniu wyn ik:
Rozdział 6.•
Ostrukturze programu -
p owyższego
programu
ciąg
dalszy
311
powinniśmy otrzymać następuj ący
Suma li czb podniesionych do kwadrat u = 169.75 Suma li czb podniesionych do potęg i trzeciej ~ 1015.88
Jak lo działa Pierwszym interesuj ącym elementem jest prototyp funkcji sumar r ay( ) . Jej trzeci parametr jest wskaźnikiem do funkcji, która ma parametr typu doubl e i zwrac a wartość typu doubl e.
do uble sumarrayCdou ble array[] . int len. double C*pfun)C doub le)); Funkcja sumar ray() prz etw arza k ażdy element tablicy przekazanej do niej jako pierwszy argument za pomocą funkcji , którą wskazuje trzeci argument. Następn i e funkcja ta zwraca sumę przetworzonych elementów. W funkcji ma i n( ) funkcję suma r r ay( ) wywołujemy dwa razy. Za pierwszym razem z nazwą funkcji squared jako trzecim argumentem, a za drugim z nazwą funkcji cubed. W każdym przy padku adres odpow iadający nazwie funkcji użytej jako argument zastęp ow any jest wskaźni kiem do funkcji w ciele funkcji sunarrayt ), dzięki czemu w pętli for zostaje wywolana odpo wiednia funkcja. Oczywiści e s ą
prostsze sposoby osiągnięcia tego , co robi ten program , ale zast osowanie funkcji daje nam du ży stopień uogólnienia. Do funkcji sumarray() możemy prze kazać dowolną funkcję , która przyjmuje jeden argument typu doubl e ora z zwraca wartość typu doubl e. wskaźnika do
Tablice wskaźników do funkcii W podobny sposób, jak definiuje się zwykłe wskażnik.i , można także d eklarować tablice wskaź ników do funkcji. Możne je także w deklaracji inicjalizować . Poniżej znajduje się przykładowa deklaracja tabl icy wskaźników.
Ka żdy element tablicy został zainicjalizowany adresem o d p o wi adaj ąc ej mu funkcji z listy inicjalizacyjnej w nawiasach. Aby wywołać funkcję produet<) przy użyciu drugiego elementu tablicy w skaźników, możemy napisać :
pfun[1](2.5. 3.5); Nawiasy kwadratowe określające element tablicy wskaźników do funkcji znajdują się bez pośrednio po nazwie tablicy i przed argum entami funkcj i, która ma być wywołana . Oczy wiście wywołanie funkcji można wykonać poprzez element tablicy wskaźników do funkcji
312
Visual C++ 2005. Od podstaw w dowolnym prawidłowym wyrażeniu, w którym orygi nalna funkcj a może s i ę legalnie po a indeks wsk aźnika może być dowoln ym wyra żen i e m , któreg o wynikiem j est prawi dł owy indeks.
j awić ,
Inicjalizowanie parametrów lunkcji
W przypadku wszystki ch funk cji , których używal i śmy do tej pory , musieliśmy dostarczać argumenty odpow i adające wszystk im parametrom w wywołaniu funkcji. Dobrze by b ył o, gdybyśmy mogli czasami pomin ąć jeden lub dwa argumenty w wyw ołaniu funkcj i oraz gdy byśmy mogli okre śli ć domyślne w artości wsmwiane w miejsce niektórych pominiętych argu mentów. Możemy to os iągnąć poprzez zainicjali zowanie parametrów funk cji w jej prototypie. P rzypuśćmy na p rzykład , że piszemy funkcj ę w y świ etl ającą komunikat, w której komunikat ten jest prz ekazywany jako argument. Poniżej znajduje się definicja takiej funkcji:
vOid showit( const char message[] )
{
cout
« «
endl
message:
ret urn:
Parametr tej funkcji j ak poniżej : vord
możemy zaini cjali zowa ć, p odając łań cuch i nicj a l i zuj ący
showit (const char message[]
=
"Coś
w jej prototypie,
j est me t ak" ) :
W powyższym przykładzie parametr message zost ał zainicjal izowan y za pomocą l iterału łań cuchowego. Jeżeli w prototypie funkcj i podamy parametr inicjalizujący i przy jej wywołan iu pominiemy odpowiadający mu argument, to funkcja zostanie wywołana z tą warto śc ią jako d omy ślną,
~ pomijanie argumentów funkcii P ominięcie d omyślną
wartoś ci.
argumentu przy wywoływaniu funkcji powoduje wykonanie j ej z wa rt ośc i ą dla tego argumentu . Jeżeli argument podamy, to zostan ie on użyty w miejsce tej Za pomo cą funk cji showit() możemy wysłać na wyjście różne komunikaty.
II Cw6_03.cpp
II Pomijanie argum entów funkcji.
#include
using st d: :cout :
using st d: :endl :
vOid showit(const char message [] i nt main(vo id)
II Wyś wietl podstawo wy komunikat. II Wyś wietl komuni kat alternatywny. II Pono wnie wyświe tl komun ikat p odstawowy. II Wyś wietl komunik at predefiniow any.
cout « endl : ret urn o; void showi t (const char message[] ) (
cout
« «
end l
message;
ret urn;
Wykonanie powyższego kodu da Coś jest nie t ak. Jest bardzo ź l e l Coś jes t nie ta k. Koniec św ia ta Jest
j uż
następujący
apokaliptyczny wyn ik:
bl iski .
Jak to dziala Jak widać, za każdym razem, gdy nie podamy argum entu, wy świetlany jest domyślny komu nikat zdefiniowany w prototypie funkcji. W przeciwnym przypadku funkcja zachowuje się normalnie. Jeżeli
z kilkoma argumentami, to dla dowolnej ich liczby możemy zd efiniować chcem y pominąć więcej niż jeden argument, aby została zastoso wana warto ś ć dom yśln a , to musim y także pominąć wszystkie argumenty znajdujące s i ę po prawej stronie pierwszego z nich . Przypuśćmy na przykład , że mam y poniższą funkcję: mamy
funkcję
wartości domyślne . Jeżeli
i nt do i t (long arg1 = 10 . long arg2 = 20. long arg3
=
30. long arg4
=
40) ;
chcemy pominąć jeden z argumentów. Może to być tylko ostatni , poni eważ nie podawać argumentu ar g3, to argument ar g4 również musielibyśmy pominąć. Gdyby śmy nie podali wartości dla arg2, musi eliby śmy tak samo postąpić z ar g3 i arg4. W przypadku użycia wartości domyślnej dla argumentu argl funkcja zostałaby wywo łana z wartościami domyślnymi dla wszystkich argumentów. i w jej
wywołaniu
jeżeli chcielibyśmy
Można
z tego wyciągnąć taki wniosek , że argumenty pos iadające wartości domyślne w pro totypie funkcji powinn y znajdować się na samym końcu listy parametrów w takiej kolejno ści, aby argument naj częściej pomijany znalazł s i ę na samym jej końcu.
314
Visual C++ 2005. Od podstaw
Wyjątki Wykonując ćwiczenia
z końca poprzedniego rozdziału, na pewno zdarzyło Ci się spowodować kompilatora i ostrzeżenia, a także błędy podczas wykonywania programu. Wy jątki służą do oznaczania błędów lub niespodziewanych sytuacji, które zdarzają s i ę w pro gramach napisanych w C++. Wiemy już, że operator new powoduje wyjątek, jeżeli nie możn a przydzielić żądanego przez niego obszaru pamięci.
jakie ś błędy
Do tej pory błędami w programie zajmowaliśmy się w ten sposób, że sprawdzaliśmy za pomocą instrukcji warunkowej i f dane wyrażenie i wykonywaliśmy określony fragment kodu w celu obsłużenia tego błędu . W C++ dostępny jest także bardziej ogólny mechanizm obsługi błędów, pozwalający na oddzielenie kodu zajmującego się takimi zdarzeniami od kodu, który jest wykonywany, gdy takie sytuacje nie mają miejsca. Należy pamiętać, że wyjątki nie są po to, by zastępować normalne sprawdzanie i walidację danych, które możemy wykonywać w pro gramie. Kod wygenerowany, kiedy używamy wyjątków, jest dość rozbudowany, w związku z czym wyjątki powinny być stosowane tylko w naprawdę wyjątkowych, prawie katastrofal nych sytuacjach, które mogą się zdarzyć, ale nie są spodziewane w normalnym biegu wyda rzeń. Błąd odczytu danych z dysku może być sytuacją, w której trzeba zastosować wyjątek, a wprowadzenie nieprawidłowych danych już nie. Mechanizm
obsługi wyjątków
wprowadza trzy nowe
•
t ry - identyfikuje blok kodu, w którym
•
t hrow - powoduje
wyjątek.
•
cat ch- blok kodu
obsługujący wyjątek.
Poniżej
znajduje
się przykładowy
słowa
kluczowe:
może wystąpić wyjątek .
program, w którym zobaczymy, jak to wszystko działa.
najlepiej jest przećwiczyć na przykładzie. Użyjemy tutaj bardzo prostego chcemy napisać program obliczający liczbę minut potrzebną do wykonania jakiejś części na maszynie. Liczba wyprodukowanych części zapisywana jest co godzinę, ale musimy wziąć pod uwagę, że maszyna ulega regularnie awariom i w tym czasie może nie zrobić ani jednej części. przykładu . Przypuśćmy, że
Do napisania takiego programu // Cw6_04.cpp
Stosowanie o bs ł ug i
możemy wykorzystać obsługę wyjątków:
wyjqtkćw .
#incl ude
usi ng st d: :cout :
usi ng st d: .endl :
i nt mai n(void)
{
i nt counts[ ]
=
(34 . 54 . O. 27. O. 10. Oj:
Rozdzial6. i nt ti me = 60 ; forC i nt i = O ; t ry
ostrukturze programu -
ciąg
dalszy
315
II Liczba minut w godzinie. 1
< si zeof count s/ sizeof counts[ O] ; i ++)
(
cout « endl
« "Godzi na
« i +1.
i f Ccount s[i ] == O) t hrow " War tość zerowa - nie
mo żn a wy k o n a ć o b l ic z e ń .
";
cout « " minut na j ed n ą c z ęś ć ; " « st at ic_cast <double>Ct ime)/ count s[i];
}
catchCconst char aMessage[]l
(
cout « endl
« aMessage
« endl :
}
retur n O;
Uruchomienie
powyższego
programu da następujący wynik:
Godzi na 1 mi nut na j ed n ą c z ęść ; 1.76471
Godzina 2 mi nut na j ed n ą c z ęś ć ; 1.11111
Godzina 3
Wa r to ś ć zerowa - nie mo ż na wyko na ć o b l ic z e ń
Godzina 4 mi nut na j ed n ą c zęść ; 2. 22222
Godzi na 5
W arto ś ć zerowa - nie moż n a wykon a ć o bl i c z eń
Godzi na 6 mi nut na j ed n ą część ; 6
Godz ina 7
W a r t o ś ć zerowa - nie mo ż na wyk ona ć
o bl ic z eń
Jak lo działa Kodw bloku try wykonywany jest w normalnej kolejności. Blok ten służy do określenia kodu, w którym może zostać zgłoszony wyjątek. Jak widać z danych wysłanych na ekran, w momen cie wystąpienia wyjątku wykonywany jest kod klauzuli catch, a następnie program prze chodzi do następnego powtórzenia pętli . Oczywiście, jeżeli nie wystąpi żaden wyjątek, to klauzula catch w ogóle nie zostanie wykonana. Zarówno blok try, jak i klauzula cat ch są trak towaneprzez kompilator jako pojedyncze jednostki, a więc tworzą razem blok pętli for, która kontynuuje działanie po wystąpieniu wyjątku . Dzielenie wykonywane jest w instrukcji wyjściowej , która występuje po instrukcji warun kowej i f sprawdzającej dzielnik. Jeżeli wykonana zostanie instrukcja throw, kontrola natych miast zostaje przekazana do pierwszej instrukcji w klauzuli catch, dzięki czemu instrukcja wykonująca dzielenie zostanie ominięta w przypadku wystąpienia wyjątku. Po wykonaniu in strukcji w klauzuli catch pętla przechodzi do następnego powtórzenia, jeżeli jeszcze jakieś jest.
316
Visual C++ 2005. Od podstaw
Wywoływanie wyjątków w dowolnym miej scu w bloku t ry. Typ wyjątku określan y jest przez opera nd instrukcj i t hrow- w naszym przykładzie wyjątek je st literałem łańcuchowym , a więc typu const che r t l . Operand po słowie kluczowym throw może być dowolnym wyra żeni em , a typ jego wyniku o kreśl a typ spowodowanego wyjątku.
Wyjątki można wyw oływ ać
Wyjątki mo gą być także powodowane w funkcj ach wywoływany ch z bloku t ry oraz prze chwytywane przez znajdujący s ię po nim kod klau zuli catch. Aby to zad emonstrować , do poprzedniego przykładu m ożemy dodać funkcję za p om ocą poniżs zej definicj i:
void t est Th row(void) (
t hrow Wywoł anie
"W a r tość
zerowa - nie
mo ż na wyk on a ć o b l i czeń .
tej funk cji wstawi amy w poprzednim
if (count s[ i] == O)
te stThrow( ) :
II
przykładzie
w miejsce instrukcj i t hrow:
Wywołaj funkcję po wodującą wyjątek.
Za każdym razem , gdy element tablicy ma wartość zero, funkcja te stThrow( ) powoduje wyj ą tek, który zostaje przejęty przez klauzulę catc h - a więc wynik będzi e taki sam jak poprzed nio. Nie zapomnij o prototypie funkcji, jeżeli dodaje sz definicję funkcji t estThrow( ) na końcu kodu źródłowego.
Przechwytywanie wyjątków Klauzula catc h po bloku try w naszym przykładzie prz echwytuj e wszy stki e wyjątki typu const char[ ]. Typ ten został okre śl ony przez sp ecyfik a cję parametru zn aj dującą się w nawia sie po słowi e kluczowym catc h. Dla bloku t ry musimy dostarczyć co najmniej jed en blok cat ch, który musi znajdować się bezpośrednio po bloku try. Klauzula catch przechwytuje wszystkie
wyjątki (właściw ego
typu) występujące w dowolnym miejscu w kodzie bloku znaj po bloku t ry , włącznie z wyjątkami spowodowanymi przez funkcje lub po średnio wewnątrz bloku try.
dującego się bezpośrednio wywołan e bezpośrednio
chcemy, aby klauzula catch ob słu g iwała wszystkie wyjątki spowodowane w bloku tr y, musimy zasto sować elipsę ( ... ) w nawiasach zawierający ch deklara cj ę wyj ątku:
Jeżeli
catc h ( ... ) { II Kod
obs ług i
wszystkich
wyjątkó w.
}
zdefiniujemy więcej klauzul catc h dla bloku t ry, to na ostatn im miejscu.
Jeżeli
powyższy
blok musi
znajdować s ię
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
317
liI!!mI1!lmI Zagnieżdżone bloki trv Bloki try można zagnieżdżać. W takim przypadku, jeże li w wew nętrznym bloku t ry zostanie spowodowa ny wyjątek, po którym nie występ uje klauzula catch o d powiadająca typem temu wyj ątkowi, to poszukuje się klauzul catch zew nętrznego bloku try. Przyj rzyj my się temu na pon i ższym przykład z i e :
II Cw6_05.cpp
II Zagnieżdż one bloki try.
#lnclude
using std: .cm :
usi ng std: :cout;
using std : .endl :
i nt mai n(void ) {
int height = O;
const double i nchesToMet ers = 0.0254;
char ch = ' t ' ;
t ry {
II Zewnętrzny blok try.
whil e(ch == ' t ' I ICh == ' T' )
{
cout « " Wp r owa d ź Cln » height ;
wyso k ość
w calach:
II Wczytaj
t ry
wysokoś ć
do konwersji.
II Defini cja bloku try, w którym II mogą po wstawać wyją tki.
{
if (hei ght > 100) t hrow "Podano zbyt if( hei ght < 9) th row hel ght; cout
« « «
dużą l i c z bę " :
II Spowodowano
wyją tek.
II Spowodowano wyją tek.
st at ic_cast <double>(height)*i nchesToMet ers
" met rów"
endl ;
}
catc h(const char aMessage[ ])
II Poczqtek klauzuli catch, która II przechwyt uj e wyją tki typu II const char{j.
{
cout
«
aMessage
end l ;
«
}
cout « "Czy chcesz ci n » ch;
kon tyn u ować ( t
l ub n)?";
}
catch(i nt badHeight )
{
cout
«
}
ret urn O:
badHe ight
«
..
cal i jest
pon i ż ej
mi ni mum . "
«
end l :
318
Visual C++ 2005. Od podstaw W p ow y ższym kod zie w bloku t ry znajduj e się pętl a whi l e oraz zagnieżdżony blok t ry, w którym mogą być wywoływane wyj ątk i dwóch typów. Wyj ątek typu const char [ ] zostanie przechwycony przez kl au zulę catc h wewnętrznego bloku t ry, ale dla wyjątku typu in t nie ma skoj arzonej z blokiem t ry klauzuli catc h. W związku z tym wykonywana jest klauzula catch bloku zewnętrznego . W tym przypadku program zostaje natychmiast zakoń czony, ponie waż instrukcja znajdując a s i ę po klauzuli catch to retur n.
ObslUga wyjątków wMFC Nadsz edł
dobry mom ent do poruszenia kwestii wyjątków i biblioteki MFC, ponieważ korzy z nich . Prze s zukując dokument a cj ę dołączon ą do Visual C++ 2005 , mo żemy w spisie treś c i spotkać nazwy TRY , THROWoraz CATCH. Są to makropolecenia zdefiniowane w bibliotece MFC, które utworzono przed imp lementacj ą procedur ob sługi wyjątków w języku C++. Polecen ia te na śladuj ą zachowanie instrukcji t ry, cat ch oraz t hroww C++, ale narzędzia j ęzyk a do obsługi wyj ątków traktują je jako przestarzałe i nie powinno się już ich używać. Są jednak dwa powody, dla których wciąż się tam znajduj ą. Nadal istnieje wiele programów, w których użyto tych makropoleceń, a bardzo ważne jest, aby stary kod działał jak najdłużej . Ponadto więk szość mechani zmów MFC, które mogą powodow ać wyjątki , zostało zaimple mentowan ych przy u życiu tych makropol ec eń . We wszystkich nowych programach powinno si ę u żywać s łów kluczowych t ry, t hrow oraz catch , ponieważ współpracują one z MFC. s ta liś m y ju ż
Jest tylko jedna sprawa, o której trzeba pamiętać podczas używani a funkcji MFC powodują cych wyjątki. Funkcje te zazwyczaj powodują wyjątki typów klasowych (na temat typów kla sowych dowiemy się przed przejściem do MFC) . Mimo że wyjątek spowodowany prLCZ funk cj ę z biblioteki MFC jest dane go typu klasow ego (np. CDBExcept i on), to musimy go przechwyci ć jako wskaźnik, a nie jako typ wyjątku . Jeżel i więc zgło s zony wyjątek je st typu CDBExcept ion, typ będący parametrem klauzuli catch to CDBExcept ion*. W dalszej części książki zobaczymy sytuacje, kiedy to ma zna czenie.
ObslUga blędów przydzielania pamięci Przydzielając pamięć
zmiennym za pomocą operatora new(w rozdziałach 4. i 5.), nie brali pod uwagę takiej ew entualności, że pamięć mogłaby nie dać się przydzielić. Jeżeli nie nastąpi przydzielenie pamięci, to zgłoszony zostaje wyjątek i program zostaje zakończony . Zignorowanie tego wyjątku w wielu sytuacj ach je st całkowi c i e dopu szczalne, ponieważ brak pami ęci dla programu zazwyczaj i tak powoduje jego zamknięcie i nic na to nie m ożemy poradzi ć . Sąjednak sytua cje, w których możemy co ś zrobić , j eśli tylko dostaniemy szansę lub zechcemy zaj ąć się tym problemem na własną rękę. W takiej sytuacj i możemy przechwycić wyjątek spowodowany przez operator new. Stwórzmy przykład, który pokaże nam taką sytuację. śmy
Rozdział 6.•
~
Ostrukturze programu ,.- ciąg dalszy
319
Przechwytywanie wyjątkU spowodowanego przez operator new
Wyj ąt ek spowod owany przez operator new w sytuacji, gdy nic można przydzielić pamięci, jest typu bad_al l oc. Jest to typ klasowy zdefiniowany w standardowym nagłówku , wię c aby go używać , musimy skorzys ta ć z dyrek tywy #i nc l ude. Pon i żej znajduje s i ę cały kod:
II Cw6_06.cpp
#include #incl ude
using st d' :bad_al loc;
using st d: :cout;
using st d: :endl;
II Dyr ektywa dla typu bad_al/ oc.
int main( ) {
char* pdata = O:
size_t count = -s tat ic_cast<size_t>(0)/2:
t ry
(
pdat a = new char[count] : cout « "P a mi ę ć zo s tał a przydzielona " « end l :
}
catc h(bad_al loc &ex)
{
cout
« « «
"Operacj a przydzielania pami ę c i za k oń cz ył a s ię ni epowodzentem . " "Informacj a od obiekt u wyją tk u j est nastę puj ą c a : " eX.what () « endl :
«
endl
}
delet e[] pdata .
return O:
Na moim komputerze rezultat d ziałania tego programu jest następujący:
Operacja przydz lel ania p amięc i zak o ńczyła s ię niepowodzeniem .
Informacja od obiektu wyj ą t k u je st n a s t ęp uj ąc a: bad al locati on
Je śli
masz bardzo
dużo pamięci
w swoim komputerze, to
możesz szczę śliwie uniknąć
tego
wyjątku.
Jak to działa Powyższy
ślony jest
program dynamicznie przydziela pamięć tablicy typu char[], której rozmiar okre przez zmienną count zdefiniowaną za pomocą następującego kodu :
si ze_t count
=
-s t at ic_cast<size_t>(0)/2 :
Rozmiar tablicy jest lic zbą całko w itą typu si ze_t, a więc zmienną count również zadeklaro waliśmy jak o tego typu. Wartość tej zmiennej obliczana jest w dość skomplikowanym wyraże niu. Wartość Ojest typu int , a więc wartość będąca wynikiem wyrażen ia st at ic_cast<si ze_t>(O ) to zero typu size_t . Zastosowanie operatora - powoduje odwrócenie wszystkich bitów
320
Visual C++ 2005. Od podstaw na przeciwne, dzięki czemu otrzymujemy w artoś ć typu S i ze_t, w której wszy stkie bity to l, co odpowiad a maksymalnej wartośc i przechowywanej przez typ S i ze_t , gdyż jest to typ bez znaku. Wartość ta jest w iększa od maksymalnej i l ośc i pamięci , którą może przydzielić ope rator new za jednym razem, a więc dzielimy ją przez 2, aby m ógł sobie z nią poradzić . Jest to jednak nadal bardzo duża warto ś ć i jeżeli nie posiadamy bardzo dużej ilości pamięci w kom puterze , żądanie przydzielenia pamięci zakończy się niepow odzeniem. Przydzielanie pamięci odbywa s i ę w bloku try. Jeżeli opera cja zakoń czy się powodzeniem, to otrzymamy potwierdzającą to wiadomoś ć , ale jeżeli - zgodnie z oczekiwaniami - zak oń czy się niepowodzeniem, to operator new spowoduje wyjątek typu bad_a11 oc. W wyniku tego wywołany zostanie kod klauzuli eateh. Funkcja wh at ( ) wywołana dla referencji ex do obiektu bad_a11oc zwraca łańcuch opisujący problem, który spowodował wyjątek. Rezult at tego wywołania funkcji widzimy na ekranie. Większość klas wyjątk ów ma zaimplementowaną funkcję what() , dostarczającą w postaci łańcucha inform acji na tem at powodu wystąpien i a wyjątku.
z obsługi wyjątków spowodowanych brakiem pamięci , musimy sposób na jej przywrócenie do wolnego obszaru. W większości przypadków zw i ąza ne jest to z dużym wysiłkiem włożonym w pracę nad zarządzaniem pamięcią w programie. Wysiłek ten jest rzadko podejmowany. Aby
odnieść jakąś korzyść
mieć
Przeładowywanie
funkcji
Pr zypu śćmy , że napisaliśmy funkcję znajdującą największą wartość
w tabl icy
zawieraj ącej
liczby typu doubl e: II Funkcj a zn ajdują ca
największą wartość
w tablicy liczb typu double.
doubl e ma xdoub leCdouble array[] . i nt len) (
double max = array[O] : Iort t nt i = 1: i < len: i++ )
i fCmax < array[ i ] )
ma x = array[ i ] :
ret urn max;
Chcemy teraz utworzyć funkcję znajdującą największą wartość w tablicy elementów typu long, a w ięc piszemy drugą funkcję podobną do poprzedniej:
long ma xlong Clong array[]. int len): Nazwy funkcji dobraliśmy w taki sposób, aby odzwierciedlały rodzaj wykonywanego zadania. Podej ś cie to jest jak najbardziej w porządku, gdy mamy do czynienia z dwiema funkcjami. Ale tej samej funkcji możemy potrzebować dla jeszcze innych typów argumentów. Szkoda, że dla każdej z nich musimy wymyślać inną nazwę. Lepiej by było, gdybyśmy mogli używać tej samej nazwy funkcji ma x( ) w odpowiedniej wersji w zależności od potrzeby. Pewnie nie będzie to zaskoczeniem, gdy powiem, że coś takiego jest możliwe. Mechanizm C++, który nam to umożliwia , nazywa się przeładowywaniem funkcji.
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
321
Czym jest przeładowywanie funkcii
Przeładowywan ie
funkcji to mechanizm p ozwalający na użycie tej samej nazwy funkcji do zdefiniowania kilku funkcji o takich samych listach parametrów. Kiedy funkcja taka jest wywoływana, kompilator wybiera właściwą wersję na podstawie podanych argumentów. Oczywiście kompilator za każdym razem, wybierając jedną z wersji funkcji , musi jednoznacz nie określić , która ma zostać użyta, a wi ęc lista parametrów każdej z przeładowanych funkcji musi być niepowtarzalna. Kontynuując z funkcją max( l, możemy utworzyć funkcje przełado wane o nast ępujących prototypach: i nt max(i nt ar ray[ ] . int len): 10n9 ma x(long arra y[ ]. lnt len): double maxtdouble ar ray[ ] . i nt len) :
Wszystkie te funkcje
II Prototypy II funkcji II prz eciążonych.
mają taką samą nazwę,
można rozróżnić dzięki
temu,
że
ale inne listy parametrów. Przeciążone funkcje ich parametry są różnego typu lub mają różną liczbę pa
rametrów. Warto
zauważyć , że różne
wyższego
zbioru nie
typy zwracane nie
double ma x(long array[ ] . int len) :
Powód j est taki, prototyp :
że
odróżniają
możemy dodać poniższej
funkcji tej nie
można
od siebie wersji danej funkcji. Do po
funkcji :
II Nieprawidło we przeładowan ie.
by
było odróżni ć
od funkcji
po siadającej następujący
long ma xI long ar ray[] . i nt l en) :
Zdefiniowanie funkcji w taki sposób spowoduje, o błędzie :
że
kompilator
zgłosi następujący komunikat
err or C2556: ' double ma x(1ong [ J. i nt ) ' : over loaded funct i on dif fer s only by ret urn t ype from ' long max(lo ng [J . int ) '
i program się nie skompiluje. Zasady te mogą wydawać się niezbyt sensowne, dopóki nie przy pomnimy sobie, że możemy pisać instrukcje takie jak ta poniżej : long numbers[] ~ {L 2. 3. 3. 6. 7. 11. 50. 40}; i nt len = siz eof numbe r s/ sizeof number s[O]: maxInumber s . l en) :
Fakt, że wywołanie funkcji max( l w tym przypadku nie ma większego sensu, bo odrzucamy wynik, wcale nie oznacza, że jest ono niedozwolone. Gdyby typ zwracany mógł służyć j ako znak odróżniający, kompilator w poprzednim kodzie nie mógłby się zdecydować, czy wybrać wersję z typem zwracanym lon g, czy doubl e. Z tego też powodu typ zwracany nie może być stosowany jako cecha odróżniająca przeładowanych funkcji . Każda funkcja (nie tylko funkcje przeładowane) ma swoją sygnaturę, którą stanowi jej nazwa oraz lista parametrów. Wszystkie funkcje w programie muszą mieć niepowtarzalne sygnatury. W przeciwnym przypadku program się nie skompiluje.
322
VisIlai C++ 2005. Od pOIlstaw
R!lmI!!mI Stosowanie funkcji przeładowanych Przeładowywanie śmy.
funkcji prze ćwic zymy na przykładzie funkcji maxr ), którąjuż zdefiniowali Wypróbujemy przykł ad zaw ieraj ący trzy jej wersje dla tablic typów i nt , long oraz doub1e.
II Cw6_07.cpp
II Stosowanie funkcji
prze łado wanych.
#i nclude
using st d: :cout :
USing St d: .endl ;
int ma xt int arr ay[] . i nt len); long max(l ong ar ray[] . tnt len) : double max(double array[] . i nt len) ;
i nt lenmedium ~ sizeof med i um/ sizeof med ium[O] ;
i nt lenlarge ~ siz eof large/ si zeof l arge[O] ;
cout « endl « ma x(sma11 . lensma11 ) ;
cout « endl «ma x(medium . lenmedium) ;
cout « endl « max(large. lenlarge) ;
cout « endl ;
retu rn O;
II Maksyma łn a liczba liczb typu int. int ma xt tnt x[], int len)
{
i nt max ~ x[O ].
for (i nt i ~ l ; i < len: i ++ )
if(max < x[i ] )
max ~ xCi ] :
ret urn max;
II Maksym a/na liczba liczb typu long.
long max(long xC]. i nt len) {
long ma x ~ x[O] ;
for( i nt i ~ 1: i < len; i ++ )
i f( max < xCi ] )
max ~ x[i ] ;
retur n max;
II Maksym a/na liczba liczb typu double.
double ma x(doub le xC ]. int len)
{
double max
~
x[O],
Rozdzial6.• ostrukturze programu for(int i = 1; i < le n: i f'(max < xl i l )
max = xl i l:
ret urn max:
Program
działa
tak, jak
ciąg
dalszy
323
1++ )
się spodziewaliśmy,dając następujący rezultat:
34 2345 345.5
Jak lo działa W kodzie znajdują się prototypy wszystkich trzech przeładowanych funkcji ma xt ). W każ dej instrukcji wyjściowej kompilator na podstawie listy parametrów wybiera odpowiednią wersję funkcji max( l . Wszystko działa jak należy, gdyż każda z tych funkcji ma niepowtarzalną sygnaturę dzięki innej niż u pozostałych liście parametrów.
Kiedy stosować przeładowywanie 'Iunkcii Przeładowywanie
funkcji pozwala nam upewnić się, że nazwa funkcji opisuje wykonywaną i nie jest ona mylona z inform acjami zewnętrznymi, takimi jak typ przetwarzanych danych . Jest to zbliżone do tego, co się dzieje z podstawowymi operacjami w C++-. Aby dodać do siebie dwie liczby, zawsze używamy tego samego operatora , bez względu na typ operan dów. Nasza przeładowana funkcja ma x( l ma taką samą nazwę, bez względu na typ przetwarza nych danych . Powoduje to, że kod jest bardziej przejrzysty i łatwiej jest używać tych funkcji . funkcję
Cel przeciążania funkcji jest jasny : chodzi o to, aby móc wykonywa ć tę sam ą opera cję przy użyciu różnych operandów, używają c jednej naz11Y funkcji. Zatem za każdym razem, gdy mamy klikafunkcji wykonujqcych to samo zadanie, ale z różnymi typami argumentów, powinniśmy j e przeładować i użyć wspólnej dla wszystkich nazwy.
Szablony funkcji Ostatni przykład był dosyć uciążliwy pod tym względem , że dla każdej funkcji musieliśm y przepisywać ten sam kod, zmieniając tylko typ zmiennej i parametrów. Na szczę ście można tego uniknąć . Istnieje możliwość utworzenia ogólnego przepisu, na podstawie którego kompi lator będzie automatycznie tworzył funkcje , stosując różne typy parametrów. Kod definiujący taki przepis generuj ący określoną grupę funkcji nazywa się szablonem funkcji . Szablon funkcji posiada jeden lub większą liczbę parametrów typu, a określona funkcja generowana jest poprzez dostarczenie argumentu typu dla każdego z parametrów szablonu. W związku z tym wszystkie funkcje tworzone na podstawie jednego szablonu zawierają ten
324
Visual C++ 2005. Od podstaw sam podstawowy kod, który jest dopasowywany do indywidualnych potrzeb za pomocą poda nych argument ów. Aby zobaczyć, jak to działa w praktyce, możesz zd efiniować szablon funkcji max () z poprzedniego przykładu .
Stosowanie szablonu lunkcji P oniżej
znajduje
si ę
definicja szablonu dla funkcji ma x( ):
t emplat e T max(T xl l
i nt len)
(
T max = x[OJ:
t ort i nt i = 1: i < len: i++ )
if(max < x[i] )
ma x = x[iJ:
ret urn max:
S łowo
kluczowe template informuj e, że jest to sza blon funkcji. W trójkątnych nawiasach kluczowym t empl ate znaj d ują s i ę oddzielone przecinkami parametry typów, które s ą używane do utworzenia danej funk cji z szablonu . W tym przypadku podany został tylko jeden parametr określający typ T. S ł ow o kluczowe c l ass znajdując e s i ę przed T oznacza, że litera ta jest parametrem typu tego szablonu - c l ass jest rodzajowym terminem określ ającym typ. W dalszej części książki dowiemy się, że definiowanie klasy jest równoznaczne z definio waniem własnego typu danych. W rezultacie w C++ mamy typy fund am entalne, takie jak i nt i char, oraz typy zdefiniowane samodz ielnie. Warto zwrócić uwagę , że zamiast słowa klu czowego clas s do identyfikowania parametrów w szablonie funkcji można użyć słowa kluczo wego typename. W takim przypadku definicja szablonu wyglądałaby następująco: po
sł owie
template T max (T xC] . int len) (
T max = x[OJ:
for(i nt i = 1: i < len: i ++)
if (max < xl i l )
ma x = x[i J:
ret urn max:
Niektórzy programiści wolą u żywać s łowa kluczowego t ypename, jako że słowo kluczowe cl as s zazwyczaj oznacza typ zdefiniowany przez użytkownika, natomiast typename jest bar dziej neutralne i dzięki temu łatwiej z niego wywnioskować, ż e oznacza ono zarówno typy fundamentalne, j ak i zdefiniowane przez programi stę . W praktyce oba te słowa kluczowe uży wane są bardzo często. Wszystkie przypadki wystąp i e n ia' symbolu T w definicji szablonu funkcji zamieniane są na określony argument typu, taki jak l ong, podawany przy tworzeniu egzemplarza funkcji . J eśli własnoręczni e wstawimy l ong w szędzie tam , gdzie znajduje się litera T, to przekonamy się , że otrzymamy w pełni sprawną funkcję obliczając ą największą warto ś ć w tablicy typu l ong:
long max(l ong x[J . i nt len ) {
long ma x = x[OJ:
for (i nt i = 1: i < len: i ++)
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
325
if(max < xCi ] )
max ~ xCi ] ;
ret urn max:
Tworzenie
określonej
funkcji z szablonu nazywa
się konkretyzacją.
Za każdym razem, gdy w programie używamy funkcji maxt ), kompilator sprawdza, czy funk cja odpowiadająca typowi argumentów, których użyliśmy do jej wywołania, rzeczywiście ist nieje. Jeżeli wymagana funkcja nie istnieje, kompilator tworzy ją, wstawiając typ argumentu użyty w wywołaniu funkcji w każde miejsce, w którym pojawia się parametr T w kodzie defi nicji szablonu. Możemy przećwiczyć szablon funkcji max() przy użyciu tej samej funkcji mai n( ) co w poprzednim przykładzie.
R!lmmJI Slosowanie szałllonu funkcji Poniższy kod stanowi zmodyfikowaną wersję popr zedniego szablonu funkcji ma x( ) :
II Cw6_08.cpp
II Stosowanie szablonów funkcji.
#i nel ude
using std ; ;cout ;
using st d; ;endl ;
II Szablon funkcji znajdującej najwię kszy element tablicy.
int lensmal l = sizeof sma l l /slzeof smal l [O] ;
int lenmedium = slzeof medi um/ si zeof medi um[O] :
i nt lenlarge = si zeof large/ si zeof large[O] :
cout « endl « max (sma ll . lensmall) ;
eout « endl « max(medium. l enmedium ) ;
eout « endl « max(large. lenlarge) :
eout « endl ;
ret urn O:
Program ten daje identyczny wynik jak w poprzedniej wersji .
przykładu
z wykorzystaniem
326
Visual C++ 2005. Od podstaw
Jak to IJziala Dla każdej instrukcji wy s yłającej na wyjści e najwi ększ ą w arto ść w tablicy twor zona jest z szablonu nowa wersja funkcji max(). Oczywi ście , j e że li dodamy je szcze jedną instrukcj ę wywołującą funkcję max() z jednym z typów wcześniej używany ch , to nowa wersja kodu nie zostanie wygen erow ana . Warto zauważyć , że używani e szablonów w żaden sposób nie wpływa na zmniej szenie roz miaru skompil owanego programu. Kompilator twor zy nową wersję kodu dla każdej funkcji, której chcem y uży ć . W rzec zywi stości używanie szablonów m oże nawet spowodować zw i ę k szenie rozmiaru programu , gdyż nowe funkcje m ogą by ć tworzone nawet w takich przy padkach, gdzie w zupełno ści wy starczyłaby odpowiedni a konwersja typów. Aby wymu si ć utworzenie określonych egzemplarzy szablonu , należy dołączy ć odpowiednią deklarację. Je żel i na przykład chcem y mie ć pewność, że zostanie utworzona funkcj a max ( ) w wersji z typem fl oat, to po defini cj i szablonu moglibyśmy zastosować poniżs z ą d eklarację:
fl oat max(fl oat . 1nt ) : Powyższa
deklaracja wymusi stworzenie tej wersji funkcji. Nie wnosi ona zbyt wiele do nasze go programu. Może s i ę jednak przydać , gdy wiem y, że może zosta ć wygenerowanych kilka wersji funkcji szablonowej, a my chcemy wymusi ć wygenerowanie podzbioru , którego planu jemy użyć z argumentami przekonwertowanymi w razie koni eczności do odpowiedni ego typu.
Przykład uiywania funkcji Nauczyliśmy się już
wielu podstawowych technik programowania w C++, włączając w to o funkcjach zdobytą w tym rozdziale . Po przebrni ęciu przez różn e narzędzia dos tępne w danym języku czasam i ni eł atwo jest zorientować s i ę jak one mają się do siebie. Nadszedł więc czas na stworzenie czegoś bardziej treściwego ni ż prosty program demonstracyjny. wiedzę
Rozpracujemy bardziej realistyczny przykład w celu zapoznani a si ę z m etodologią rozbijania problemu na poszczególne funkcje . Na proces ten składają s i ę : defin icja problemu do ro zwią zania, analiza problemu w celu okreś len ia, jak można dokonać jego implementacji w CH , oraz pisanie kodu. Stosuję tutaj podejście mające na celu zilustrowanie sposobu, w jaki funk cje, dz iałając razem, doprowadzają do wyniku końcowego. Nie jest to poradnik , jak stworzyć program.
Implementacja kalkulatora Przypuśćmy, że
potrzebujemy programu działającego jak kalkulator. Nie mam tu jednakna jednego z tych urząd zeń i ustrojstw naszpikowanych przyciskami i zaprojektowanych dla osób łatwo dających się zadowoli ć . Nasz program je st dla tych, którzy wiedzą, czego chcą z arytm etycznego punktu widz enia . Mo żna wpisać do niego c ał e wyrażenie arytmetyczne i natychmiast otrzymać wynik . Poniżej pokazane jest przykładowe wyrażenie, które będzie można do niego wpi s ać: my śli
Rozdział 6.•
2*3.14159*12.6*12 .6 / 2
+
Ostrukturze programu -
ciąg
dalszy
327
25.2*25.2
Aby uniknąć zbędnych komplikacji, program nie będzie akceptował nawiasów w wyrażeniach, a całe liczenie musi odbyć się w jednym wierszu. Będzie za to można umieszczać spacje w dowolnych miejscach, aby użytkownik mógł uatrakcyjnić wizualnie wprowadzane przez siebie dane. Wprowadzane wyrażenia mogą zawierać operatory mnożenia, dzielenia, doda wania i odejmowana, reprezentowane odpowiednio za pomocą symboli *, /, + oraz -. Wartość wyrażeń będzie obliczana z zastosowaniem normalnych zasad arytmetycznych, czyli na począt ku wykonywane są mnożenie i dzielenie, a po nich dodawanie i odejmowanie. Program będzie pozwalał na wykonywanie dowolnej liczby obliczeń, a jego zamknięcie nastąpi w chwili wprowadzenia pustego wiersza. Będzie także wyświetlał pomocne i przyjazne komu nikaty o błędach.
Analiza problemu Dobrym miejscem do rozpoczęcia jest wprowadzanie danych. Program przyjmuje wyrażenia arytmetyczne o dowolnej długości, które są zapisane w jednym wierszu. Mogą być one dowol nie skonstruowane w obrębie podanych warunków. Jako że nie da się z góry ustalić, co jest czym w takim wyrażeniu, musimy je najpierw wczytać jako łańcuch znaków, a następnie rozszyfrować jego strukturę wewnątrz programu. Możemy zdecydować, że obsługiwane będą łańcuchy o długości do 80 znaków, które będą przechowywane w tablicy zadeklarowanej za pomocą poniższych instrukcj i:
const int MAX ~ 80 ; char buffer[MAX] ;
II Maksymalna długość wyrażenia włącznie ze znakiem 'lO'. II Miejsce na wyrażenie, którego wartoś ć ma zostać obliczona.
Aby zmienić maksymalną liczbę znaków łańcucha przetwarzanego przez program, wystarczy tylko zmodyfikować wartość początkową zmiennej MAX. Musimy
zrozumieć podstawową strukturę
ściowym.
informacji, które Przeanalizujemy ją krok po kroku.
mogą pojawić się
w
łańcuchu
wej
go maksymalnie uprościć. W tym celu analizy usuwamy wszystkie spacje. Funkcję wykonującą tę czynność możemy nazwać eat spacesO. Funkcja ta przeszukuje całą zawartość bufora wejściowego czyli tablicę buffer [] - i nadpisuje wszystkie spacje. Proces ten wymaga użycia dwóch indek sów do tablicy buforowej i oraz J i rozpoczyna się na początku bufora. Zazwyczaj element j będzie przechowywany w lokalizacji i, a więc spacja znajdująca się w i zostanie nadpisana przez następny znak, który znajduje się w lokalizacji indeksowej j, która nie jest spacją. Dzia łania te przedstawiono na rysunku 6.2.
Przed przed
rozpoczęciem przetwarzania łańcucha należy rozpoczęciem
Proces ten polega na skopiowaniu zawartości tablicy do niej samej, odrzucając spacje. Na ry sunku 6.2 widać tablicę buffer przed kopiowaniem i po nim. Strzałki wskazują kopiowane znaki oraz miejsca, do których zostały one wstawione.
328
Visual C++ 2005. Od pOlIslaw Tablica buffer przed
operacją
samokopiowania
indeksj Indeks i nie jest zwiększanyw tych miejscach, gdyż zawierają one spacje, które zo stają nadpisane _ _ prze z znajdujące s ię po nich w tablicy buffer znaki nie będą ce spacjam i
indeks i Tablica buffer po operacji samokopiowania
Rysunek 6.2 Po usunięciu spacji z wyrażenia mo żemy zabrać się za obliczanie jego wartości. Definiujemy zatem funkcję expr() zwracającą wynik całego wyrażenia w buforze wejściowym . Aby podjąć decyzję , co dokładnie ma robi ć funkcja expr O, musimy dokładniej przyjrzeć się strukturze danych wejściowych. Operatory dodawania i odejmowania mają najniższy priorytet i wyko nywane są na samym końcu. Łańcuch wejściowy możemy traktować jako składający s i ę z jed nego lub większej liczby składników połączonych operatorami + lub -. Każdy z tych operato rów możemy określić słowem operdod. Za pomocą tej terminologii ogólną postać wyrażenia wejściowego możemy przedstawić w następujący sposób: wyrażenie :
sk ładnik
operdod
składn i k
... operdod
s k ła dn ik
Wyrażenie zawiera co najmniej jeden składnik i może mieć dowolną liczbę następujących po nim kombinacji "operdod składnik" . Jeżeli usunęliśmy wszystkie spacje, to po każdym skład niku może znajdować się jeden z trzech możliwych znaków:
• Znak \0
oznaczający , że doszliśmy
• Znak - oznaczający, już do tej pory części
do
końca łańcu cha .
że następny składnik należy odjąć
od
wartości
obliczonej
wyrażenia.
• Znak + oznaczający, że n astępny już do tej pory części wyra żen ia.
składnik należy dodać
do
wartości
obliczonej
Jeżeli po skład n i ku znajduje się coś innego, oznacza to, że należy wyświetlić komunikat o błę dzie i zakończyć program. Na rysunku 6.3 widoczna je st struktura przykładowego wyrażenia.
Rysunek 6.3
Koniec danych
operator dodawania lub odejmowania
operator dodawania lub odejmowania
wejściowych
Rozdzial6.• ostrukturze programu- ciąg dalszy
329
Następnie potrzebujemy bardziej szczegółowej i dokładnej definicji składnika. Składnik to szereg liczb połączonych operatorami * lub I . A zatem ogólna forma składnika przedstawia się następująco: s kła dn ik :
l ic zba operato rmd l i czba
oper atormd l i czba
operatormd reprezentuje zarówno operator mnożenia, jak i dzielenia. funkcję
Możemy zdefiniować
t erm() , która będzie obliczała wartość składnika. Funkcja ta będzie skanowała łań
cuch w poszukiwaniu pierwszego łańcucha liczbowego, po którym znajduje się operator mno żenia lub dzielenia i następna liczba. W momencie napotkania czegoś innego niż operat ormd, funkcja uznaje, że jest to operator dodawania lub odejmowania, i zwraca wartość obliczoną do tego momentu . Ostatnia rzecz, którą musimy zrobić, to znaleźć sposób na rozpoznawanie liczb. W celu mak symalnego uproszczenia kodu nasz program będzie rozpoznawał tylko liczby bez znaku. A więc liczba to zbiór cyfr, po których może wystąpić przecinek oddzielający część dziesiętną oraz jeszcze więcej cyfr. W celu określenia wartości liczby przeszukujemy bufor, aby odnaleźć cyfry . Je śli znajdziemy coś, co nie jest cyfrą, to sprawdzamy, czy jest to przecinek dziesiętny. Jeżeli nie jest, to oznacza to, że znak ten nie ma nic wspólnego z wartością liczby, i zwracamy to, co znaleźliśmy do tego momentu. Jeżeli natrafimy na przecinek, to szukamy więcej cyfr. Po natrafieniu na znak nie będący cyfrą mamy cał ą liczbę i ją zwracamy . Funkcję rozpoznającą liczby i je zwracającą nazwiemy numbe r ( ). Na rysunku 6.4 pokazano rozbicie przykładowego wyrażenia na składniki i liczby .
operator dodawania lub składnik odejmowania
składnik
r: .J~
(- - - - - A---~..,
~
(Y~k:aP k a cYf r~
liczba
liczba
mnożenie
liczba
~
Koniec danych wejściowych
Rysunek 6.4 W tej chwili mamy już wystarczająco dużo informacji na temat problemu, aby rozpocząć pisa nie kodu. Możemy zacząć od potrzebnych nam funkcji, które następnie połączymy w jeden program w funkcji main( ). Pierwszą i prawdopodobnie najłatwiejszą do napisania funkcją jest funkcja eat spaces (), która ma za zadanie usuwać wszystkie puste miejsca z łańcucha wej ściowego.
330
Visual C++ 2005. Od podslaw
Usuwanie spacji złańcucha Prototyp funkcji eat spaces ()
może być następuj ący :
void eat spaces(c har* str ):
II Funk cj a
usu wając a
spacje.
Funkcja ta nie musi nic zwracać, gdyż spacje można u sunąć, bezpośrednio modyfikując łań cuch za pomocą w skaźnika przekazanego jako argument. Sam mechanizm usuwania spacji je st bardzo prosty . Kopiujemy łańcu ch do samego siebie, zastępując spacje znakami, jak opi syw a l i ś my wcześniej. Poni żej
znajduje
II Funkcja
się przykładowy kod
usu wają ca
takiej funkcji:
spa cj e z łańcuch a.
vo i d eat spaces (char* st r)
{
i nt i = O: O: while ((*(st r + i ) = *(str + if( *(st r + i) 1= ' ') i nt j =
j ++)) I~
i++ .
' \ 0' )
II Kopiowanie do - indeks do lancucha. II Kopiowanie z - indeks do łańcu cha. II Pętla >rykonywana, j eżeli znak nie jest 10. II Zwiększaj i, dopóki nie IIjest spacją.
ret urn:
Jak działa ta funkcja Całe działanie
tej funkcji zostało zawarte w pętli whi Ie. Jej warunek kopiuje łańcuch, przeno znak znajdujący się w lokalizacji j do lokalizacji i, a na stępnie zw i ę ksza j do następnego znaku . Jeżeli skopiowany znak jest znakiem \ 0, oznacza to, że o siągn ięto koniec łańcuch a . sząc
Jedyną czynnością wyko n ywa ną
w instrukcji pętli jest zwiększanie i do następnego znaku, ostatni skopiow any znak nie był spacj ą. J eżeli był spacją, to i nic zostaje zwiększone, dzięki czemu spacja zostaje nadpisana przez znak skopiowany w następnym powtórzeniu pętl i .
je żeli
Jak jąca
widać,
nie było to trudne zadanie . Następna fu nkcja, którą napiszemy , to funkcja zwraca wynik obliczania wartości wyrażenia.
Obliczanie wartości wyrażenia Funkcja expr() zwraca wartość wyrażenia zawartego w a więc jej prototyp przedstawia się następująco: double expr(char* st r );
łańcuchu
II Funkcj a ob liczająca
podan ym jako argument,
wartość wyrażenia.
Zadeklarowana powyż ej funkcja przyjmuje jako argument łańcuch i zwraca wynik w postaci liczby typu doubl e. Proce s obliczania wartości wyrażenia przedstawić można na logicznym diagramie, utworzonym na podstawie naszych wcześniejszych rozważań na ten temat. Diagram ten widoczny je st na rysunku 6.5.
Rozdzial6.• ostrukturze programu -
ciąg
dalszy
331
Rysunek &.5 Znajdź wartość
pierws zego
wskaź n ika
Ustaw wartoś ć na wa rtość pierwszego składnika
wyrażenia
Następnym
Zwró ć wartość
znakie m jest 'lO'?
wyrażeni a
N a stępnym
Odejm ij wartoś ć
znakiem jest '-'?
następnego składnika
od
Dodaj
N a stępnym
znakiem jest '+'?
wartości wyrażen ia
warto ść
na stępnego składn ika
do
wartości w y ra że n i a
BŁĄD
Przy
użyciu
II Funkcja
podstawowej defin icj i tego mechanizmu
obliczająca wartość wyrażen ia
możemy napisać następującą funkcję:
ary tmetycznego.
double expr(cha r* st r) (
double value = 0.0: i nt i ndex ~ O:
II Do przechowywania wyniku. II Śledz i lokalizację bieżącego znaku.
value = termr st r . index) :
II Pobi erz pierwszy znak.
t or (: : )
II Pętla nieskończona, wszystko odbywa się II we wną trz.
switch(*(str + index++))
II Podej mij działan ia na podsta wie II bie ż qcego znaku .
case ' \0 " retu rn val ue:
II Jest eśmy na końc u łańc ucha, II a więc zwracamy, co mamy.
332
Visual C++ 2005. Od podstaw case v- ": val ue += t ermCst r, i ndex): break:
II Znaleziono znak +, a II nast ępny skladnik.
case ' - ' :
II Znal eziono znak -, a II odejmuje my II następny skladnik.
value -~ t erm(str . i ndex) : break : default : cout « end l « «
"Arrrgh l *#1i Tu j est
wię c
dodaj emy
wię c
II Jeśli doszliśmy tutaj , to II zna czy 10 , że lańcu ch jest II niepra widlowy. błą d!"
endl :
exit (l) ;
Jak dziala lafunkcja Biorąc pod uwagę, że funkcja ta potrafi przean alizować każde wyrażenie arytmetyczne, które raczymy jej podsunąć (pod warunkiem że używamy w nim operatorów z naszego zbioru), nie zawiera ona bardzo du żo kodu. Definiujemy zmienną typu i nt o nazwie i ndex, śledzącą na szą bieżącą lokalizację w łańcuchu. Zainicjali zowaliśmy ją wartością O, która odpowiada indeksowi pierws zego znaku w łańcuchu . Zdefiniowaliśmy także zmienną typu doubl e, w któ rej będziemy przechowywać wartość wyrażenia przeka zanego do funkcji w tablic y typu char o nazw ie st r.
Jako że każde wyrażenie musi mieć co najmniej jeden składnik, pierwsząnaszą czynnościąjesl pobranie wartości pierw szego składnika za pomocą funkcji t erm. ), którą dopiero napiszemy. W związku z tym istnieją trzy wym agania dotyczące funkcji t erm( ) :
l Powinna przyjmować wskaźnik typu char* oraz zmienną typu i nt jako parametry, Drugi parametr powinien
być
indeksem pierwszego znaku
składnika
dostarczonego
łańcucha .
2. Powinna uaktualniać wartość przekazywanego indek su, aby n a stępującemu
po ostatnim znaku
3. Powinna zwracać Resztę
odpowiadał
znakowi
składnika.
wartość składnika w
postaci liczby typu doubl e,
programu stanowi nieskończona pętla fo r . Wewn ątrz tej pętli wszelkie d zi ałania kontrolowane są przez instrukcję switch, która z kolei podejmuje decyzj e na podstawie bie żącego znaku w łańcuchu. Jeżeli j est to znak +, to wywoływana jest funkcja term ( ) w celu obliczenia wartoś ci następnego składn i ka wyrażenia i dodania jej do zmiennej val ue. Jeże li jest to -, to od zmiennej val ue odejmujemy wartoś ć zwróconą przez funkcj ę t erm t ). W przy padku znaku \ 0 zwrócona zostaje bieżąca wartość zmiennej val ue, gdyż oznacza to, że doszli śm y do końca łańcucha . Jeżeli napotkany znak jest jeszcze inny, wyświetlona zostaje repry menda dla użytkownika za wpi sanie niewła ściwych znaków i program kończy działanie,
Rozdział 6.•
Pętla
Oslrukturze programu -
ciąg
dalszy
333
dopóki znajduje znak + lub -. Każde wywołanie funkcji t er m( l powoduje zmiennej index do znaku następującego po składniku, którego wartość została obliczona, i powinien to być albo znak + czy -, albo znak końca łańcucha \ 0. A zatem funkcja może zakończyć działanie w normalny sposób po napotkaniu znaku \ 0 lub wyjąt kowo poprzez wywołanie funkcji exit (l . Przy składaniu całego programu należy pamiętać o dyrektywie #i ncl ude dołączającej plik nagłówkowy , który zawiera definicję funk cji exit ( l . powtarza
się,
przesunięcie wartości
Wyrażenie arytmetyczne moglibyśmy również przeanalizować za pomocą funkcji rekuren cyjnej . Definicję wyrażenia możemy także przedstawić w trochę inny sposób, określając je jako składnik lub składnik , po którym następuje wyrażenie . Jest to definicja rekurencyjna (ponieważ zawiera to, co jest opisywane) - często spotykany sposób definiowania struktur języków programowania. Jest ona tak samo elastyczna jak pierwsza, ale przy jej zastoso waniu otrzymalibyśmy rekurencyjną wersję funkcji expr ( l, zamiast - jak w powyższym przykładzie - używać pętli . To alternatywne podejście można potraktować jako ćwiczenie po ukończeniu wersji finalnej programu.
Obliczanie wartości składnika Funkcja t erm( l, przyjmując dwa argumenty w postaci łańcucha do zanalizowania oraz indeksu lokalizacji w łańcuchu , zwraca wartość składnika w postaci liczby typu doub l e. Są także inne sposoby dojścia do tego celu, ale ten jest bardzo prosty . W związku z tym prototyp funkcji t er m( l może wyglądać następująco: bieżącej
doub le t erm(char* str . int & i ndex) :
II Funkcj a analizują ca składn ik.
Drugi parametr określiliśmy jako referencję. Zrobiliśmy to, ponieważ chcemy, aby ta funk cja mogła modyfikować wartość zmiennej i ndex w programie wywołującym, ustawiając ją na znaku znajdującym się za ostatnim znakiem składnika, znalezionym w łańcuchu wejściowym . Moglibyśmy także zwrócić zmienną i ndex jako wartość, ale wtedy musielibyśmy zwrócić wartość składnika w jakiś inny sposób, a więc wybór ten wydaje s i ę całkiem uzasadniony. Sposób analizowania składnika ma podobną strukturę do tej, której użyliśmy dla wyrażenia . Składnik jest liczbą, po której prawdopodobnie następuje jedna lub więcej kombinacji opera torów mnożenia lub dzielenia z innymi liczbami . Definicję funkcji term( l możemy napisać w następujący sposób: II Funkcj a znajdująca
wartość składnika.
double t erm (char* str . int & i ndex) (
doub le value = 0.0: value ~ number (str. index) : II Powtarzaj
aż
II Przechowuj e wynik. II Pobierz p ierwszą cyfrę składnika.
do znalezienia odpowiedniego opera/ ara.
while((*(st r + i ndex)
~=
'* ' ) II (*(str + i ndex) ==
'I' ) )
(
ł i ndex) ~= '*' ) value *= number (str . ++index) ;
if( *( s t r
II Jeśli jest to operator mnożenia, II to pomnóż przez następną liczbę.
334
Visual C++ 2005. Od podstaw if (*(str + i ndex) == '/') val ue /= number (st r . ++l ndex);
II Jeśli j est to op erator dzielenia, II to podziel przez n astępną liczbę.
}
ret urn va lue ;
II Skończone.
więc
zwra camy, co mamy.
Jak działa ta funkcja doubl e o nazwie val ue, w której będz iemy prze Jako że składnik musi zawiera ć co najmni ej jed ną liczbę, pierw szą n aszą c zynności ą je st sprawdzenie wartości pierwszej liczby poprzez wywo łanie funkcji number ( ) i zapi sanie jej do zmiennej val ue. Zakładamy, że fu nkcja number O przyjmuj e jako argum enty łań cu ch oraz indeks lokalizacji w łańcuchu oraz zwrac a warto ść znalezionej liczby. Ze względu na fakt , że funkcja number( ) musi także uaktualnić indeks w łańcuchu do lokalizacji znajdującej si ę za znalez i on ą liczbą, drugi parametr znowu określim y jako referencję podczas pisania tej defini cji.
Najpi erw deklarujemy
z mie n n ą lokalną typu
chowywać warto ś ć bi eżąc ego składnika.
funkcji term( ) stanowi pętla whi l e, która powtarza się, dopóki nie znajdzie znaku * lub l . Jeżeli pętl a znajd zie w bi e żąc ej lokali zacji w łańcuchu znak *, to zwi ększy o l zmi enn ą i ndex, aby wskazywała ona początek następnej liczby, wywoła funkcję number( ) w celu spraw dzenia w arto ści następnej liczby , a nast ępnie p omno ży przez zwróconą w arto ś ć zawartość zmiennej val ue. Podobnie , j eże l i b ieżącym znakiem okaże się znak /, zmienna i ndex zostanie zwiększona o jeden, a zawarto ś ć zmiennej val ue zostanie podzielona przez wartość zwró coną przez funkcję number( ). Jako że funkcja number () automatycznie zmienia wartość zmiennej i ndex, aby wskazywała znak następuj ący po znalezionej liczbie, zmienna ta zostaje natych miast ponownie ustawiona na następny znak do stępny w łańcuchu w kolejnym powtórzeniu. Resztę
Pętla przerywa działanie w momencie napotkania znaku innego niż operator mnożenia lub dzielenia, podczas gdy bieżąca warto ść składnika przechowywana w zmiennej val ue zwrócona zostaje do programu wywołującego.
Ostatnią potrzebną nam funkcją analityczną jest bową
funkcja nurnber ( ), która znajduje
wartoś ć
licz
wszystkich liczb w łańcuchu .
Analizowanie liczby Biorą c
pod uwagę sposób , w jaki prototyp musi by ć następujący:
używaliśmy
double number (char* st r. int &index) : Określenie
argumentu
funkcji number O
w ewn ątrz
funkcji t ermO, jej
II Funkcja rozpoznająca liczby .
drugiego parametru jako referencji umo żl iwia funkcji aktualizowanie w programie wywołującym, a tego właśn ie nam potrzeba.
warto ści
bezpośrednio
W tym miejscu możemy użyć funkcji dostępnej w bibliotece standardowej C++. W pliku nagłówkowym znajdują się definicje kilku funkcji sprawdzających pojedyncze znaki. Zwracają one wartości typu i nt, gdzie liczby dodatni e odpow iadają warto ści logicznej true, a ujemne f al se. Cztery z tych funkcji zostały przedstawione w poniższej tabeli:
Rozdzial6. •
ostrukturze programu -
ciąg
dalszy
335
Funkcie wnag!ówku slUzące do sprawdzania pojedynczych znaków i nt is al pha(int e)
Zwraca tr ue, jeśli podany argument jest znakiem w przeciwnym przypadku.
l nt is upper(in t e)
Zwraca t rue, jeśli podan y argument jest wielką literą, lub false w przeciwnym przypadku.
i nt is lower( int e)
Zwraca true, j e ś li argument jest małą literą, lub false w przeciwnym przypadku.
l nt isdigit(int e)
Zwraca t rue, jeśli podany argument jest c yfrą, lub fa l se w przeciwnym przypadku.
należącym
do alfabetu , lub false
zdefiniowano więcej funkcji, ale nie będę ich tutaj szcze chcesz wiedzieć więcej na ich temat, to poszukaj frazy ; is routines " w bibliotece pomocy MSDN. W pliku
nagłówkowym
gółowo opisywał. Jeśli
W naszym programie potrzebujemy tylko ostatniej z opisanych powyżej funkcji. Należy zapa miętać, że funkcja isd i qit O sprawdza znaki , jak np. ,,9" (w standardzie ASCII znak 57 w no tacji dziesiętnej) , a nie numeryczną wartość 9, ponieważ dane wejściowe są w postaci łańcucha. Funkcję
numberC )
możemy zdefiniować
w
następujący
sposób:
II Funkcja rozpoz nająca liczby w lancuchu.
double number (ehar* str . int& lndex) {
double value = 0.0;
II Przechowuje wynik.
whi le(i sdigit (*(st r + index)) ) val ue - 10*va lue + (*(st r + index++ ) - ' O' ) ;
II Pętla zbierająca cyfry
if(*(st r + index)
,~
' .' )
retu rn val ue ; dou ble fact or = 1.0; whi le(i sdigit (*(st r + (++ index) )))
wiodące .
II Skoro jesteśmy tutaj, to znaczy,
II że napotkaliśmy znak,
II który nie jest cyfrą.
II Sprawdzamy. czy j est to prz ecinek II dziesiętny, II i jeśli nie, to zwracamy wart ość II zmiennej value. II Wsp ółczynnik miej sc dzies iętnych . II Powtarzaj. dopó ki są cyfry.
{
faetor *= 0.1; value = val ue + (*(st r + i ndex) - ' O') *faetor ;
II Zmniejsz współczynnik II dzies ięciokrotnie. II Dodaj miej sce po przecinku.
}
ret urn val ue ;
II Zakończenie p ętli i wynik.
Jak dZiała la flmkcja Deklarujemy zmienną lokalną val ue typu doubl e i przechowujemy w niej znalezioną liczbę. lnicjalizujemy ją wartością O. O, ponieważ cyfry będą dodawane w trakcie pracy pętli .
336
Visual C++ 2005. 011 podstaw w łańcuchu stanowi ciąg cyfr w postaci znaków ASCII, funkcja przechodzi przez liczby cyfra po cyfrze . Wykonywane jest to w dwóch etapach w pierwszym zbierane są cyfry przed przecinkiem, a w drugim, jeżeli zostanie znaleziony przecinek, cyfry za nim.
Jako
że liczbę
łańcuch, obliczając wartość
Pierwsza faza wykonywana jest w pętli whil e, która powtarza się, dopóki bieżący znak wska zywany przez zmienną i ndex jest cyfrą. Wartość cyfry jest sprawdzana i dodawana do zmien nej value w poniższej instrukcji pętli: val ue
~
lO*value
+
(*(st r
+ r ndex- «) -
'O' ) ;
Sposób, w jaki jest ona zbudowana, wymaga bliższego przyjrzenia się . Znaki ASCII mają od 48 odpowiadającej cyfrze ,,0" do 57 odpowiadającej cyfrze ,,9". Jeżeli zatem odejmiemy kod ASCII cyfry "O" od kodu innej cyfry, to przekonwertujemy ją na jej odpo wiednik w zapisie cyfrowym od do 9. Choć nie jest to konieczne, podwyrażenie *(str + i ndex++ ) - ' O' ; umieściliśmy w nawiasach, aby było bardziej przejrzyste. Zawartość zmien nej val ue mnożymy przez 10 w celu przesunięcia wartości o jedno miejsce po przecinku w lewo przed dodaniem wartości cyfry, ponieważ są one ustawione od lewej do prawej to znaczy najbardziej znaczące cyfry występują jako pierwsze. Proces ten został zilustrowany na rysunku 6.6. wartości
°
Cyfry w liczbie
~ 5 Kody ASCIIw zapisie
3
dziesiętnym
Wartość początkowa
Pierw sza cyfra
value
Druga cyfra
value
=O
=1O"value + (53 - 48) = 10"0 .0 + 5
=5,0
=10"value + (49 - 48) = 10"5 .0 + 1
= 51.0
Trzecia cyfra
value = 10"value + (51 - 48) 10"51.0 + 3
513.0
= =
Rvsunek 6.6 Jeżeli natrafimy na coś innego niż cyfra, to jest to albo przecinek, albo coś innego. Jeśli nie jest to przecinek, to znaczy, że skończyliśmy, i zwracamy zmienną val ue do wywołującego pro gramu. Jeśli jest to przecinek, to zbieramy znajdujące się za nim cyfry ułamkowe za pomocą drugi ej pętli . W pętli tej używamy zmiennej fac t or , której wartość początkowa wynosi LO, do ustawienia miejsca po przecinku dla bieżącej cyfry, a następnie zawartość tej zmiennej
Rozdzial6. •
ostrukturze programu -
ciąg
dalszy
337
mnożona jest przez O, l za każdym razem, gdy dodawana jest cyfra . Zatem pierwsza cyfra po przecinku mnożona jest przez O, l, druga przez 0,0 I, trzecia przez 0,00 l itd. Proces ten przed stawiony został na rysunku 6.7.
Cyfry części liczby
czę ści
Cyfry
całkowitej
ułamkowej
~
~
5
3
6
liczby
O
S
Kody ASCIIw zapisie dziesiętnym Przed przecinkiem dziesiętnym value 513.0 factor = 1.0
=
=
Pierwsza cyfra
factor O.l*factor value = value + factor*(54 4S) = 513.0 + 0.1*6 = 513.6
Druga cyfra
factor = O.l*factor value = value + factor*(49 - 4S) = 513.6 + 0.01*0 = 513.60
Trzecia cyfra
factor = O.l*factor
value = valu e + factor*(56 - 4S)
= 513.60 + O.OOl*S = 513.60S
Rysunek &.7 W momencie znalezienia znaku niebędącego cyfrą kończymy wykonywanie tej pętli , a więc po drugiej pętli zwracamy wartość zmiennej val ue. Mamy już prawie wszystko . Potrzebujemy jeszcze tylko funkcji mai n( ) do pobrania łańcucha wejściowego i uruchomienia całego procesu.
Składanie całego programu Instrukcje dyrektyw #i ncl ude możemy umieścić razem oraz programu wszystkie prototypy wykorzystywanych funkcji :
wypisać
II Cw6_09.cpp
II Program implem entujqcy kalkulat or.
#include #include #include usi ng st d: .ci n:
usi ng st d: :cout :
using st d: :endl ;
II Dla strumienia wejścia-wyjś cia .
II Dlafunkcji exiu) .
II DlaJunkcji isdigiti).
void eat spaces(cha r* st r) :
II Funkcja usuwajqca spacje.
na samym
początku
338
Visual C++ 2005. Od podstaw double expr (char* st r ) : doub le t erm (char* str. i nt &i ndex) : do uble number( char* st r . int& t ndex ) :
II Funkcja ob liczająca warto.~ć wyrażenia. II Funkcja ana liz ująca skladnik. II Funkcj a rozpoznająca liczby .
const i nt MAX= 80 :
II Maksymalna długos ć wyrażenia. II włą czni e ze zna kiem '\O'.
Zdefiniowaliśmy również zmi enną MAX,
przetwarzanym przez program
która
określa maksymalną liczbę
(włącznie z końcowym
znaków w wyrażeniu
znakiem \ 0).
funkcji main O i program jest skończony. Funkcja main() i, jeżeli jest on pusty, kończyć program. W przeciwnym przypadku nastąpi wywołanie funkcji expr( l obliczającej wartość wprowadzonego wyrażenia i wyśw ie tlającej wynik. Proces ten powinien powtarzać się w nieskończoność . Nie brzmi to zbyt groź nie, więc spróbujemy.
Następnie możemy dodać definicję
powinna
pobierać łańcuch
i nt mai n(l {
char buffe r[MAX] cout
« « « « «
~
{O} :
II Obsza r
wejś ciowy
dla
wyraż enia
endl "Wi t amy w na szym przyj aznymka lkul at orze. end l "Wpr o wad ź wyrazenie l ub pusty wi ersz. aby endl :
do obli czenia.
z a k o ń c zy ć. "
t or r : . ) {
ci n.get l ine(buff er, s tzeof bu rrer ) : eat spaces( buff er):
II Wczytaj lańcuch wejściowy, II Us uń z niego spa cje.
if ( !buff er[O] l ret urn O;
II Pusty wiersz p owoduje zamknięcie kalkulatora.
cout
II
« «
"\t = " « expr( bu ff er ) endl « end l :
Wyś wietl wartość wyrażenia .
Jak działa lafUllkcia W funkcji mai nO tworzymy tablicę typu char o nazwie buffer przyjmującą łańcuchy o dłu gości do 80 znaków (włącznie ze znakiem końcowym). Wyrażenie wczytywane jest wewnątrz n ieskończonej pętli for za pomocą funkcji wejściow ej getLi ne( l. Po wczytaniu łańcucha usuwamy z niego spa cje, wywołując funkcję eatsp aces( l. pozostałe czynności przeprowadzane przez funkcję mai n (l wykonywane są w pętl i. sprawdzenie, czy wprowadzony łańcuch nie jest pusty, czyli zawiera tylko znak \0, W przypadku pustego wiersza działanie programu zostaje zakończone oraz wyświetlona zostaje wartość łańcucha zwróconego przez funkcję expr( l.
Wszystkie
Następuje
Po wprowadzeniu wszystkich funkcji rezultat poniższego:
działania
programu powini en
być
podobny do
Rozdzial6.• ostrukturze programu -
ciąg
dalszJ
339
2 * 35 = 70 2/3 + 3/4 + 4/ 5 + 5/ 6 + 6/7 =
3.90714
+ 2.5' + 2.5*2.5 + 2.5*2.5*2 .5 = 25 .375 Możemy podać dowolną liczbę wyrażeń
snąć
Enter, aby
za kończy ć
do obliczenia, a kiedy nam
się
znudzi, wystarczy wci-
program.
Rozszerzanie programu Mając już działający
Czy nie byłoby
kalkulator,
miło, gdybyśmy
możemy zacząć myśleć
mogli
używać
o rozszerzaniu jego funkcjonalności. nawiasów? Nie jest to chyba aż takie trudne?
Pomyślmy,
w
jaki związek występuje pomiędzy tym, co może znajdowa ć się w nawiasach a analizą wyrażenia, którą przeprowadziliśmy do tej pory. Spójrzmy na przywyrażenie, które chc iel ibyś my przetworzyć :
w nawiasach w naszym oryginalnym twierdzeniu stan ow i ą część skład nika. To prawda, i nieważne , jakiego rodzaju obliczenia są wykonywane. Gdybyśmy mogli wstawić z powrotem do oryginalnego łańcucha wartość wyrażeń w nawiasach , to otrzymalibyśmy coś , z czym można sobie poradz ić . Wskazuje to najeden ze sposobów poradzenia sobie z nawiasami . Wyrażenie w nawiasach moglibyśmy traktować jak każd ą inną liczbę i zmodyfikować funkcję nurnber() , aby odnajdywała wartość tego , co s i ę pomiędzy nimi znajduje. Wydaje się to dobrym pomysłem , ale odnajdywanie wartości tego, co jest w nawiasach, wymaga trochę zastanowienia. Wyrażenie umieszczone w nawiasach jest doskonałym przykła dem pełnego wyrażenia. Mamy już funkcj ę , która oblicza wartość wyrażeń - expr ( ). Jeżeli uda nam się zmu si ć funkcję nurnber ( ) do zwrócenia nam zawartości nawiasów oraz wyłuska nia jej z łańcucha, to tak otrzymany podłańcuch moglibyśmy przesła ć do funkcji expr( ), dzięki czemu rekurencja znacznie uprościłaby nasz problem. Co więcej , nie musielibyśmy martwi ć się o zagnieżdżone nawiasy. Jako że w nawiasach zawsze znajduje się to, co okre śliliśmy jako wyrażenie, funkcja zajmuje się tym automatycznie. Rekurencja znowu górą. Spróbujmy przepisać II Funkcj a
funkcję
nurnbe r ( ), aby
rozpozn ająca wyrażenia w
rozpoznawała wyrażenia
nawiasa ch lub liczby w
łańc u chach.
doub le number (char* st r . i nt & index) double val ue = 0,0: i f( *(st r + index) ==
II Prz echowuj e wynik. ' (' l
II Po czątek naw iasu .
(
char* psubstr = O: psubstr = ext ract(st r . ++i ndex) : value = expr(psubstr) : delete[]psubst r :
II Wskaźnik dła podla ncucha. II Wyłuskaj łań cu ch w nawiasach. II Oblicz wartos ć podłan cucha. II Wyczyść wolną pam ięć.
w nawiasach.
340
Visual C++ 2005. Od podstaw re t urn valu e :
II Zwróć
wartość podłancucha .
whi l e( i sdi git (*(st r + i ndex) ) ) II Pętla zbierająca val ue = 10*val ue + (*(str + i ndex- - ) - ' O') :
if(*( st r + i ndex) ret urn val ue ;
I ~
'. ')
wiodące
cyf ry.
II Skoro jestesmy tutaj , to znaczy. że n apotkaliśmy znak, .11 który nie jest cyfrą. II Sp rawdzamy, czy j est to przecinek dzi es ięt ny. II i jeśli nie, to zwracamy wartoś ć zmiennej value.
doubl e fact or = 1.0: whi l e (i sdlgi t(* (st r + (++i ndex)) ) ) { fa ct or * ~ 0.1 : val ue = va l ue + (*(st r + i ndex) -
' O' )*f act or:
return val ue :
II Zakoń czen ie pętli i wynik.
II Wsp ółczynn ik miej sc dz ies ię tnych. II Powtarzaj , dopóki s ą cyfry. II Zmniejsz
wsp ółczyn n ik dzies ięciokrot n i e.
II Dodaj miej sce po przecinku .
To j eszcze j ednak nie wszystko , poniew aż nadal potrzebujemy funkcji ext ract ( ), ale o tym za moment.
Jak działa ta funkcja Zadziwiające ,
jak niewielkich zmian mu sieliśmy dokonać , aby dodać obsługę nawiasów. tutaj oszukujemy, bo używamy funkcji ext ractt ), której jeszcze nie stworzyliśmy . Ale mimo to dzi ęki dodaniu tylko j ednej funkcji zyskujemy ob sługę nawiasów zagnieżdżanych do dowolnej głęboko ści . To jest j ak polewa na na szym cie ście i na dodatek wszystko dzięki magicznemu działaniu rekurencji. Oczywiście trochę
Pierwszą czynnośc ią funkcji number ( ) j est tera z poszukanie nawiasu otwi erającego . Jeżel i taki się znajdzie, to wywołuj e ona funkcję ext r act( ) w celu wydobycia z oryginalnego łań cu cha podłańcucha otoczonego nawiasami. Adres tego wydobytego podłańcuch a przechowywany j est we wskaźniku psubst r , który następnie przekazuj emy jako argument do funkcji expr() w celu przetworzenia. Wyn ik przechowywany j est w zmiennej va l ue. Po zwolnien iu pam ięc i przydzielonej w wolnym obszarze w funkcji ext r act() (kiedy już ją zaimplementujemy) otrzymaną wartość zwracamy, jakby była zwykłą liczbą. O czywiści e , jeżel i nie ma żadnego nawiasu ot w ieraj ące go, funkcja number( ) kontynuuje swoje dz iałanie.
Wydobywanie podłańcucha Musimy teraz napisać funkcję ext ract ( ). Nie jest to zadan ie bardzo trudne, ale też i nie banalne. N ajwię c ej komplikacji powoduj e fakt , że każda para nawi asów może zawierać dodatkowe zagn i eżdżone naw iasy. W związku z tym nie możemy ograniczyć się tylko do odnalezienia pierwszego nawiasu otw i eraj ące go . Musim y sp raw dz ić, czy nie ma ich więcej , a w przypadku znalezienia ich wi ększej liczby ignorować odpowiadaj ące im nawiasy zamykające. Możemy tego dokonać , tworząc licznik lewych nawi asów znale zionych przez funkcj ę i zwi ększając go o I za k ażdym razem, gdy znajduj emy otwieraj ący nawias. Jeż el i liczn ik nie ma wartoś c i
RozdZiał 6.•
Ostrukturze programu -
ciąg
dalszy
341
zerowej, to odejmujemy od niego jeden za każdym razem, gdy znajdziemy nawias zamykający. Oczywiście, jeżeli licznik ma wartość O i natrafimy na nawias zamykający, to oznacza to, że doszliśmy do końca podłańcucha . Mechanizm wydobywania podłańcucha otoczonego nawiasami przedstawia rysunek 6.8.
Sygnalizuje początek
'(' count:
Znalezienie znaku')' w momencie, gdy licznik znaków '(' ma wa rtość zero, oznacza koniec wyrażen ia w nawiasach
pod/ańcucha
o kopiowanie
zarmen
~. 2
+
*
3
Pod/ańcuch,
(
S
-
2
)
/
2
*
(
,
,
+
9
)
\0
który z naj d owa ł się w nawiasach
Rysunek 6.8 Jako
że
wydobywany tutaj
podłańcuch
zawiera
podwyrażenia otoczone
nawiasami, funkcja
extract ( ) wywołana zostaje jeszcze raz w celu przetworzenia także tych
łańcuchów.
Zadaniem funkcji ext ract() jest również przydzielanie pamięci dla podłańcucha oraz zwracanie do niego wskaźnika . Oczywiście indeks bieżącej lokalizacj i w oryginalnym łańcuchu musi zostać ustawiony na znak z n aj d uj ący się za znalezionym podłańcuchem, w związku z czym jego parametr musi zostać zdefiniowany jako referencja. Biorąc pod uwagę powyższe rozważania, prototyp funkcji extract.O przedstawia się następująco:
cha r* extract (cha r* st r . i nt &i ndex) ; Możemy
teraz
IIFunkcj a
spróbować napisać definicję
II Funkcja wydobywająca podla ńcuch II (wymaga cstring).
wydobywają ca podlańcuchy.
funkcji .
znajdujący s ię pomiędzy
nawiasami
char* ext ract( char* str . i nt & index) {
char buffer [M AX] ; char* pst r = O: int numL = O; int bu fi ndex = index;
II Tymczasowe miejsce dla podlańcucha . II Wskaźnik do nowego podtancucha, II który ma zos tać zwrócony. II Licznik znalezionych lewych nawia sów. II Zapisanie początkowej war/ości index.
do {
buffe r[ i ndex - bufindex] - *(str swit ch(buffer[in dex - bu findex])
+
i ndex) ;
{
case ' )' : if(numL == O) {
siz e t size = i ndex - bufi ndex;
342
Visual C++ 2005. Od podstaw buffer [ index - bu findex] = ' \ 0' : l/'} ' zam ień na '10'. ++index: pstr = new char[i ndex - buf index] : if( 'pst r) {
cout
"Operacja przydzielania pa m i ęc l nie programzost a ł z ak o ń c z ony . ";
« «
p ow io d ła s i ę . "
exit(l ) ;
l
st rcpys tpst r , index-bufi ndex. buffe r ) ; II Skopiuj podłancuch do now ej pamięci.
ret urn pstr :
II Zwró ć podla ńcu ch w nowej pamię ci.
}
else num L-- ; break:
II Zmniej sz
case ' (': numL ++ ; break:
II Zwiększ
liczbę
liczbę
zn aków
't . które trzeba dop asowa ć .
znaków '(', któr e trzeba
dopasować.
)
} whi le( *(st r cout
«
+
i ndex-« ) ! = ' \ 0' ) :
" Wy jśc ie
być n i epr aw ldł owe
«
poza gran ice "
II Pętla zapobiegają ca przekroczeniu k ońca łań c/l cha.
wy r aże n ia
- wprowadzone dane
m u s l a ły
endl :
exi t Cl ) ;
__ l
ret urn pstr :
----'
Jak działa la funkcja Na początku deklarujemy nową tablicę, w której tymczasowo będziemy przechowywać łań cuch. Nie wiemy, jakiej będzie on długości , ale nie może być dłużs zy niż MAXznaków. Nie możemy zwrócić adresu tablicy buffer do funkcji wywołującej, gdyż jest ona lokalna i zostanie usunięta po zakończeniu działania funkcji . W związku z tym w obszarze wolnej pamięci musimy jej przydzielić odpowiednią ilość pamięci , kiedy będziemy wiedzieli , jakiego rozmiaru jest łańcuch. Robimy to, deklarując zmienną pstr typu wskaźnik do char , którą zwracamy przez wartość , gdy łańcuch znajduje się już w obszarze wolnej pamięci . Zmienna numL służy jako licznik lewych nawiasów w podłańcuchu (zgodnie z zasadami opisywanymi wcześniej). Wartość początkowa zmiennej l ndex (kiedy funkcja rozpoczyna działanie) przechowywana jest w zmiennej buf; ndex, której używamy w połączeniu ze zwi ęk szanymi wartościami zmi ennej; ndex do indeksowania tablicy buffer. Całą wykonywalną część
naszej funkcji stanowi w zasadzie jedna duża pętla d o-włu l e. Podkopiowany jest ze zmiennej st r do tablicy buffer po jednym znaku za każdym powtórzeniem . W każdym cyklu nast ępuje też sprawdzanie, czy kopiowany znak nie jest lewym nawiasem. Jeżeli tak, to zmienna numL zostaje zwiększona o jeden, a jeżeli znaleziony został prawy nawias i zmienna numL ma wartość różną od zero, to zostaje ona zmniejszona o jeden. W przy. padku znalezienia prawego nawiasu, gdy zmienna numL ma wartość Owiemy, że doszliśmy do końca podłańcucha. W zw iązku z tym w tablicy buffer znak ) zostaje zamieniony na \0, zostaje też przydzielona ilość pamięci wystarczająca do przechowania tego podłańcucha . łańcuch
Rozdział 6.•
Następnie
Ostrukturze programu -
ciąg dalszy
343
łańcuch znajdujący się w tablicy buffer do obszaru wolnej pamięci za new przy użyciu funkcji strcpy_s() zadeklarowanej w pliku nagłówkowym . Jest ona bezpieczną wersją starej funkcji strcpy() zadeklarowanej w tym samym nagłówku. Funkcja ta kopiuje łańcuch określony przez trzeci argument (buffer) do adresu
kopiujemy
pomocą operatora
określonego
lowego -
przez pierwszy argument (pst r) . Drugi argument
określa długość łańcucha
doce-
pstro
Je żeli zostaną wykonane
instrukcje z samego dołu pętli , to oznacza to, że doszliśmy do znaku \ 0 na końcu wyrażenia w str , nie znajdując prawego nawiasu odpowiadającego nawiasowi lewemu, a więc wyświetlamy komunikat o błędzie i zamykamy program.
Uruchamianie zmodyfikowanego programu Po zamienieniu funkcji number() w starej wersji programu, dołączeniu pliku nagłówkowego pomocą dyrektywy #incl ude oraz włączeniu prototypu i definicji nowej, przed chwilą napisanej funkcji extract O jesteśmy gotowi do zabawy z naszym niezwykle wszechstronnym kalkulatorem. Jeśli wszystko poskładaliśmy bez żadnych błędów , to możemy otrzymać następujący wynik :
za
Wit aj w naszym przyj aznym kalkulat orze. l ub pusty wiersz . aby
Przyjazny i pomocny komunikat w ostatnim wierszu danych na wyjściu został spowodowany wpisaniem przecinka zamiast kropki w powyższym wyrażeniu - powinno być 2.4. Jak widać, zyskaliśmy obsługę zagnieżdżonych do dowolnej głębokości nawiasów przy względnie niedużym stopniu komplikacji kodu. Wszystko to dzięki zdumiewającym możliwościom rekurencji.
Programowanie wC++/CLI Wszystko, co było powiedziane do tej pory na temat funkcji w natywnym C++, ma również zastosowanie do języka C++/CLI, pod warunkiem że typy parametrów oraz typy zwracane są typami fundamentalnymi, które - jak wiemy - są odpowiednikami typów klas wartości w programach CLR, uchwytów śledzących oraz referencji śledzących. Jak wiemy, na adresach przechowywanych w uchwytach śledzących nie można wykonywać działań arytmetycznych. W związku z tym techniki zastosowane do parametrów będących tablicami w natywnym C++, traktujące je jako wskaźniki, na których można wykonywać działania arytmetyczne,
344
Visual C++ 2005. Od podstaw nie mają zastosowania do C+ +/C LI. Znika wiele komplikacji , które mogą powstać wraz z przekazywani em argument ów do funkcji w natywnym C++, ale na nieuważnych wci ąż czeka je szcze jedna pułapka w C++/CLI. Wersja kalkulatora dla CLR pomoże nam zrozumieć sposób pisania funkcji w CH /CLI. Mechanizm wywoływania (throw) i przechwytywania (cat ch) wyjątków w programach CLR działa tak samo jak w programach w natywn ym C++, choć istnieją pewne różnice . Wyjątk i powodowane w programie w C++/CLI zawsz e muszą by ć zgłaszane za pomo c ą uchwytów śledzących . W konsekwencji, zgłaszane wyj ątki zawsze powinny być obiektami i w miarę mo ż liwości powinno się unikać zgłaszania literałów , a w szczególności literałów łańcuchowych . Rozważmy poniższy przykład kodu , w którym zgła sza ny i przechwytywan y jest wyjątek : t ry {
t hrow L" Z ł a p mn Ie .
je ~ l i
pot raf i sz . " '
}
catc hCSt r ing ex) { Console : :WriteLi neCL"St ri ng A
II
A :
Wyjąlek
nie zosianie lulaj prze chwycony.
(O}" .ex) .
}
Klauzula catch w powyższym kodzie nie mo że przechwycić zgłoszonego obiektu , ponieważ instrukcja t hrow zgłosiła wyjątek typu const wchar_t*, a nie typu St ri ng"". Aby móc przej ąć tak zgło s zo n y wyjątek , klauzula catc h pow inna wygląda ć następująco: t ry { th row L "Zła p mn ie.
jeś
l i potra f i sz . " ;
}
catc hCconst wc har_t* ex)
II W p o rządku -
wyją tek jest
odpo wiedniego typu.
{
Str i ng exc = gcnew Stri ngCex) ; Conso l e : :WriteLi ne tL"wchar_t ; {O }" . exc) : A
Tym razem klauzula catc h przechwyciła Aby
wyjątek, gd yż
jest on odpowiedniego typu.
do przechwycenia dla pierwszej wersji klauzuli catc h, musielikod bloku t ry:
zgłosi ć wyj ątek możliwy
b y śmy z m i en ić na stępująco
t ry { t hrow gcnew Stri ng ( L" Zła p mnie. je ś li potraf i sz . ") : } catch CString ex) II W porzqdku - wyją tek j est odpo wiedniego typu. { Consol e :WriteLi ner L"Stri ng {O j" .ex) ; } A
A
:
Teraz
wyjątek
odnoszący się
jest obiektem klasy St ri ng i został do łańcucha.
zgłoszony
jako typu St r inq", czyli uchwyt
Rozdział 6.•
Ostrukturze programu -
ciąg dalszy
345
W programach w C++/CLI możemy również korzystać z szablonów funkcji , ale zamiast nich tak zwanych funkcji generycznych (ang. generic functions) , które są bardzo podobne do szablonów, choć istnieją między nimi także pewne istotne różnice. możemy także używać
Funkcje generyczne że
funkcje generyczne na pierwszy rzut oka robią to samo co szablony funkcji i mogą to jednak różnią się one od siebie sposobem działania i różnice te sprawiają, że są one wartościowym dodatkiem w programach CLR. Gdy używamy szablonów, kompilator tworzy z nich kod źródłowy potrzebnych funkcji. Kod ten jest następnie kompilowany razem z resztą kodu programu. W niektórych przypadkach może to oznaczać wygenerowanie dużej ilości kodu i znaczne zwiększenie rozmiaru modułu wykonywalnego. Funkcje generyczne natomiast same są kompilowane i w momencie wywołania funkcji pasującej do specyfikacji funkcji ogólnej rzeczywiste typy zamieniane są na typy parametrów w czasie wykonywania programu. Podczas kompilacji nie jest generowany żaden dodatkowy kod i nie ma ryzyka zwiększenia objętości programu , tak jak w przypadku używania szablonów funkcji. Mimo
wydawać się zbędne,
Niektóre aspekty definiowania funkcji ogólnych uzależnione są od wiedzy na tematy, o których mowa dopiero w dalszych rozdziałach, ale wszystko w końcu stanie się jasne . Tutaj dostarczę tylko niezbędne wyjaśnienia nowych właściwości, a szczegółów dowiesz się póź niej w trakcie czytania książki. będzie
Deliniowanie funkcji generycznych Funkcję generyczną
definiujemy przy użyciu parametrów typu, które zostają za stąpione kiedy funkcja zostaje wywołana . Poniżej znajduje się przykładowa definicja funkcji generycznej : właściwymi typami,
generi c where T:IComparable T Ma xE lement (arrayA x) {
T ma x = x[OJ: fort int i = 1; i < x->Lengt h: 1 ++) if (max->Compa reTo(x[iJ ) < O) max = xCi J: return max : Powyższa funkcja generyczna wykonuje to samo zadanie co szablon funkcji w natywnym C++, który widzieliśmy wcześniej. Słowo kluczowe generic w pierwszym wierszu informuje, że dalej znajduje się definicja funkcji generycznej . W pierwszym wierszu znajduje się parametr typu określony symbolem T. Słowo kluczowe t ypename otoczone nawiasami trójkątnymi informuje, że znajdujący się po nim symbol T stanowi nazwę parametru typu w tej funkcji generycznej oraz że parametr ten zostanie zamieniony na rzeczywisty typ w momencie wywołania funkcji. W definicjach funkcji generycznych z wieloma parametrami typów nazwy tych parametrów umieszcza się w nawiasach trójkątnych i przed każdym z nich musi znaleźć się słowo kluczowe t ypena me oraz są one oddzielone od siebie przecinkami.
346
VisIlai C++ 2005. Od podstaw Słowo kluczowe where znaj duj ące si ę po nawiasie trójkątnym wp rowadza warunek dotyczący rzeczywistego typu, który może zostać wstawiony w miejsce symbolu T, kiedy używan a jest funkcja generyczna. Warunek w naszym przykładzie mówi, że typ, który ma zostać wstawiony w miejsce symbolu T, w funkcji ogólnej musi implementować interfejs IComparable. Interfejsam i zajmiemy się trochę p ó źn iej , a na razie wyjaśnię tylko , że oznacza to, iż typ ten musi definiować funkcję CompareTo( l, która pozwala na porównywanie dwóch obiektów tego typu. Bez tego warunku kompilator nie wi edziałby, jakiego rodzaju operacje mogą być wykonywane przy użyciu typu, który m oże zastąpić symbol T, poni eważ aż do momentu u życia funkcji ogólnej nie ma na jego temat ża dnych informacj i. Przy zastosowaniu tego warunku możemy za pomocą funkcji CompareTo () porównać wartość zmiennej max z dowolnym elementem tablicy. Funkcja CompareTo( )zwraca liczbę całkowitą mniejszą od zera, jeżeli obiekt, dla którego została wywołana (w tym przypadku ma x), jest mniejszy ni ż podany argument, zero, jeśl i jest równy argumentowi, lub liczbę większą od zera, jeżeli jest on większy od argumentu.
W drugim wierszu znajduje się nazwa funkcji generycznej Ma xEl ement , jej typ zwracany T oraz lista parametrów. Wygląd a to j ak zwykły nagłówek funkcji, z tą różnicą, że zawiera ogólny parametr typu T. Typ zwracany funkcji generycznej oraz typ elementu tablicy , który jest czę ścią specyfikacji typu parametru, są typu T, a więc oba te typy określone zostają w momencie użycia funkcji generycznej. Używanie hmkcii generycznych
Najprostszym sposobem wywołania funkcji generycznej jest użycie jej jak zwykłej funkcji. Na przykład funkcji generycznej MaxE l ement( ) z poprzedniego podrozdziału moglibyśmy użyć w następujący sposób :
array<double>A dat a = {1.5 . 3.5. 6. 7. 4.2 . 2.1} ; double ma xData = MaxE lement( dat a) : Kompilator w tym przypadku może wydedukować, że argumentem typu funkcji ogólnej jest doubl e, i wygenerować odpowiedni kod do wywołania tej funkcji . W czasie wykonywania tej funkcji, wszystkie miejsca wystąpienia symbolu T zastąpione zostają określeniem typu double. Jak już powiedziałem wcześniej, to nie jest to samo co funkcja szablonowa - podczas kompilacji nie są tworzone żadne egzemplarze funkcji. Kiedy zostaje wywołana, skompilowana funkcja generyczna potrafi przyjąć zamienniki argumentów typu . Warto zauważyć, że jeżeli do funkcji generycznej przekażemy jako argument literał łań cu chowy, kompilator dojdzie do wniosku, że typ argumentu to St ri ng bez względu na fakt, C"Ej jest to stała łańcuchowa typu "Witam! ", czy też typu L"Wi t am ! ''. A
,
Istnieje możliwość, że kompilator nie będzie mógł odgadnąć typu argumentu z wywołania funkcji generycznej. W takich przypadkach najlepiej jest podać argumenty typu jawnie w trójkątnych nawiasach po nazwie funkcji w wywołaniu. Na przykład wywołanie w poprzednim fragmencie kodu moglibyśmy zapisać następująco:
double ma xData = MaxEl ement<double>(dat a) : Przy jawnie
określonych argumentach
typu nie ma mowy o dwuznaczności .
Rozdzial6. - ostrukturze programu -
ciąg
dalszy
347
Istni eją pewne ograniczenia d o tyczące tego, co mo żna przek azywać jako argument typu do funkcji ogólnej . Argument ten nie może by ć typem klasowym natywne go C++ ani natywnym wskaźnikiem lub referencją, a także uchwytem do typu klasy wartości, takim j ak mt". Z tego wynika, że dozwolone są tylko typy klas wartoś ci, takie jak .j nt czy doub 1e, oraz uchwyty ś le dzące , takie jak Stri nq" (ale nie uchwyty do typów klas wartości).
Wypróbujmy to na przykładzie.
mmtmI Używanie funkcji generycznych Poniżs zy
kod definiuje trzy funkcje generyczne i z nich korzysta:
#include "st dafx.h" using namespace Syst em: II Funkcj a ge neryczna znajdująca
największy
element tablicy.
generic where T: IComparable T Ma xE lement( arrayAx) (
T max ~ x[O ] : for( i nt i ~ 1: i < x-c-Lenqt h: i ++ ) if( max·>CompareTo(x[i ] ) < O) max ~ x[i]: return ma x: II Funkcja generyczna
usu wająca
element z tablicy .
generic where T: IComparable arrayA RemoveElement( T element . arrayA dat a) {
arrayAnewData = gcnew array(dat a· >Lengt h . l ): i nt index ~ O: II Indeks do elementów tablicy newData. boal found = fal se: II Wskazuje, że zos tał znaleziony element do usuni ę cia.
for each(T item in dat a) ( II Sprawdzanie nieprawidlowego indeksu lub znalezionego elementu.
Console : .Writ eLi ne(L"Nie znaleziono element u do ret urn data : }
newDat a[index++]
=
it em :
us u ntęc ta " ) :
348
Visual C++ 2005. Od podstaw ret urn newDa ta : II Funkcja generyczna
wyświetlająca
elementy tablicy.
generic where T:IComparable void Lis tElements (arrayA data ) (
for each(T item in da ta) Consol e : Wrlte( L"{ O. l O)" . item); Conso l e: :Wr lteL iner ): int main(a rray<System : :Str ing A> Aargs ) {
array<double>Adat a = {1.5. 3.5. 6.7. 4.2. 2. l }: Console : :Writ eLi ne(L"Tabl ica zawiera:" ): Li st Element s(dat a); Console: :W r it e L i n e ( L" \ n N aj w i ęk s z y element = {O} \ n". MaxE l ement (dat a)) : array<double>A result = RemoveElement (MaxElement(data J. data) : Console : :Wri t eLine(L" Po us un ięc i u na jw ię ks zeg o element u t abl ica zawiera. ") , Li st Element s(result ); ar rayct nt >" numbers = {3, 12. 7. O. 10. U} :
Conso le : :WrlteLi ne(L"\nTabl ica zawiera:" ): List Elements(n umbe rs): Conso le : : W r it e L i n e ( L " \ n N a j w i ę k s zy el ement = (O}\ n" . MaxE lement (numbers )) : Console : :Wr1t eLine(L"\nPo u s u n i ęc i u naj w ię ks zego elementu t abl ica zawier a:"); Li st Elements( RemoveElement (MaxElement( numbers) . numbers )) : arrey-St r tnq" >" st rings = {L"Dwie ". L " g ł owy ". L"to ". t' nt e" . L"jedna"} : Console: :Wri t eL ine(L"\ nTablica zawiera ''' ); Li st Element s Cst ri ngs): Console : :WrlteLi n e C L " \ n N a j wi ę k szy element = {O }\n". MaxElement (st rings)): Conso le : :WriteLine(L"\ nPo us u n ięc i u na jw i ę ks zeg o elementu t abli ca zawie ra:"): ListElements (RemoveE lement( MaxElement (st ri ngs) . st ri ng s)) ; return O:
Wynik d ziałania tego programu jest następujący: Tabl ica zawier a: 1. 5 3.5 Najw iększy
Po
element
=
6.7
3.5
Tabl ica zaw1e ra: 3 12 Najwię k sz y
Po
element
=
7
elementu t abli ca zawiera: 4.2 2. 1 7
O
10
12
u s u n i ęc i u najwięk sze go
3
2.1
6.7
us u n ięc i u n a j w i ę k s z eg o
1. 5
4.2
elementu t abl ica zawie ra: O
10
11
11
Rozllzia.6.• Osll'uklurze programu Ta blica zawiera: Owie g łowy Naj w i ęk s z y
Po
to
nie
ciąg
dalszy
349
j edna
element = t o
usu nię c i u najw ięk s z eg o
Dwie
głowy
element u t abl ica zawiera: nie jedna
Jak lo działa Pierwszą funkcją generyczną zdefiniowaną
z
w tym kodzie jest funkcja MaxEl ement ( ), identyczna element tablicy, a więc nie
funkcją, którą widzieliśmy wcześniej, znajdującą największy
będę jej już
tutaj
opisywał .
Następna
funkcja ogólna RemoveElement s( ) usuwa element przekazany jako pierwszy argument z tablicy określonej przez drugi argument. Funkcja zwraca uchwyt do nowej tablicy powstałej w wyniku tej operacji. W pierwszych dwóch wierszach definicji funkcji widać, że zarówno typy parametrów, jak i typ zwracan y zostały okre ślone za pomocą symbolu T.
generic where T: IComparable arrayA RemoveEl ement(T element. arrayAdat a) Warunek przy symbolu Tjest taki sam jak w pierwszej funkcji generycznej, czyli narzuca, aby typ użyty jako argument typu implementował funkcję CompareTo( ), która pozwala na porównywanie obiektów tego typu. Drugi parametr i typ zwracany są uchwytami do tablicy elementów typu T. Pierwszy parametr jest po prostu typem T. Najpierw funkcja tworzy
tablicę
do przechowywania wyników:
arrayAnewOata = gcnew array(data->Length - l ) : Tablica newDa t a jest tego samego typu co drugi argument (tablica elementów typu T), ale ma o jeden element mniej, ponieważ jeden ma zostać usunięty z oryginalnej tablicy . Elementy z tablicy data do tablicy newDa t a kopiowane
int i ndex = O: boal found = false; for each(T lt em in data)
są w pętli
f or each:
II Indeks do elementów tablicy newData. II Wskazuje . że został znaleziony element do
( II Sprawdza nie nieprawidłowego indeks u lub znalezionego elementu,
Console : :Writ el1ne(L"Nie znaleziono element u do ret urn dat a:
u s un i ę c i a" ) :
usuni ę cia ,
350
Visual C++ 2005. Od podstaw newData[ index++]
~
i t em :
Skopiowane zostają wszystkie elementy z wyjątkiem jednego zidentyfikowanego przez pierwszy argument funkcji. Zmiennej i ndex używamy do zaznaczenia kolejnego elementu tablicy newData, który ma otrzymać następny element z tablicy data . Każdy element tablicy data zostaje skopiowany, chyba że j est on równy zmiennej el ement . W takim przypadku zm ienna f ound zostaje ustawiona na true i instrukcj a cont i nue przeskakuje do następnego powt órzenia. Istnieje możliwość , że w tablicy znajdzie się więcej elementów takich samych jak pierwszy argument. Zmienna found zapobiega pomijaniu w p ętli kolejnych elementów, które są takie same j ak el ement . Mamy także sprawdzanie zmiennej i ndex przekraczającej dozwolony limit indek sowanych elementów w tablicy newDa ta . Mogłoby się to zdarzy ć w przypadku, gdy w tablicy data nie ma ani jednego elementu równego pierwszemu argumentowi funkcji. W takiej sytuacj i zwracany jest tylko uchwyt do oryginalnej tablicy. Trzecia funkcj a generyczna tylko
wyświetla
elementy tablicy ar rey-T>:
gener ic voi d Lis tElements(ar rayAdata) (
for each(T it em i n data) Console: :Wrlte(L "{O,lO)" , item) ; Conso le: ;Wrl t eLt net i: Jest to j edna z nielicznych sytuacji , w których nic j est potrzebny żaden warunek dla parametru typu . Z obiektami niewiadomego typu można zrobić bardzo niewiele i dlatego też parametry typów funkcji generycznych zazwyczaj m aj ą warunki . Działanie funkcji jest bardzo prosteza pomocą pętli f or eac h każdy element tablicy wysyłany jest do wiersza pol eceń w polu o szerokości 10. Moglibyśmy zrobić to jeszcze lepiej, dodając parametr dla szerokości pola i tworząc łańcuch formatujący , którego moglibyśmy użyć jako pierwszego argumentu funkcji Write O w klasie Conso le. Moglibyśmy również dodać do pętli mechanizm wysyłający określoną liczbę elementów do wiersza na podstawie s ze roko ś c i pola. Funkcja ma i n( ) wykonuje te wszystkie funkcj e generyczne przy użyciu param etrów typów doubl e, int oraz St r tnq". A zatem widzimy , że wszystkie trzy funkcje generyczne mogądzia łać z typami wartośc i i uchwytami. W drugim i trzecim przykładzie funkcje generyczne zostały użyte łącznie w jednej instrukcji. Spójrzmy na poni ższą przykładową in strukcję użycia funkcji:
ListElement s(RemoveE lement( MaxElement (st rl ng s) . str i ngs) ) ; Pierwszy argument funkcji generycznej RemoveElement O jest wygenerowany przez funkcję generyczną M axE l ement ( ), a więc funkcje te mogą być używane w taki sam sposób jak zwyczajne funkcje . We wszystki ch przypadkach użycia funkcji generycznych kompilator może samodzielnie odgadnąć argumenty typu, ale je śli chcemy, możemy je określić jawnie. Na przykład poprzednią instrukcję moglibyśmy zapisać następująco :
List El ement s(RemoveElement <Stri ngA>(MaxEl ement <St rl ngA>(st rlngs ) . str ings) ) ;
Rozdział 6.•
Ostrukturze programu -
ciąg
dalszy
351
Kalkulator ClB Spróbujmy teraz przepisać nasz kalkulator w języku C++ /CLI. Przyjmiemy tę s amą strukturę programu oraz hierarch ię funkcji co w programie w natywnym C++, ale defini cje i deklaracje funkcji będąjuż napisane w CH /CLI. N a dobry początek zaczniemy od prototypów funkcji na samym początku pliku źródłowego (projekt ten ma nazwę Cw6.11): II Cw6_ 11.cpp: main project fi le. II Kalkul ator CLR obs ługujący nawiasy .
#include "stdafx.h" #i nclude
II D/a funkcji exiu) .
using namespace System ; Str i ngA eatspacesCStr i ngA st r ) ; doubl e expr(Str ingA str ); do uble t erm(Stri ngA st r . i nt A i ndex) ; double numberCStr i ngA str. i nt A i ndex) ; Stri ngA extract (St ringA str . int A index):
II Funkcj a u s uwając a spacje. II Funkcj a ob liczająca war/ oś ć wyrażen ia. II Funkcja a na liz ująca s k ładn i k. II Funkcja rozp o zn ają ca liczby. II Funkcja wydo bywają ca podtancuchy .
teraz uchwytami . Parametry łańcuchowe s ą typu St ri ng a parametr i ndex zap i suj ący bi eżące położenie w łańcuchu jest typu i nt ' . Oczywiście łańcuch zwracany jest jako uchwyt typu St r i ng Wszy stkie parametry
są
A
,
A
•
Implementacj a funkcji ma i n()
wygląda następująco :
i nt mai n(array<System : :Stri ng A> Aargs) {
St ri ng A buff er :
II Obszar
wejścio wy
do oblicza nia
war/ości wyrażeń .
Console: :Wri tel i net l "Wi t aj w naszym przyjaznym kal kulatorze. ") ; Console: :Writel i ne(l " Wp r owa d ź jaki e ś wyrazenie l ub pust y ~J i ersz . aby
z a k oń c zy ć . " ) ;
fort : :) {
buffer = eatspaces(Console: :Readl i ne() );
II Wczytaj wiersz dany ch
if (Stri ng: : IsNullOrEmptyCbuff er )) ret urn O:
II Pusty wiersz
Console : :Writ el i net t."
=
{O}\ n\n" .expr(buffer )): II
wejściowych .
koń czy pracę
kalkulatora.
Wyś lij na wyjście wartość wyrażenia .
}
ret urn O: łatwiejsz a do odczytania. Wewnątrz nieskończonej pętli for do klasy Stri ng IsNull Or Empty() . Funkcja ta zwraca t rue, jeżeli łańcuch przekazany jako argument jest nu11 lub zerowej długo ści, czyli robi dokładnie to, co jest nam potrzebne.
Funkcja ta jest o wiele krótsza i wywołuj emy funkcję należ ącą
352
Visual C++ 2005. Od podstaw
Usuwanie spacji złańcucha wejściowego u suwaj ąca
Funkcja
II Funkcja
równ ież jest
spacje
usuwająca
krótsza i pros tsza:
łań cucha .
spacje z
StringA eat spacesCStri ng A st r) { II Tablica przech o wująca
łań cllchy
bez spa cji.
array<wcha r_t >Achars = gcnew array<wchar_t>Cst r ->Lengt h); 1 nt lengt h = O; II Liczba znaków w tablicy. nieb ędąc e
II Skopiuj znaki
spa cjami do tablicy cha rs.
for eachCwc har_t ch in str ) if Cch 1= ' ' ) chars[l engt h++] = ch; II Zwróć
tab licę
chars jako
ła ńc uch.
ret urn gcnew St ringCc hars , O, lengt h); Naj pierw tworzy my tabl ic ę , w której zostanie umieszczony łań cuch poddany procesowi usuwania spacji , Jest to tablica eleme ntów typu wcha r _t , pon ieważ łańcuc hy w C++/CLI s kła dają się ze znaków Unieode. Proces usuwania spacj i j est bardzo pros ty - wszystkie znaki, które nie są spacjami, kopiujemy z łańcuch a st r do tablicy chars, li c z bę skopiowanych znaków przechowując w zmiennej l ength. Na koniec tworzy my nowy obiekt klasy St ri ng za p omocą konstruktora tej klasy, który two rzy obiek t z eleme ntów tablicy. Pierwszym argumentem przekazanym do konstruktora jes t tablica, która stanowi źródło znaków dla łańcuc ha, drugi argument to indeks pierwszego znaku z tablicy zawierającej łań cuch , a trzeci argume nt to c ałkowita liczba znaków w tablicy, które maj ą zostać u żyt e . W klasie St ri ng zdefiniowa nych je st więcej konstruktorów s łużących do tworzenia łań cu ch ó w na ró żn e sposo by.
Obliczanie wartości wyrazenia arytmetycznego P oni ż ej
znajduje
II Funkcja
się
imp lementacja funkcji
obliczająca wartość wyrażenia
double exprCSt r lngA str )
ob liczającej wartość wyrażen ia:
arytm etycznego .
{ II Śledzi położenie bieżącego znaku.
int i ndex = O; A
double value
~
term Cst r , index);
II Pobierz pierwszy element.
whileC*i ndex < str ->Leng t h) {
switchCst r[ *index ])
II Wybierz
działanie
zgodne z
b i eżą cym
znaki em.
{
case '+' ; ++ C*index) ; val ue += t ermCstr , index) ; break;
II Znaleziono znak +, II a wi ę c zwiększ ws kaźn ik index o jeden i dodaj II następny składn ik.
case ' - ' ; ++ C*i ndex);
II Znaleziono znak -, a więc II zmni ej sz wskaźnik index o jeden i dodaj
Rozdzial6.• ostrukturze programu value -= t erm(st r . l ndex) : break: defaul t:
ciąg
dalszy
353
II następny składnik.
II Wykonanie lego kodu oznacza, II wyrażenie jest nieprawid/owe.
Console: :Wr lteLl ne( L"Ar rrgh!*#! I Tu Jest extt t l i:
że
wprowadzone
b ł ą d .v n") :
}
ret urn va lue : Zmienna i ndex została zadeklarowana jako uchwyt, ponieważ chcemy ją przekazać do funkcji t erm ( ), która z kolei ma zmodyfikować oryginalną zmienną. Gdybyśmy zmienną i ndex zadeklarowali po prostu jako typ i nt, to funkcja te rm() otrzymałaby tylko kopię jej wartości i nie mogłaby zmienić oryginalnej zmiennej . Deklaracja zmiennej i ndex spowodowała zgłoszenie przez kompilator komunikatu ostrzegawczego, ponieważ instrukcja ta polega na autopakowaniu (ang. autoboxing) wartości O w celu utworzenia obiektu klasy wartości I nt32, do którego odnosi się uchwyt. Ostrzeżenie jest zgłaszane, ponieważ często można się natknąć na instrukcje, w których uchwyt jest inicjalizowany wartością zerową. Oczywiście, aby to zrobić, należy zamiast Ojako wartości począt kowej użyć null ptr. Jeżeli chcemy, aby ostrzeżenie się nie pojawiało, możemy tę instrukcję przepisać następująco: i nt
Ą
i ndex
Powyższa
go
~
gcnew i nt( O) ;
instrukcja jawnie używa konstruktora do utworzenia obiektu i zainicjalizowania O, dzięki czemu kompilator nie zgłasza żadnego ostrzeżenia .
wartością
Po przetworzeniu pierwszego składnika za pomocą funkcji term() pętla whi l e przeszukuje w celu znalezienia operatora + lub -, po którym znajduje się następny składnik. Instrukcja switch identyfikuje i przetwarza te operatory, Jeśli przyzwyczailiśmy się do natywnego C++, to możemy czuć pokusę napisania instrukcji case w instrukcji switch trochę inaczej, na
łańcuch
przykład :
II Nieprawid/owy kodt! Nie
działa!!
case '+' : value +~ t erm(st r . ++(*index));
II Znaleziono znak +, a więc
II zwiększamy zmienną index o jeden i dodajemy II następny składnik.
break: Oczywiście
zwykle zapisalibyśmy ten kod bez pierwszego komentarza. Kod ten jest nieale dlaczego? Funkcja term( )jako drugiego argumentu spodziewa się uchwytu typu i nt Ą i to zostało jej tutaj podane, mimo że nie jest to to, czego byśmy się spodziewali. Kompilator powoduje obliczenie wartości wyrażenia ++( *i ndex ) oraz przechowanie wyniku w lokalizacji tymczasowej . Wyrażenie to rzeczywiście zwiększa wartość wskazywaną przez i ndex, ale uchwyt przekazywany do funkcji t erm ( ) jest uchwytem do lokalizacji tymczasowej, przechowującej wynik obliczonego wyrażenia, a nie uchwytem i ndex. Uchwyt ten został utworzony poprzez samopakowanie wartości przechowywanej w lokalizacji tymczasowej. Kiedy funkcja te rm( ) aktualizuje wartość wskazywaną przez uchwyt do niej przekazany, aktualizowana jest lokalizacja tymczasowa, a nie lokalizacja wskazywana przez i ndex. A zatem prawidłowy ,
354
Visual C++ 2005. Od podstaw wszystkie aktualizacje indeksu łańcucha wykonane w funkcji t er m( ) zo stają utracone. Jeśl i oczekuje sz, że funkcja uaktualni zmienną w wywołującym programie, to nie możesz używać wyrażenia jako jej argumentu - zawsze w takich przypadkach używaj uchwytu do nazwy zmiennej.
Sprawdzanie wartości składnika Podobnie jak w wersji w natywnym C++, funkcja term( ) prze szukuje łańcuch przekazany do niej jako pierwszy argument, zaczynając od znaku o indeksie wskazywanym przez drugi argument. II Funkcja
sprawdzająca wartość składn ika.
double term(Stri ng st r . int A
A
index)
(
double value
number( st r . index) ;
~
II Powtarzaj, dopóki
while(*i ndex
<
są
znaki i
właśc iwe
II Pobierz pierwszą
liczbę składnika.
ope ratory.
str ->Lengt h)
(
if (st r[*index] == L'* ' )
II Jeś li znajdziesz znak mnożenia,
(
++(*index) ; value *= number (st r. i ndex);
II zwiększ index i II i pomnóż przez
następną liczbę .
}
else if( st r[*index] == L' j ' )
II Jeśli znajdziesz znak dzielenia,
{
++ (*i ndex): value j~ number (str . i ndex) ;
II zwiększ index II i p odziel przez
następną liczbę.
}
el se break;
II
Wyjdź
z pętli.
} II Skończone, a
wi ęc
zw racamy, co
uzyska l iśmy.
ret urn va l ue: funkcji number( ) w celu sprawdzenia wartości pierwszej liczby lub wydobycia w nawiasie w składn iku funkcja przeszukuje łańcuch za pomocą pętli whi le. Pętla ta kontynuuje działanie, gdy w łańcuchu cały czas dostępne są znaki, dopóki nie zostanie odnaleziony operator * lub / poprzedzający jakąś inną liczbę czy wyrażenie w nawiasie.
Po
wywołaniu
wyrażenia
Sprawdzanie wartości liczby Funkcja number ( ) wydobywa i oblicza wartość wyrażenia w nawiasie, jeżeli takie zostanie znalezione. W przeciwnym przypadku określa wartość następnej liczby w łańcuchu : II Funk cja
rozpoznają ca l iczbę .
double number (Stri ng st r . i nt i ndex) A
A
(
double val ue = 0.0: II Poszukiwanie
wyrażenia w
i f (st r[*i ndex]
==
II Do przechowywania wyniku . nawi asach.
L' (' )
II Początek nawiasu.
Rozdział 6.
• Ostrukturze programu -
++ (*indexl, Str i ng substr = extract( str , i ndexl : ret urn expr(substr l : A
II Pętla zbierająca
whil e((*index
<
wiodące
ciąg
dalszy
355
II Wydoby cie podlań cu ch a w nawiasach. II Zwrócenie wartośc i podlańcucha .
cyfry .
st r ->Lengt hl && Char: . Lsflt qi t t str . *i ndex))
{
value = 10 .0*value + Char : :Get Numer icVal ue(st r[ (*i ndexl] l : ++ (*i ndexl : niebędący cyfrą.
II Znaleziono znak
if(( *i ndex -- st r ->Lengt hl II st r[ *index]
1=
' .
'l
II A więc poszukuj emy przecinka II dziesiętnego . II Jeśli nie ma, z wracamy zmienną II value.
ret urn va l ue: double factor = 1.0 : ++(*1ndex):
II Współczynn ik dla miejsc po p rzecinku. II Przesunięci e do cyf ry.
II Powtarzaj . dopóki są cyfry.
while( (*i ndex
<
str->Lengt hl && Char:: IsDigi t (st r . *indexl l
{
factor *= 0.1; II Zmniej sz wspólczynn ik dzies ięciokrotn ie. va l ue = value + Char: :Get Numeri cVa l ue(st r[ *i ndex]l *factor ; li Dodaj miejsce II p o przeci nku.
++( *i ndexl : ret urn va l ue;
II Koniec pętli -
skoń cz one.
Podobnie j ak w wersji w natywnym C++, funkcja ext ra ct() została użyta do wydobycia nawiasów, a wydobyty łańcuch zostaje przekazany do funkcji expr ( ) w celu obliczenia jego wartości. Jeżeli żadne wyrażenie w nawiasach nie zostanie znalezione (na co wskazuje brak otwierającego nawiasu) , łańcuch wejściowy jest skanowany w poszukiwaniu liczby , która składa się z szeregu zera lub w iększej liczby cyfr , po których nastę puje opcjonalny przecinek dziesiętny plus cyfry składające się na część ułamkową. Funkcja IsDigi t O pochodząca z klasy Char zwraca wartość t ru e ,jeżeli znak jest cyfrą, lub fa lse w przeciwnym przypadku. Znak ten znajduje się w łańcuchu przekazanym do funkcji jako pierwszy argument w lokalizacji wskazywanej przez drugi argument. Istnieje także inna wersja funkcji IsDigit O przyjmująca pojedynczy argument typu wchar_t ; moglibyśmy jej użyć z argumentem str [*i ndex]. Pochodząca z klasy Cha r funkcja GetNumeri cValueO zwraca jako typ doub1e wartość znaku cyfry Unicode, która przekazywana jest jako argument. Istnieje także inna wersja tej funkcj i, do której można przekazać uchwyt do łańcucha oraz indeks określający konkretny znak . wyrażen ia spomiędzy
Wydobywanie wyrażenia wnawiasach Funkcję
ext ract ()
tować następująco :
zwracającą podłańcuch znajdujący się
w nawiasach
możemy
zaimplemen-
356
VisUalC++ 2005. Od podstaw wydobywająca podlańcuch IV
II Funkcj a
na wiasach.
St ri ng A extract (Str i ng A st r . 1nt A index) ( II Tym czasowe miej sce dla podlań cu cha .
arr ay<wcha r_t>A buffer = gcnew array<wc har_t>(st r- >Length ): Stri ngA subst r : II Podlancuch. ktory ma zosta ć zwrócony. i nt numL = O; II Licznik znalezionych lewych nawiasów . i nt bufi ndex = *1ndex: II Zachowaj wart oś ć po czątkową wskaźnika index. whi le(*lndex < st r ->Length) {
bu ffer[*index - bufindex] swi t ch(st r[*i ndex])
~
st r[*i ndex] ;
(
case ') ': if(numL == O) {
array<wchar_t>A subst rChars = gcnew array<wcha r_t >(*l ndex - bufi ndex): st r->CopyTo (buf index. subst rChars . O. substrChars->Length ): subst r = gcnew Str i ng( substrChars): ++( *i ndex): ret urn subst r :
II Zwróć
lań cuch
w nowej pam ięci .
}
el se numL -- : break: case . (' : numL++ : break :
W tym przypadku znowu strategia jest taka sama jak przy użyciu natywnego C++, ale różni w szczegółach . W celu znalezienia pasującego nawiasu zamykającego funkcja w zmie nej Ln um zapisuje liczbę nowych nawiasów otwierających. Podłańcuch zostaje wydobyt gdy zostaje odnaleziony nawias zamykający oraz wartość licznika lewych nawia sów Lnumw nosi zero. Podłańcuch ten zostaje skopiowany do tablicy substrCha rs za pomocą funkcji Cop To( ) dla obiektu klasy Str i ng o nazwie str. Funkcja kopiuje znaki , rozpoczynając od znal znajdującego się w miejscu określonym przez pierwszy argument, do tablicy określon w drugim argumencie. Trzeci argument określa element, od którego zacząć operację w tabli docelowej, a czwarty określa liczbę znaków do skopiowania. Łańcuch zwrócony w wynil wydobywania tworzymy za pomocą konstruktora klasy St ri ng, który z wszystkich element ć tablicy buduje obiekt - subst rCha rs , przekazywany jako argument. tkwią
Po
złożeniu
wszystkich tych funkcji w jedną całoś ć w projekcie konsolowym CLR otrzymar C++ /CLI kalkulatora, działającą pod kontrolą CLR. Rezultat powinien b taki sam j ak przy użyciu natywnego C+ +.
implementację dokładnie
Rozdział 6.•
Ostrukturze programu -
ciąg ltalsz,
357
Podsumowanie Mamy już szeroką wiedzę na temat tworzenia i stosowania funkcji. Użyliśmy wskaźnika do funkcji w praktycznym kontekście w celu obsłużenia sytuacji wyjątkowej związanej z brakiem pamięci w obszarze wolnym. Zastosowaliśmy także przeładowywanie funkcji w celu zaimplementowania zestawu funkcji wykonujących te same zadania, ale z parametrami różnych typów . Więcej na temat przeładowywania funkcji dowiemy się w następnych rozdziałach . Poniżej
znajduje
się
lista
najważniejszych zagadnień
poruszonych w tym rozdziale:
•
Wskaźnik do funkcji przechowuje jej adres oraz informacje parametrów oraz typu zwracanego przez funkcję.
•
Wskaźnika do funkcji można użyć do przechowywania adresu dowolnej funkcji z właściwym typem zwracanym oraz liczbą i typem parametrów.
•
Wskaźnik
do funkcji
Wyjątkiem
liczby i typu
można wykorzystać
Wskaźnik można również przekazać
•
dotyczące
do jej wywołania w adresie, który zawiera. do funkcji jako jej argument.
jest sposób sygnalizowania błędu w programie, tak od kodu pozostałych operacji .
że
kod
obsługi
tego
błędu można oddzielić
•
Wyjątki wywołuje się
•
Kod mogący powodować wyjątki powinien być umieszczony w obrębie bloku t ry, a kod obsługujący określony wyjątek w klauzuli cat ch znajdującej się bezpośrednio po bloku t ry. Po bloku try może wystąpić kilka klauzul cat ch, z których każda przechwytuje wyjątek innego typu.
•
Funkcje przeładowane to funkcje o takiej samej nazwie, ale różnej
•
Kiedy wywoływana jest przeładowana funkcja, ta, która ma zostać wywołana,
wybierana jest przez kompilator na podstawie liczby oraz typów podanych argumentów.
•
za
pomocą
instrukcji z użyciem
słowa
kluczowego throw.
liście
parametrów .
Szablon funkcji jest przepisem na automatyczne wygenerowanie funkcji przeładowanych.
•
Szablon funkcji ma co najnmiej jeden argument, który jest zmienną typu. Egzemplarz funkcji (tzn. definicja funkcji) tworzony jest przez kompilator przy każdym wywołaniu funkcji , które odpowiada unikalnemu zestawowi argumentów typu dla szablonu.
•
Kompilator
można zmusić
funkcję, którą chcemy Zdobyliśmy także
do utworzenia egzemplarza funkcji z szablonu, w deklaracji prototypu.
określając
nieco doświadczenia w używaniu wielu funkcji w programie, tworząc przykalkulator. Należy jednak pamiętać, że wszystkie dotychczasowe techniki stosowania funkcji były wykorzystywane w kontekście tradycyjnego programowania proceduralnego. Kiedy zaczniemy programowanie w podejściu zorientowanym obiektowo , to nadal będziemy bardzo często używać funkcji, ale nasze podejście do struktury programu oraz projektowania rozwiązań problemów ulegnie radykalnej zmianie.
kładowy
358
Visual C++ 2005. Od podstaw
Ćwiczenia Kod
ź ró d łowy
wszystk ich
przykład ów
w tej
książce
oraz
rozwiązania
do
ćwiczeń można
pobrać ze strony www.helion.pl.
l
Przyjrzyj
się pon iższej
funkcji:
i nt ascVal (s i ze_t i . const char* p)
{ II Drukuj wartos ć AS CII znaku. i f ( Ip II i > st rlen(p))
ret urn - l ; el se retu rn p[ i]:
Napisz program wywołujący tę funkcję poprzez wskaźnik i potwierdzający, że d ziała . Do wykonania tego zadania będz ie potrzebna dyrektywa #i nc l ude dołączająca nagłówek w celu umo żliwienia korzystan ia z funkcji st r l en( l-
2. Napisz rodzinę funkcji przeciążonych o nazwie equal O , które przyjmują dwa argumenty tego samego typu , zw racające l , jeżeli argumenty te są równe, lub Ow przeciwnym przypadku. Napis z wersje z typami argumentów char, i nt, doub l e oraz char*. Do sprawdzani a, czy łańcuch y są równe , wykorzystaj funkcję st rcmp( l z biblioteki wykonawczej . Jeżeli nie wiesz , jak u żyć funkcji st rcmp( l, poszukaj informacji na ten temat w internecie. Potrzebna będzie dyrektywa #include dołączająca nagłówek do programu. Napisz kod sprawdzający, czy w ywoływan e są właściwe wersje funkcji.
a.
Jeżeli
do kalkulatora w obecnej postaci podamy nieprawidłowy łańcuch , to ukaże komunikat o błędzie , ale nie informujący o miejscu jego wystąpienia. Napisz procedurę drukującą wprowadzony łańcuch i umieszczającą znak daszka (A) pod znakiem, który spowodował błąd , jak pon iżej : się
12
+
4.2*3
.. Dodaj do kalkulatora operator potęgowania C"), umieszczając go na równi z operatorami * i /. Jakie są ograniczenia takiej implementacji i jak można sobie z nimi poradzić?
S. Dla zaawansowanych: rozszerz kalkulator, aby obsługi wał funkcje trygonometryczne i inne funkcje matematyczne, pozw alając na wpisywanie
wyrażeń
takich jak:
2 * s i n(O .6)
Wszystkie funkcje z biblioteki mat h pracują na radianach. Stwórz własn e wersje funkcji trygonometrycznych pozwalające na używanie stopni, na przykład: 2
* sind (30)
7 Deliniowanie własnych
typÓW danych
Rozdział ten poświęcony jest tworzeniu własnych typów danych, które służą do rozwiązy wania pewnych określonych problemów. Będziemy także mówić o tworzeniu obiektów, co stanowi podwaliny programowania zorientowanego obiektowo. Początkującym obiekty mogą wydawać się trochę tajemnicze, ale - jak przekonamy się w tym rozdziale - są one po prostu egzemplarzami naszych własnych typów danych . się :
W rozdziale tym dowiesz •
Czym
są struktury
i jak się ich używa.
•
Czym
są
•
Jakie
•
Jak
•
Jak kontrolować
•
Czym
•
Czym jest konstruktor
•
Jak
•
Czym jest konstruktor kopiujący i jak wygląda jego implementacja.
•
Jaka jest różnica pomiędzy klasami w C++/CLI a klasami w natywnym C++.
•
Jakie
•
Czym
są
•
Czym
sąpola
•
Czym jest konstruktor statyczny.
klasy i j ak
się
ich
używa.
są podstawowe składniki
się
klasy oraz jak się definiuje typy klasowe .
tworzy i używa obiektów klas. dostęp
są konstruktory
używać
do
składowych
i jak się je tworzy. domyślny.
referencji w
kontekście
właściwości mają klasy
pola
klasy .
literałowe
klas.
w C++/CLl oraz jak się je definiuje i ich używa.
oraz jak się je definiuje i ich używa.
i nitonly oraz jak sięje definiuje i ich używa .
360
Visual C++ 2005. Od podstaw
S'lruktury W jęZykU C++
Struktura to typ definiowany przez programistę za pomocą słowa kluczowego st ruct. Słowo to pochodzi jeszcze z języka C, a C++ przejął je oraz rozszerzył jego zakres. W C++ strukturę można zastąpić klasą, gdyż wszystko, co można za jej pomocą osiągnąć, można również zro bić, używając klas . Jednak ze względu na fakt , że system Windows został napisany w języku C, zanim zaczęto szerzej używać C++, słowo kluczowe struct jest wszechobecne w progra mowaniu dla tego systemu. Struktury są do dziś używane, dlatego należy je znać. Najpierw zajmiemy się strukturami (w stylu C), a później przejdziemy do oferujących większe możli wości klas.
Czym jest slruktura Prawie wszystkie zmienne, które widzieliśmy do tej pory, mogły przechowywać dane jednego typu - liczbę , znak lub tablicę elementów tego samego typu. Prawdziwy świat jest jednak trochę bardziej skomplikowany. Do opisania dowolnego obiektu fizycznego, nawet w mini malnym stopniu, potrzebujemy co najmniej kilku jednostek danych. Pomyślmy na przykład , ile informacji trzeba podać, aby opisać tak prosty przedmiot jak książka. Możemy podać autora, wydawcę, datę wydania, liczbę stron, cenę, tematykę oraz numer ISBN i nie jest to bynajm niej koniec listy. Do przechowywania każdego z wymienionych parametrów potrzebnych do opisania książki możemy zdefiniować oddzielną zmienną, ale najlepiej by było , gdybyśmy mieli jeden typ danych, na przykład KSIAZKA, który reprezentowałby wszystkie te typy . Jestem pewien, że się nie zdziwisz, kiedy powiem, że do tego właśnie celu używa się struktur.
Definiowanie struktury Pozostańmy
przy przykładzie z książką. Przypuśćmy, że w jej definicji chcemy umieścić nastę informacje: tytuł, autor, wydawca oraz rok publikacji. Strukturę do przechowywania tych informacji możemy zdefiniować w następujący sposób: pujące
st ruct KSI AZ KA {
cha r Tyt ul [BO);
char Aut or[ BO) ;
cha r Wydawca[BO);
i nt Rok;
}; Powyższy kod nie tworzy żadnych zmiennych, ale definiuje nowy ich typ o nazwie KSI AZ KA. W definicji tej użyliśmy słowa kluczowego struct , a obiekty składające się na naszą książkę podaliśmy pomiędzy nawiasami klamrowymi. Warto zauważyć, że każdy wiersz zawierający definicję obiektu struktury zakończony jest średnikiem oraz że pojawia się on także po klamrze zamykającej . Elementy struktury mogą być dowolnego typu, z wyjątkiem takiego samegojak struktura. Nie można umieścić elementu typu KSIAZKA w definicji struktury KSI AZKA. Może się wydawać, że jest to pewne ograniczenie, ale zamiast tego do definicji możemy wstawić wskaź nik do zmiennej typu KS IAZKA, o czym przekonamy się już niebawem .
Rozdział 7.•
Deliniowanie własnych typÓW danych
361
Elementy Tytu l , Autor , Wydawc a oraz Rok, znajdujące s ię pomiędzy nawiasami klamrowymi , nazywają s i ę składowymi lub polami struktury KS IAZ KA. Każdy obiekt typu KSIAZKA zawiera pola Tytu l , Autor, Wydawca oraz Rok. Zmienne typu KSIAZKA możem y teraz tworzyć w dokładn ie taki sam sposób jak każde inne zmienne: KSI AZKA Powiesc :
II Deklaracja zmi ennej Powiesc typu KSIAZKA.
w powyż szym kodzie zad ek l aro wa l iś my zm i e n n ą o nazwie Pawi esc, której możemy używać do przech owywania informacji o ksi ążc e . Jedyne, czego teraz potrzebujemy, to nauczyć si ę umieszc zania informacji w poszczególnych składowych, które składają się na zmienną typu KS I AZKA.
Inicializowanie struktury Pierwszym sposobem dostar czenia danych do składowych struktury jest zdefini owanie warto śc i początkowych w jej deklaracji. Przypuśćmy, że zmienną Powt esc chcieliśmy za in i cja l i zow ać danymi dotyczącymi jednej z naszych ulubionych książek - Programowanie dla opornyc h, wydanej w 1981 roku przez wydawnictwo Rynsztok. Jest to historia faceta, który bohatersko pisał kod programu, nie wychodząc z igloo. Jak się pewnie domyśl asz , książka ta stała się inspi racj ą znanego hitu kasowe go w Hollywood, zatytu łow aneg o Przeminęło z wiadrem. Autorem był niejaki l.C. Palec , do którego należy także trzytomowe nowatorskie dzieło Przewodnik konesera po spinaczach do papieru. Maj ąc takie bogactwo wiedzy, możemy przystąpi ć do pisa nia deklaracji zmiennej Pawi esc: KSIAZKA Powi esc
~
{ "Programowanie dl a opor nych". " I .C. Pa l ec" . "Wydawni ct wo Rynsztok" . 1981
II II II II
Wartość początko wa składo wej
Tytul. Aut or. Wartość począ tko wa skladowej Wydawca. Wartos ć począ tko wa skladowej Rok.
Wartość początko wa s kłado wej
}: Warto ści początkowe umieszczone zostały między nawiasami klamrowymi i oddzielone od siebie przecinkami w podobny sposób jak przy podawaniu wartości początkowych dla ele mentów tablicy . Tak jak w tablicach kolejność wartości początkowych musi być o czywiście taka sama jak kol ejność odpowi ad ających im pól w defini cji struktury.
Uzyskiwanie dostępu do pól strukturV W celu uzyskania dostępu do pól struktury możemy posłuży ć się operatorem wyboru składo wej , który ma postać kropki. Aby odnieść s ię do określon ej s kład owej, piszemy nazwę zmien nej struktury, po niej kropkę, a następnie nazwę składowej , do której chcemy uzyskać dostęp. Aby zmienić zawartość składowej Rok struktury Pawi esc, możem y po służyć się n astępującą instrukcją:
Powiesc .Rok
~
1988:
362
Visual C++ 2005. Od podstaw Powyższa instrukcja spowodowałaby ustawienie wartości pola Rok na 1988. Ze składowej struktury możemy korzystać w dokładnie taki sam sposób jak z każdej innej zmiennej takiego samego typu co to pole. Aby zwiększyć wartość pola Rok o dwa , możemy użyć następującej instrukcji :
Powiesc .Rok
+=
2;
Powyższa każdej
instrukcja zwiększa innej zmiennej.
wartoś ć
pola Rok struktury w taki sam sposób jak w przypadku
~ Uiywallie slrukll.lr Sposób uzyskiwania dostępu do pól struktury przećwiczymy teraz na innym przykłado wym programie konsolowym. Przypuśćmy, że chcemy napisać program dotyczący niektó rych elementów znajdujących się na podwórku , takich jak te, które zostały przedstawione na rysunku 7.1.
Rysunek 7.1
Dom Współrzędne położen ia
0,0
~--+--+- 70 ----+----+---~ o
.
o
co
30-H-~
Basen +---+-+---1-
70
-------+-+i
10
Współrzędne położenia
100,120
Rozdział 7.•
Definiowanie własnych typÓW danych
363
Współrzędne O, O postanowiłem umieścić w lewym górnym rogu podwórka. Prawy dolny róg ma współrzędne 100, 120. A zatem pierwsza współrzędna określa położenie w poziomie wzglę dem lewego górnego rogu (wartości rosną od lewej do prawej), a druga współrzędna określa położenie w pionie w odniesieniu do tego samego punktu (wartości rosną od góry do dołu).
Na rysunku 7.1 widać również położenie względem lewego górnego rogu podwórka basenu oraz dwóch chat. Jako że podwórko, chaty oraz basen są prostokątami , możemy zdefiniować typ st ruct reprezentujący dowolny z tych obiektów:
st ruc t RECTANGLE {
int int int int
Left: Top; Right; Bottom;
II Para współrzędnych II punktu znajdującego s i ę w lewym górnym rogu. II Para współrzędnych II punktu znajdującego się w prawym dolnym rogu .
}:
Pierwsze dwa pola struktury RECTANGL E odpowiadają współrzędnym lewego górnego rogu prostokąta, a pozostałe dwie współrzędnym prawego dolnego rogu . Struktury tej użyjemy w prostym programie wykonującym pewne operacje na obiektach z podwórka: II Cw?_Ol .cpp
II Ćwiczenie struktur na podwórku.
#include using st d; :cout ; using std.rcndl : II Definicja struktury reprezentującej prostokąty.
struct RECTANGLE {
int Left ; int Top :
II Para współrzędnych II punktu znajdującego się w łewym górnym rogu.
int Right ; int Bottom ;
II Para współrzędnych II punktu znajdującego się w prawym dolnym rogu II podwórka.
}; II Prototyp funkcji
obliczającejpowierzchnięprostokąta.
long Area(RECTANGL E& aRect): II Prototyp funkcji przesuwającejprostokąt.
void MoveRect(RECTANGLE& aRect . int x. int
y );
int ma in(void) {
RECTANGLE Ya rd ~ { O. O. 100. 120 l: RECTANGLE Pool = { 30. 40. 70. 80 l. RECTANGLE Hutl . Hut2; Hutl .Left = 70;
Hutl .Top = 10:
Hut l.Right = Hut l .Left Hut l.Bottom ~ 30:
Hut2 = Hutl;
+
25;
II Definicja przypisująca Hut2
wartość
Hutl.
364
Visual C++ 2005. Od podstaw MoveRect CHut 2, 10, 90):
ret urn O: II Funk cj a o b liczając a powierzchnię pros toką ta .
10ng AreaCRECTANGLE& aRect ) (
retu rn CaRect ,R ight . aRect .Left )*(aRect ,Bot tom - aRect .Top) : II Funk cja p rzesuwająca prostokąt.
void MoveRect CRECTANGL E&aRect, int x. int y) {
int 1ength = aRect .Right aRect .Left : int widt h = aRect .Bot tom aRect Top:
II Pobi erz długość prostokąta . II Pobi erz szerokość prostokąta .
aRect. Left = x: aRect .Top = y: aRect ,Ri ght = X + 1engt h:
II Ustaw lewy górny punkt II w nowej łokaliza cji . II Wsp ółrzędne punktu pra wego dołn ego rogu II ob licz jako II przyr ost od nowego położenia,
aRect .Bot t om = y
+
width:
return: Rezultat
działania
tego programu przedstawia się
następująco:
chaty Hut 2 t o 10,90 i 35,110
Powi erzchnia podwórka wynosl 1200 0
Powierzchnia basenu wynosi 1600
W s pó ł r z ęd n e
Jak to działa Zauważ, że
definicja struktury znajduje s ię w powyższym przykładzie w zasięgu globalnym, w zakładce Class View projektu, Dzięki takiemu zlokalizowaniu tej definicji zmienne typu RECTAINGLE możemy deklarować w dowolnym miejscu pliku .cpp. W programie złożonym z większej ilości kodu takie definicje zostałyby umieszczone w pliku z rozszerzeniem .h, a następnie - w razie potrzeby - dołączone za pomocą dyrektywy #lOC ' l ude do każdego pliku .cpp . Możemy ją także znaleźć
Rozdzial7.• Definiowanie własnych typÓW danych
365
Do przetwarzania obiektów typu RECTANGL E zdefiniowali śmy dwie funkcje. Funkcja Area () oblicza powierzchnię obiektu typu RECTANGLE, który przekazujemy jako argument w postaci referencji jako iloczyn długości i szeroko ści, gdzie długo ść to różni c a pomiędzy poziomymi położeniami definiujących punktów, a sz erokość to różnica pomiędzy pionowymi położe niami definiuj ących punktów . Dzięki przekazaniu referencji kod dzi ała nieco szybciej , gdyż argument nie j est kopiowany. Funkcja MoveRect() modyfikuje defin iujące punkty obiektu RECTANGLE w celu umieszczenia go w punkcie o współrzędnych x, y, które przekazywane są do niej jako argumenty. Mówiąc o położeniu obiektu RECTANGLE, mam na myśli położenie jeg o lewego górnego wierzchołka . Jako że obiekt RECTANG LE został przekazany jako referencj a, funkcja może b ezpośrednio modyfikowa ć zawartość jeg o pól. Po obliczeniu długości i szero kości przekazanego obiektu pola Left i Top zostają odpowiedn io ustawione na x i y, a warto śc i pól Ri ght i Bottomsą obliczane poprzez zw iększenie x i y o długo ś ć i szeroko ś ć oryginalnego obiektu RECTANGLE. W funkcji mai n( )
zainicjal izowali śmy
zmienne typu RECTANGLE o nazwach Yard i Pool warto które widać na rysunku 7.1. Zmienna Hut l reprezentuje chatę znajdu jącą si ę w prawym górnym rogu ilustracj i, a jej pola zostały ustawione na właściwe wartości za pomocą instrukcji przypisania. Zmienna Hut2, odpowiadając a chacie po lewej stronie na dole rysunku, najpierw zost ał a ustawiona na taką samą warto ś ć j ak zmienna Hutl za pom o cą poniższej instrukcj i przypis ania: ści ami współrzędnych,
Hut 2 = Hutl :
II Defin icja p rzypisująca Hut2
wartość
Hutl .
Instrukcja ta spowoduje skopiowanie wartości pól zmiennej Hutl do odpowiadających im pól zmiennej Hut 2. Strukturę danego typu można przypisać tylko do innej struktury takiego samego typu. Nie można b ezpo średnio zwiększać struktury ani używać j ej w wyrażeni ach arytmetycznych. Aby
zmieni ć położenie
chaty Hut 2 do jej miejsca na dole po lewej stronie podwórza, wywo MoveRect ( ), jako argumenty podaj ąc żądane współrzędne . Ten pokrętny spo sób uzyskiwania współrzędnych chaty Hut 2 jest całkowicie niepotrzebny i służy wyłącznie jako przykład użycia struktury jako argumentu funkcji . łujemy funkcję
Pomoc mechanizmu Intellisense wpracy ze strukturami Jakjuż prawdopodobnie udało Ci się zauważyć, edytor w Visual C++ 2005 jest bardzo inteli gentny - zna na przykład typy wszystkich zmiennych. Jeżeli najedziemy kursorem na jakąś zmi enną w oknie edytora, to pokaże s i ę mała chmurka zjej definicją. Funkcj a ta może być także bardzo pomocna w pracy ze strukturami (a także klasami , o czym się wkrótce przeko namy), p oni eważ zna ona typy nie tylko zwykłych zmiennych, lecz takż e typy składowych należących do zmiennej struktury określonego typu. Jeżeli Twój komputer jest wystarczająco szybki, to po wpisaniu operatora wyboru składowej po nazwie zmiennej strukturalnej edytor wy świetli chmurkę z awierającą listę wszystkich s kład owych . Kliknięcie jednej z nich spo woduje pojawienie się komentarza, znajdującego s ię w oryginalnej definicji tej struktury, dzięki czemu wiemy, do czego ona służy. Sposób działania tego mechanizmu przedstawiono na rysun ku 7.2, na którym widoczny jest fragment kodu powyższego programu.
366
Visual C++ 2005. Od podstaw
Rysunek 7.2
00
ii
61
v o id Hove:Re ct ( RECT ANGL E & age c t ,
52 63 64
(
Funkc ja p r
i nt
l eng t h
i nt
e t.d t. b
65
66 ';;7
1
6::69 !
701 71; , ,
aRe ct. Left e ne c c . Top
. . . .
e.Rect. Right a Rec t
.1
~e~ u ~ ~ JąCd.
pr a~t ok ~ t
.
r n t; x ,
i nt
oRec t . Right - enec c . Lett ; e nec c . Bo t t om - enec c . TOp;
y)
/ / Pob i e r z.
fI Pob le rz
długość: p r c a r.o k qc a . a ae r o ko a ć p r o s t c k ąt a .
// TJst ,aIJ l evy g or: n y pu nkt.
x;
.x+
fI
y;
ii
le ngth:
n o a e j l o kal i aec j i . Łr z ę dne p u n k tu pra wego dol
"jl ap ó
. ~ ~ " I RECTANGLE : :Botttlm " Left RiJhl >ł Top
V
punktu znajdując ego s ię w praw ym dolnym rogu podwórka. File: cw7 D1a.cpp
Jest to bardzo ważny argument za stosowaniem krótkich i treściwych komentarzy do kodu. Dwukrotne kliknięcie lub naciśnięcie klawisza Enter w momencie, gdy jeden z elementów listy jest podświetlony spowoduje automatyczne jego wstawienie po operatorze wyboru skła dowej , likwidując w ten sposób jedno ze źródeł literówek w kodzie. Wspaniale, prawda? Jeśli chcemy, to możemy niektóre właściwości mechanizmu lntellisense lub cały ten mecha nizm wyłączyć w menu Tools/Options, ale wydaje mi się, że jedyna sytuacja, w której może być to potrzebne, to zbyt wolny komputer niepozwalający na efektywne jego wykorzystanie. Właściwości Statement completion (uzupełnianie instrukcji), można włączyć lub wyłączyć po prawej stronie karty edytora C/C++. Aby przywrócić te opcje po ich wyłączeniu można użyć menu Edit lub dokonać tego za pomocą klawiatury. Na przykład wciśnięcie kombinacji klawiszy Ctr/ +J powoduje pokazanie się pól obiektu znajdującego się pod kursorem. Edytor pokazuje również listę parametrów funkcji podczas wpisywania kodu do jej wywoływania lista ta pokazuje się po wpisaniu otwierającego nawiasu zawierającego argumenty. Właściwość ta jest szczególnie przydatna w pracy z funkcjami bibliotecznymi, ponieważ bardzo trudno jest zapamiętać ich wszystkie parametry. Oczywiście, w kodzie programu musi się już znaj dować dyrektywa #i nc1ude dołączająca odpowiedni plik nagłówkowy . Bez tego edytor nie wiedziałby, co to jest za funkcja. Więcej na temat pomocy ze strony edytora dowiemy s i ę przy okazji omawiania klas .
Po tej krótkiej i niezwykJe
interesującej
dygresji
przejdźmy
z powrotem do struktur.
Struk'lura HECY W programach dla systemu Windows bardzo często wykorzystuje się prostokąty. Z tego też powodu w pliku nagłówkowym windows.h znajduje się predefiniowana struktura RECT. Jej definicja jest dokładnie taka sama jak definicja struktury zdefiniowanej przez nas w ostatnim przykładzie:
st ruct RECT {
i nt 1ef t : i nt to p:
II Para współrz ędnych
II górn ego lewego wierzchołka.
i nt r ig ht: i nt bot t om;
II Para wspó łrzędnych
II prawego dolnego wierz ch ołka.
} :
-
Rozdzial7.• Definiowanie własnJch tJPÓW danJch
367
Struktura ta jest zazwyczaj używana do definiowania w różnych celach prostokątnych obsza rów na ekranie. Jako że struktura RECT jest tak często używana, plik nagłówkowy windows.h zawiera również prototypy kilku funkcji służących do manipulowania prostokątami oraz ich modyfikacji . Na przykład dostępna jest funkcja In fl ateRect< ) zwiększająca rozmiar prosto kąta oraz funkcja Equal Rect ( ) porównująca dwa prostokąty. W bibliotece MFC zdefiniowana jest także klasa CRect, która jest ekwiwalentem struktury RECT. Kiedy poznasz już klasy, to z pewnością będziesz z niej korzystać częściej niż ze struk tury RECT. Klasa CRect dostarcza wiele funkcji do manipulowania prostokątami, z których bar dzo często się korzysta, pisząc programy dla systemu operacyjnego Windows przy użyciu bi bliotek MFC .
Używanie wskaźników ze strukturami Jak się można było spodziewać, do zmiennych strukturalnych można tworzyć wskaźniki. W rzeczywistości wiele funkcji zadeklarowanych w pliku nagłówkowym windows. h, które pracują z obiektami RECT, wymaga jako argumentów wskaźników do struktur RECT, ponieważ pozwalają one na uniknięcie kopiowania całej struktury w momencie przekazania argumentu do funkcji.
RECT* pRect = NULL:
II Definicja
Zakładając, że zdefiniowaliśmyobiekt wić
w normalny sposób za
pRect = &aRect :
wskaźnika
do RECr
RECT (aRect) , wskaźnik do tej zmiennej pobrania adresu:
możemy
usta
pomocą operatora
II Ustaw
wskaźnik
na adres zmiennej aRect.
dowiedzieliśmy się
podczas wprowadzania koncepcji struktury, struktura nie może za pola tego samego typu co ona sama, ale może zawierać wskaźnik do struktury tego sa mego typu co ona. Na przykład:
Jak
wierać
st ruct List Element {
RECT aRect: ListElement* pNext :
II Pole RECr struktury. II Ws kaźn ik do elementu listy .
}:
Pierwszy element struktury Li stEl ement jest typu RECT, a drugi jest wskaźnikiem do struk tury typu Li stEl ement - takiego samego typu jak struktura właśnie definiowana (należy pamiętać, że element ten nie jest typu Li st El ement, a typu wskaźnik do Li stEl ement). Pozwala to na utworzenie takiego łańcucha obiektów typu Li st El ement, w którym każdy obiekt struk tury Li stEl ement może zawierać adres następnego obiektu Li stEl ement. Ostatni obiekt będzie miał wskaźnik zerowy. Zostało to zilustrowane na rysunku 7.3. Każda ramka na diagramie reprezentuje obiekt typu Li st El ement . W polu pNext każdego obiektu, z wyjątkiem ostatniego zawierającego zero , znajduje się adres następnego obiektu w łańcuchu . Tego rodzaju struktury nazywają się listami powiązanymi . Ich zaletą jest to, że jeżeli znamy adres pierwszego elementu listy, to możemy znaleźć wszystkie pozostałe. Jest to szczególnie ważne w przypadku dynamicznego tworzenia zmiennych, ponieważ za pomocą
368
Visual C++ 2005. Od podstaw
Rysunek 7.3
lE1
lE2
lE3
pola: aRect pnext = &lE2 -
pola : aRect pnext = &lE3 -
pola : aRect pnext = &lE4
I---
-l
~lE5
lE4
pola: aRect pnext = &lE5 -
-
pola: aRect pnext =0
Brak dalszych elementów
listy powiązanej można je wszystkie śledzić. Za każdym razem, gdy tworzona jest nowa zmien na, zostaje ona po prostu dołączona na końcu listy poprzez dodanie jej adresu do składowej pNext ostatniego elementu łańcucha.
Uzyskiwanie dostępu do pól struktury poprzez wskaźnik Przyjrzyjmy s ię poniższym instrukcjom:
RECT aRect ~ { O. O. 100. 100 }: RECT* pRect ~ &a Rect: Pierwsza z nich definiuje obiekt aRect jako obiekt typu RECT, pierwszą parę jego pól inicjali zując wartościami (O, O), a drugą parę wartościami (100, 100). Druga instrukcja deklaruje pRect jako wskaźnik do typu Rect oraz inicjalizuje go adresem obiektu aRect . Dostęp do pól obiektu aRect można uzyskać poprzez wskaźnik za pomocą następującej instrukcji:
(*pRect) ,Top
+~
10:
II Zwiększ pole Top o 10.
Umieszczenie części instrukcji wyłuskującej wskaźnik w nawiasach było tutaj konieczne, ponieważ operator wyboru składowej ma większy priorytet niż operator wyłuskania. Bez nawiasów wskaźnik zostałby potraktowany jako struktura i nastąpiłaby próba wyłuskania tej składowej, a więc instrukcja nie dałaby się skompilować. Po wykonaniu powyższej instruk cji składowa Top będzie miała wartość 10 i oczywiście pozostałe składowe zostaną zmienione. Użyta tutaj metoda uzyskiwania dostępu do pól struktury poprzez wskaźnik wydaje się nie co nieporęczna. Jako że operacje tego typu w języku C++ mają miejsce bardzo często, został utworzony specjalny operator pozwalający wyrazić to samo w o wiele bardziej czytelnej i intu icyjnej formie . Zajmiemy się teraz tym operatorem.
Operator pośredniego
dostępu
do składowych
pośredniego dostępu do składowych (- » służy do uzyskiwania dostępu poprzez do pól struktury. Operator ten wyglądem przypomina niewielką strzałkę (-» i zbu dowany jest z symbolu większości (» poprzedzonego znakiem odejmowania ( -). Instrukcję, w której uzyskiwaliśmy dostęp do pola Top obiektu aRect, przy użyciu tego operatora możemy przepisać w sposób następujący:
Operator wskaźnik
Rozdział 7.•
pRect->Top
+=
10 :
Deliniowanie własnych typów danych II Zw iększpo le
369
Top 0 10.
Jak w i d ać, instrukcja ta jes t o wiele bardziej tre ściwa. Operatora pośredniego do stępu do skła dowych używ a s i ę także w przyp adku klas i do koń ca k siążki zetkniemy s i ę z nim je szcze wielokrotnie.
Typy danych. obiekty. klasy i egzemplarze Zanim przejdziemy do języka, skład n i oraz technik programowania klas, spróbujemy s i ę, jak nasza dotychczasowa wiedza ma się do koncepcji klas.
przyjrze ć
Do tej pory uczyli śmy s i ę, że w natywnym C++ mo żn a tworzyć zmienne dowolnego rodzaju z fundament alnych typów danych : i nt, l onq, doubl e itd. Dowiedzieli śmy się także, że za pomocą s ło w a klu czowego st ruct m ożna defini o w a ć struktury, kt órych na stępnie mo żna używać jako typy zmiennych r epre z entujących zbiór kilku innych zmiennych. Zmienne typów fundamentalnych nie pozwalają na adekwatne odwzorowywanie obiekt ów św i ata rzeczywi stego (ani też wymyślonych obiektów). Trudno by było na przykład odwzorować pud ełk o za pomocą typu i nt, ale właściwo ści takiego obiektu m ożn a zdefiniować w polach struktury. Wymiary takiego pudełka można zdefiniować w zmiennych l ength, widt h oraz hei ght, które n a stępnie poł ączymy w jedną struktur ę o nazwie Box:
str uct Box {
doub le lengt h: double wi dth: doub le height : }: Mając taką definicję nowego typu danych o nazwi e Box, można d efini ować zmienne tego typu w taki sam sposób jak zmienne typów podstawowy ch. Można następnie tworzyć, manipulow ać i niszczyć tyle obiektów typu Sox, ile potrzebujemy. Oznacza to, że za pomo cą słowa kluczowego stru ct możemy nadawać naszym obiektom pożądane właściwości oraz używa ć ich jako fundamentów naszych programów. Czy to właśnie na tym polega programowanie zorientowane obiektowo?
Nie do końca. Programowanie zorientowane obiektowo (ang. Object Oriented Programming OOP) opiera si ę na kilku tilarach (kapsułkowanie, polimorfizm oraz dzied ziczenie), a to, co widzieliśmy do tej pory, nie odpowiada tym technikom . Nie przejmuj s ię , jeże l i nie rozumiesz, co oznaczają wym ienione terminy -tymi technikami będziemy się zajmować do końca rozdziału , a nawet k siążk i. Koncepcja struktur w C++ zo stała po suni ęta o wiele dalej niż jej oryginalna wersja w C do niej d ołączona koncep cja klasy, ści śle zw iązan a z program owani em zorientowanym obiektowo . Pojęcie klasy, która pozwala na tworzenie własnych typów danych i używanie ich podobnie jak typów natywny ch, jest jedną z fundamentalnych cech języka C++. Słowem kluczowym opisuj ącym ten koncept jest słowo c l ass. Słowa kluczowe st ruct i c l ass w C++ mają prawie identy czne znaczen ie, z wyj ątki e m kontroli dostępu do skład owych , o których
została
370
Visual C++ 2005. Od podstaw dowiemy si ę w dalszej c zęści tego rozdziału. Słowo kluczowe st ruet w CH pozo stało w celu utrzymania wstecznej zgodności z językiem C. Wszystko, czego można dokonać przy użyciu słowa kluczowego struet , można zrobić za pomocą ela ss - a nawet więcej .
}: Definiując klasę CSox, w rzeczywisto ści tworzymy nowy typ danych , podobnie jak w przy padku definicji struktury Box. Jedyną różn icą w tym przypadku jest użycie zamiast słowa kluczowego st ruet słowa el ass oraz słowa kluczowego publ i e po średniku poprzedzającym definicję składowych klasy. Zmienne definiowane jako część klasy noszą nazwę zmiennych składowych kłasy, ponieważ są zmiennymi zawierającymi dane skł ad aj ąc e się na klasę.
Nadali śmy
naszej klasie nazwę CSox, a nie Box, Mogliśmy oczywiście nazwać ją także Sox, ale konwencja MFC mówi, że wszystkie nazwy klas powinny być poprzedzone literą C- dobrze jest wyrobić sobie nawyk stosowani a takiego nazewnictwa. Zmienne składowe klasy w MFC są natomiast poprzedzane przedrostkiem m_w celu odróżnienia ich od innych zmiennych. Będę się trzymał tej konwencji . Należy jednak pamiętać, że w innych kontekstach, w których możemy używać C++ oraz zwłaszcza w C++/CLI, zasady te mogą nie obowiązywać . Cza sami konwencje nazywania klas i ich zmiennych składowych mogą być inne, a nawet może w ogóle takich konwencji nie być . kluczowe publ i e stanowi wskazówkę dotyczącą różnic pomiędzy strukturą i klasą. Jego sprawia, że zmienne składowe klasy są ogólnodo stępne, w podobny sposób jak pola struktury, choć te drugie są do stępne domyślnie . Jak przekonamy się trochę później w tym rozdziale, istnieje możliwo ść ograniczenia dostępu do zmiennych składowych klasy. Słowo
użycie
Możemy zdefiniować zmienną o
nazw ie big Box reprezentującą egzemplarz klasy CBox:
CBox bigBox: Odbywa się to tak samo jak w przypadku deklarowania zmiennej typu st ruet lub ogólnie jakiejkolwiek innej zmiennej. Po zdefiniowaniu klasy CSox deklaracje zmiennych tego typu są standardowe.
Pierwsza klasa Pojęcie
klasy zostało wymyślone przez pewnego Anglika w celu uszczęśliwienia całego narodu. Powstało ono na podstawie teorii, że ludzie, którzy znają swoje miejsce i funkcję w społeczeństwie, mają o wiele większe szanse na bezpieczne i wygodne życie niż ci, którzy go nie znają. Słynny Dane Bjame Stroustrup, który stworzył j ęzyk C++, bez wątpienia zdobył gruntowną wiedzę na temat koncepcji klasowych, studiując na uniwersytecie w Cambridge w Anglii, i zastosował tę wiedzę w bardzo udany sposób w swoim nowym języku.
Rozdzial7. • Deliniowanie własnych typÓW danych
371
Klasa w języku C++ jest bardzo podobna do angielskiej koncepcji pod tym względem , że ma ona zazwyczaj ściśle określoną rolę oraz zestaw dozwolonych czynności. Różni się jednak od klasy angielskiej pod tym względem, że ma bardzo silny wydźwięk "socjalny", koncen trując się na ważności klas pracujących. W rzeczywistości czasami klasa w C++ jest wręcz przeciwieństwem ideałów angielskich, ponieważ, jak się przekonamy, klasy pracujące w C++ żyją na koszt klas, które nic nie robią.
Operacje na klasach W języku C++ można tworzyć nowe typy danych w postaci klas w celu reprezentowan ia obiektów dowolnego typu. Jak się niebawem przekonamy , zastosowanie klas (i struktur) nie ogranicza się wyłącznie do przechowywania danych. Można także definiować funkcje skła dowe, a nawet operacje działające pomiędzy obiektami klasy przy użyciu standardowych operatorów C++. Możemy na przykład zdefiniować klasę CBox w taki sposób, że poniższe instrukcje będą działały oraz miały takie znaczenie, jakie chcemy, aby miały :
CBox boxl : CBox box2: if( boxl
>
box2)
II Zapełnij
większe pudełko .
boxl , f ill ( ) :
else
box2. rn ():
Możemy również zaimplementować
nia, a nawet mnożenia w kontekście pudełek .
pudełek
-
jako część klasy CBox operacje dodawania, odejmowa tak naprawdę każdą operację , która ma jakikolwiek sens
To, o czym teraz mówię, jest niewiarygodnie potężną techniką, która stanowi ogromny zwrot w podejściu do programowania. Zamiast rozbijać problem na części w kategoriach kompute rowych typów danych (liczby całkowite, liczby zmiennopozycyjne itd.) i następnie pisać program , będziemy programować w kategoriach typów danych związanych z problemem, mówiąc inaczej - w kategoriach klasowych. Klasy te mogą mieć nazwy CPracowani k, CKowboj lub CSe r albo CBa rszcz, z których każda została zaprojektowana w celu rozwiązania jednego rodzaju problemu. Dopełnieniem są tutaj funkcje i operatory niezbędne do manipulowania egzemplarzami tych nowych typów danych . Projektowanie programu w tym przypadku rozpoczynamy od podjęcia decyzji, jakich typów danych potrzebować będziemy dla tego konkretnego programu. Kod będziemy pisać , mając na myśli operacje na specyficznych dla tego programu problemach , np. CTrumny lub CKowboj e.
Terminologia Przed przejściem do omówienia kolejnych zagadnień związanych z klasami w C++ zrobimy mate podsumowanie terminów, których będę często używał : • Klasa to zdefiniowany przez
programistę
typ danych .
• Programowanie zorientowane obiektowo (OOP) to styl programowania oparty na koncepcji tworzenia własnych typów danych w postaci klas.
372
Visual C++ 2005. Od podstaw • Operacja deklarowania obiektu typu klasow ego czasami nazywana jest tworzeniem egzemplarza, gdyż w j ej wyniku powstaje nowy egzemplarz kla sy. • Egzemplarze klasy noszą nazwę obiektów. • Koncepcja obiektu zawierającego w swojej definicji dane wraz z funkcjami na nich operującymi nosi nazwę kapsułkowania .
Kiedy przejdziemy do szczegółów programowania zorientowanego obiektowo, może się ono czasami wydawać nieco skomplikowane. W takich przypadkach najlepiej powrócić na chwilę do podstaw, przypomnieć sobie, do czego tak naprawdę służą obiekty, a wszystko stanie s ię na powrót jasne. A służą one do pisania programów w kategoriach obiektów specyficznych dla sedna problemu. Wszystkie narzędzia związane z klasami są po to, by pisanie programów było jak najbardziej zrozumiałe i elastyczne. Przejdźmy więc do klas.
Zrozumieć klasy Klasa jest określeniem typu danych zdefiniowanego przez programistę . Mogą one zawiera ć zmienne składowe w postaci zmiennych typów podst awowych lub innych typów zdefiniowa nych przez programistę. Zmienne składowe klasy mogą być pojedynczymi elementami da nych , tablicami , wska źnikami, tablicami wskaźników prawie ka żdego rodzaju lub obiektów czy innych klas. Pozwala nam to na bardzo dużą elastyczność, jeśli chodzi o to, co mogą zawie rać klasy . W klasach mogą także znajdować się funkcje, które operują na obiektach tych klas, uzyskując dostęp do zawartych w nich zmiennych składowych. A zatem klasa łączy definicje danych elementarnych, które składają się na obiekt, ze sposobami manipulowania danymi należącymi do poszczególnych obiektów klasy. Dane i funkcje wewnątrz klasy nazywają się składowymi klasy. Elementy danych należące do klasy nazywają się zmiennymi składowymi, zaś funkcje należące do klasy - funkcjami składowymi klasy. Funkcje składowe klasy czasami nazywane s ą metodami. W tej książce nie będę używał tego terminu, ale warto wied zieć, że taka nazwa również istnieje, gdyż możemy się na nią natknąć gdzi eś indziej. Dane składowe klasy bywają także nazywane polami i ta terminologia jest używana w odnie sieniu do języka C++/CLI, w związku z czym będę z niej również czasami korzystał. Definiując klasę, definiuje się plan typu danych. Nie jest to w rzeczywistości definicja żadnych danych, ale definicja tego , co oznacza dana nazwa klasy, tzn . co będzie zawierał obiekttej klasy orazjakie operacje można na nim wykonywać. To tak samo, jakbyśmy napisali opispod stawowego typu doubl e. Nie byłaby to prawdziwa zmienna typu doubl e, ale definicja tego, jak się go tworzy i z niego korzysta . W celu utworzenia zmiennej jednego z podstawowych typów używamy instrukcji deklaracji . Jak się przekonamy, z klasami j est dokładnie tak samo.
Rozdział 7.•
Definiowanie własnych typÓW danych
373
Deliniowanie klasy Spójrzmy jeszcze raz na przykładową klasę, o której wspominałem wcześniej - klasę pudełek.
Typ danych CBox zdefin iowaliśmy za pomocą słowa kluczowego cl ass w następujący sposób:
class CBox (
publ te : double m_Lengt h: doubl e m_Widt h: double m_H eight :
II Długo s ć p udelka w centymetrach. II Szerok ość pudelka w centymetrach. II Wysoko ś ć pudelka w centymetrach.
}:
Nazwa klasy znajduje się po słowie kluczowym cl as s, a pomiędzy nawiasami klamrowymi zdefiniowane zostały trzy zmienne składowe klasy . Zmienne składowe klasy definiuje się za pomocą instrukcji deklaracji, które już dobrze znamy i lubimy. Cała definicja klasy zakoń czona j est średnikiem . Nazwy wszystkich zmiennych składowych klasy mają zasięg lokalny w tej klasie. W związku z tym takich samych nazw jak w tej klasie można używać również w innych miejscach programu, bez obawy, że dojdzie do konfliktu nazw .
Konirola doslępu Wklasie Słowo
kluczowe publ i c przypomina trochę etykietę, ale w rzeczywistości jest czymś więcej . ono atrybuty dostępu składowych klasy, które się po nim znajdują. Określenie skła dowychjako publicznych (publ t e) oznacza, że dostęp do nich w obiekcie tej klasy można uzyskać z dowolnego miejsca znajdującego się w zasięgu obiektu klasy, do któr ego należą. Składowe klasy można także określić jako prywatne (pri vate) lub chronione (p rot ect ed). Jeżeli w ogóle nie określimy rodzaju dostępu, to domyślnie zostanie zastosowany atrybut pri vat e (jest to jedyna różnica pomiędzy klasą i strukturą w C++ - domyślny określnik dostępu do składowych struktury to publ te ). Efektom zastosowania tych słów kluczowych w definicji klasy przyjrzymy się trochę później. Określa
Deklarowanie obiektów klasy Obiekt klasy deklaruje się za pomocą deklaracji takjego samego typu jak w przypadku obiek tów typów podstawowych. W związku z tym obiekty klasy CBox możemy zadeklarować za pomocą następujących instrukcji :
CBox boxl: CBox box2 : Oczywiście każdy
II Deklaracj a obiektu box l typu CBox. II Deklaracj a obiektu box2 typu CBox.
z tych obiektów (boxl i box2) to przedstawione na rysunku 7.4 .
będzie miał własne
dane
składowe. Zostało
Nazwa obiektu boxl oznacza cały obiekt, włączając w to wszystkie trzy jego zmienne skła dowe, choć zmienne te nie zostały zainicjalizowane żadnymi wartościami (zawierają po prostu jakieś przypadkowe wartości). Musimy zatem nauczyć się uzyskiwać do nich dostęp w celu nadawania im określonych wartości.
374
Visual C++ 2005. Od podstaw boxl
Rysunek 7.4 (
box2
A-
m Length
m Width
8 bajtów
8 bajtów
I
(
m Height \
8 bajtów
I
I
A
m Length
m Width
8 bajtów
8 bajtów
m Height "
8 bajtów I
Uzyskiwanie dostępu do zmiennych składowych klasy
do zmiennych składowych klasy możemy posłużyć się operatorem do składowej, którego używaliśmy do uzyskiwania dostępu do skła. dowych struktury. Aby zatem ustawić wartość składowej m_Height obiektu box2 na przykład m wartość 18. O, możemy posłużyć się następującą instrukcją: W celu uzyskania
dostępu
bezpośredniego dostępu
box2.m Height = 18.0:
II Ustawianie
wartości
zmiennej składowej.
Dostęp
do zmiennej składowej można w ten sposób uzyskać w funkcji znajdującej się poza a to tylko dlatego, że obiekt m_Hei ght został określony z dostępem publicznym . Gdyby dostęp został określony jako prywatny lub chroniony, to instrukcja taka nie dałaby się skom pilować. Niedługo zetkniemy się z podobnymi sytuacjami. klasą,
RIlmII!I Pierwsze użycie klas Sprawdzimy, czy potrafimy używać klas podobnie jak struktur. Do tego celu program konsolowy :
II Defini cja zmiennych II obiektu box2 II w kategoriach box I.
375
s kłado wyc h
II Obliczanie pojemności pudełka box I.
boxVolume
=
boxl. m_Helght*boxl .m_Lengt h*box l .m_Width :
cout « end l
« " PO jemność
p ude ł ka
boxl
=
« boxVolume :
cout « endl
« "Suma boków pud e ł k a box2 wynosi "
« box2 .m_Height+ box2 .m_Lengt h+ box2.m_W idt h
« " centymet rów . ":
cout « endl « "Ob iekt CBox zajmuje « S i zeof boxl « " ba jty . ":
II
Wyświetlanie
rozmiaru pudełka w pamięc i.
cout « endl : return O: Podczas wpisywania kodu funkcj i ma i n( ) edytor za każdym razem, gdy używamy operatora wyboru składowej po nazwie obiektu klasy, powinien wyświetlać listę nazw składowych. Żądaną składową z listy wybieramy za pomocą dwukrotnego jej kliknięcia . Jeżeli najedziemy na chw i lę kursorem na jedną ze zmiennych w kodzie, to pokaże się jej typ.
Jak lo llziała Wróćmy
tworzenie programów konsolowych, aby przypomnieć definiowania projektów konsolowych. Wszystko tutaj działa tak, jakbyśmy się spodziewali, że będzie działało przy użyciu struktur. Definicja klasy znajduje się poza funkcją ma i n( ), dzięki czemu ma zasięg globalny. Możemy zatem deklarować obiekty w dowolnej funkcji w programie, a klasa takiego obiektu będzie pojawiać się w zakładce C/ass View po skompilowaniu programu. sobie
na
chwilę
do
części opisującej
prawidłowy sposób
W obrębie funkcji main O zadeklarowaliśmy dwa obiekty typu CBax - boxl i box2. Oczywiście, podobnie jak zmienne typów podstawowych, obiekty boxl i box2 mają zasięg lokalny w funk cji ma i n( ). Obiekty typów klasowych podlegają takim samym zasadom dotyczącym zasięgu co zmienne typów podstawowych (jak np. zmienna baxVal ume użyta w powyższym programie). Pierwsze trzy instrukcje przypisania ustawiają wartości zmiennych składowych obiektu boxl . W następnych trzech instrukcjach definiujemy w kategoriach zmiennych składowych obiektu boxl wartości zmiennych składowych obiektu bax2. Następnie
boxl , będącą iloczynem wszyst obiektu. Tak obli czona wartość zostaje następnie wysłana na ekran. Później wysyłamy na wyjście sumę wartości zmiennych składowych obiektu bax2, podając wyrażenie dodające posz czególne składniki wprost w instrukcji wyjściowej. Ostatnią czynno ścią programu jest wysłanie na ekran liczby bajtów zajmowanych przez obiekt boxl , którą uzyskujemy za pomocą operatora s i zeof.
kich trzech
mamy
instrukcję obliczającą pojemność pudełka
składowych
376
Visual C++ 2005. Od podstaw Po uruchomieniu tego programu
powinniśmy otrzymać następujący rezultat:
Po je mnoś ć pu d e łk a boxl ~ 33696
Suma bok ów pud ełka box2 wynosi 66,5 cent ymetrów,
Obiekt CBox zajmuje 24 bajty.
Ostatni wiersz informuje, że obiekt boxl zajmuje 24 bajty pamięci . Liczba ta wynosi tyle, mamy trzy zmienne składowe, z których każda zajmuje osiem bajtów. Instrukcja odpowiedzialna za wyświetlenie ostatniego wiersza mogłaby z powodzeniem zostać zapisana
ponieważ
następująco :
cout « endl II Wyświetl ilość pamięci zajmowaną przez pudelko. cc "Oblekt CBox zajmuje « sizeof (C Box) « " bajty," ; W powyższym kodzie jako operandu operatora s i zeof w nawiasach użyłem nazwy typu, a nie nazwy określonego obiektu. Jak pamiętamy z rozdziału 4., jest to standardowa składnia ope ratora s i zeof. Przykład
ten demonstruje mechanizm uzyskiwania dostępu do publicznych zmiennych skła dowych klasy. Widać w nim także, że zmienne te mogą być używane w taki sam sposób jak zwykłe zmienne. Teraz jesteśmy już gotowi na następny przełom i zajęcie się funkcjami skła dowymi klasy.
funkcie składowe klasy W celu zaobserwowania, w jaki sposób uzyskuje się dostęp do zmiennych składowych klasy z wnętrza funkcji składowych, stworzymy przykładowy program, rozszerzając klasę CBox poprzez dodanie do niej funkcji składowej obliczającej pojemność obiektu CBox. II Cw7_03.cpp
II Obliczanie objętości pudelka za pomocąfunkcji składowej.
#i ncl ude ciost ream>
using std : :cout;
usi ng st d: endl ;
class CBox
II Definicja klasy o globalnym zasięgu.
{
publ i c: doub le m_Lengt h, double m_Widt h; double m_Height ; II Funkcja
II Długość pudełka w centymetrach. II Szerokoś ć pudełka w centymetrach. II Wysokość pudelka w centymetrach.
obliczająca pojemnoś ć pudełka ,
double Vol ume () {
};
i nt mai n() {
CBox boxl ;
II Deklara cja obiektu boxl typu Cbox.
Rozdział7 .•
CBox box2: double boxVol ume
Definiowanie własnych typÓW danych
377
II Deklaracja obiektu box2 typu CBox. ~
O O.
II Przechowuje pojemnosć pudełka.
box1 m_He i ght = 18.0 : box1.m_Length = 78 .0: box1 .m_Widt h = 24.0;
II Definicja wartości
II zmiennych składowych
II obiektu box l .
box2 .m_Height ~ box1.m_Height 10. II Definicja zmiennych skladowych box2 .m_Length = box1.m_Lengt h/2. 0: Il obieklU box2 box2.m_Widt h = O 25*box1.m_Length: II w kategoriach boxl. boxVol ume = box1.Vol ume(), cout « end l «
"Po j emno ść p ude łk a
II Obliczanie
box1 = "
«
objętosci pudelka
boxl.
boxVolume:
cout « endl
cout
"Po je mno ść p ud e łk a
box2
« «
box2.Vol ume();
« « «
endl
"Obiekt CBox zajmuj e
S l zeof box1 « " baj t y
=
"
cout « endl . return O:
Jak to działa Nowy kod dodany do definicji klasy CBox został umieszczony na szarym tle . Stanowi on tylko definicję funkcji Vo lumet), która jest funkcją składową klasy. Ma ona taki sam atrybut dostępu jak zmienne składowe - pub l i c. Jest tak ze względu na fakt, że wszystkie składowe klasy mają taki atrybut dostępu, jaki został przed nimi określony, aż do momentu podania innego atrybutu . Funkcja vol ume() zwraca pojemność obiektu CBox w postaci liczby typu doubl e. Wyrażenie w instrukcji return jest po prostu iloczynem trzech wartości składowych klasy.
Nie ma potrzeby przyporządkowywania nazw zmiennych składowych klasy podczas uzy skiwania dostępu do nich z poziomu funkcji składowych. Nazwy zmiennych składowych występujące bez żadnego kwalifikatora odnoszą się do składowych obiektu, którego funk cjajest aktualnie wykonywana. Funkcja składowa Vo l ume () używana jest we fragmentach kodu znajdujących się na szarym tle w funkcji mai n() po inicjalizacji zmiennych składowych (tak jak w pierwszym przykładzie). Używanie tej samej nazwy zmiennej w funkcji mainr ) nie stanowi żadnego problemu. Aby wywołać funkcję składową określonego obiektu, należy napisać nazwę tego obiektu, po niej kropkę, a następnie nazwę funkcji składowej . Jak wspominałem wcześniej, funkcja automa tycznie uzyskuje dostęp do zmiennych składowych obiektu, dla którego została wywołana. W związku z tym za pierwszym razem, gdy ją wywołujemy, funkcja Volume() oblicza pojem ność obiektu boxl. Używanie tylko nazwy zmiennej składowej zawsze oznacza odniesienie się do zmiennych składowych obiektu, dla którego funkcja składowa została wywołana.
378
Visual C++ 2005. Od podstaw Funkcja składowa została wywołana po raz drugi bezpośrednio wewnątrz instrukcji wyjścio wej, obliczając pojemność pudełka box2. Wykonanie tego przykładu da następujący rezultat:
boxl = 33696 box2 = 6084 Ob iekt CBox zajmuje 24 baj ty . P ojemn o ś ć p u deł k a P Ojemnoś ć p u deł ka
obiekt CBox nadal zajmuje tę samą liczbę bajtów. Dodanie funkcji do klasy nie powoduje zwiększenia rozmiaru obiektów. Oczywiście funkcja ta musi być przechowywana gdzieś w pamięci, ale istnieje tylko jeden jej egzemplarz bez względu na to, ile obiektów klasy zostało utworzonych. Pamięć zajmowana przez funkcję składową nie jest zaliczana do wartości zwracanej przez operator s i zeof, zwracający liczbę bajtów zajmo wanych przez obiekt. Warto
zwrócić uwagę, że
składowej
Nazwy zmiennych składowych klasy w funkcji składowej odnoszą się automatycznie do zmiennych składowych tego obiektu, który został użyty do wywołania tej funkcji . Funkcję składową można wywołać tylko dla określonego obiektu danej klasy. W tym przypadku zo stało to dokonane za pomocą operatora bezpośredniego dostępu do składowej z nazwą obiektu. Jeżeli
w programie znajdzie się wywołanie funkcji składowej bez określenia nazyry obiektu, dla którego jest wywoływana, programu nie będzie można skompilować.
Umieiscowienie definicii funkcii składowei Definicja funkcji składowej nie musi znajdować się wewnątrz definicji klasy . Jeżeli chcemy umieścić ją poza definicją klasy, to w klasie należy zapisać tylko jej prototyp. Nasza po przednia klasa przepisana z definicją funkcji składowej poza nią wygląda następująco:
class CBox
II Definicja klasy w zasięgu globalnym.
(
publ ic: doub le do ub le dou ble dou ble
m_ Length: m_Width: m_Heig ht: Vol ume(void) :
II Dlugość pudelka w centymetrach. II Szerokość pudelka w centymetrach. II Wysokość pudelka w centymetrach. II Prototyp funkcji skladowej.
[:
Teraz pozostaje jeszcze napisanie definicji funkcji. Ze względu na fakt, że znajduje się ona poza klasą, musimy w jakiś sposób poinformować kompilator, że funkcja ta należy do klasy CBox. Dokonujemy tego, stawiając przed nazwą funkcji nazwę klasy i rozdzielając obie nazwy operatorem zasięgu, którym są dwa znajdujące się obok siebie dwukropki (: :). Definicja funk cji wyglądałaby teraz następująco : II Funkcja
obliczająca pojemność
doub le CBox: :Vol ume ( ) { }
pudelka.
Rozdział 7.•
Definiowanie własnych typÓW danych
379
Funkcja ta daje taki sam wynik jak jej poprzednia wersja , choć program nie jest już dokładnie taki sam. W drugim przypadku wszelkie wywołania funkcji traktowane są w sposób już nam znany. Jeśli natomiast definicję funkcji umieścimy wewnątrz definicji klasy, jak na przykład w ćwiczeniu Cw7_03.cpp, kompilator niejawnie potraktuje jąjako funkcję inline.
fllDkcie inline Przy zastosowaniu funkcji inline kompilator próbuje rozwinąć kod ciała funkcji w miejscu jej W ten sposób unikamy strat spowodowanych wywoływaniem funkcji, co sprawia , że program staje się szybszy. Zostało to zilustrowane na rysunku 7.5.
wywołania.
int main(void)
Funkcja zadeklarowana jako inline w klasie
Rysunek 7.5
inline void functionO {body}
~
Kornpilator w rniejsca pojawien ia się wywołań funk cji __ inline wstaw ia kod ich ciała . odpowiedn io dostosowany w celu un ikn ięcia problemów z nazwamii zasięg iem zmiennych
{body}
~1Ii
~{bOdY} Oczywi ście
kompilator sprawdza również, czy takie rozwinięcie funkcji nie spowoduje nazwami oraz zasięgiem zmiennych.
żadnych problemów z
Kompilator niekiedy może sobie nie poradzić z przeprowadzeniem operacji rozwijania kodu (np. w przypadku funkcji rekurencyjnych, dla których pozyskaliśmy adres) , ale z reguły nie ma żadnych problemów. Technikę tę najlepiej stosować z krótkimi, prostymi funkcjami, takimi jak nasza funkcja Vol ume( ) w klasie CBox, ponieważ funkcje takie wykonywane są znacznie szybciej, a wstawienie ich kodu nie wpływa za bardzo na rozmiar modułu wykonywalnego. Jeżeli
definicja danej funkcji znajduje się poza definicją klasy, kompilator traktuje ją jak i jej wywoływanie odbywa się normalnie. Jeśli jednak chcemy, to możemy sprawić, aby kompilator w miarę możliwości traktował także taką funkcję jako funkcję inline. Aby to zrobić, na początku nagłówka funkcji stawiamy słowo kluczowe i n'l i ne. W związku z tym definicja naszej funkcji wyglądałaby następująco: zwykłą funkcję
Przy zastosowaniu takiej definicji funkcji program byłby dokładnie taki sam jak w wersji oryginalnej. W ten sposób definicje funkcji możemy wyrzucić poza definicje klas i zachować korzyści płynące z prędkości wykonywania dzięki zastosowaniu funkcji inline.
380
Visual C++ 2005. Od podstaw Ten sam efekt uzyskamy, stawiając słowo kluczowe i nl i ne nawet przed zwykłymi funkcjami w programie, niemającymi nic wspólnego z klasami . Najeży jednak pamiętać, że technika ta jest najbardziej przydatna w przypadku krótkich i prostych funkcji . Musimy teraz obiektu klasy .
dowiedzieć się trochę więcej
na temat tego , co
się
dzieje podczas deklaracji
Konstruktory klas W poprzednim przykładowym programie zadeklarowaliśmy dwa obiekty klasy CBox o na zwach boxl i box2, a następnie każdej ich zmiennej skład owej przypisaliśmy po kolei warto ści początkowe . Podejście takie nie jest s atysfakcjonujące z kilku pow odów . Po pierwsze , łatwo w taki sposób pominąć przy inicjalizacji j edną ze zmiennych składowy ch, w szczególności gdy mamy do czynienia z większymi od naszej klasami. Inicjalizacja zmiennych s kłado wych kilku obiektów złożonej klasy mogłaby zaj ąć kilka stron wierszy instrukcji przypis ania. Ostatni powód to fakt, że definiując składowe z atrybutem dostępu innym niż publ i c, pozbawiamy się dostępu do nich spoza klasy . Musi być na to jakiś lepszy sposób - i jest. Nazywa się on konstruktorem klasy.
Czym lest konstruktor Konstruktor klasy to specj alna funkcja w klasie , która jest wywoływan a podc zas tworzenia nowego obiektu klasy . Umożliwia on ini cjalizację obiektów podczas ich tworzenia oraz za pewnia , że zmienne składowe zaw i eraj ą wyłącznie prawidłowe dane . Klasa może mieć kilka konstruktorów pozwalających tworzyć obiekty na różne sposoby. Przy nadawaniu nazw konstruktorom klas nie mamy żadnego pola manewru - zawsze nazy tak samo jak klasa , w której zostały zdefiniowane. Na przykład funk cja CBox() jest konstruktorem klasy CSox. Konstruktor nie posiada typu zwracanego. Nadanie konstruktorowi typu zwracanego j est błędem, nawet jeżeli określimy go jako voi d. Gł ównym przeznaczeniem konstruktorów jest nadawanie wartości początkowych zmiennym składowym klasy, a więc typ zwracany nie jest ani potrzebny, ani dozwolony. wają się
~ Dodawanie konstruktora do klasy CBOK Rozszerzymy
n aszą klasę
Cbox o konstruktor.
II Cw7_04.cpp
II Zastosow anie konstrukt ora.
#inc l ude
using st d: :cout:
using st d: :endl :
class CBox {
II Definicja klasy w zas ięgu globalnym.
Rozdzial1.• Oetiniowanie własnych typów danych publ ic double m_Length; double m_Width; double m_Height ;
381
II D/ugość p udelka w centymetrach. II Szerokoś ć pudel/w w centymetrach. II Wysoko ść pudelka w centymetrach.
II Defin icja konstruktora.
CBox(do uble l v. doub le bv. double hv) (
cout « endl « "Konst rukt or m_Length = l v: m_Wi dth = bv; m_He ight = hv:
zo s t a ł wywo ł a ny . " ;
II Ustawianie wartoś ci
II zmien ny ch s kładowych.
II Funk cj a obliczająca pojemn ość pudelka.
doub le Vol ume() ( )
};
i nt mai n() CBox boxl(78.0.24.0.18.0) ; CBox cigarBox(8.0.5 .0.1.0);
II Deklaracj a i inicjalizacj a obiektu boxl . II Dek/aracj a i inicja liza cj a obiektu cigarBox .
double boxVo l ume = 0.0 ;
II Przechowuj e pojemność pudelka.
boxVol ume ~ boxl .Volunet) : cout « end l
II Obliczanie obję toś c i p udelka boxl .
cout
«
"Pojemn o ść pu d e łka
« « «
end l
" P o j emn o ś ć p u d e ł ka
boxl
=
"
«
boxVolume:
ci garBox - "
ci garBox.Vol ume( ) ;
cout « endl ;
ret urn O;
Jak to działa Konstruktorowi CBox() przekazane zostały trzy parametry typu doubl e, odpow i adaj ąc e warto ściom początkowym zmiennych składowych mL enqt h, m_Width oraz m_Hei ght obiektu klasy CBox. Pierwsza instruk cja konstruktora wyświetla komunikat informujący, że nastąpiło wy wołanie konstruktora. W programie przezn aczonym do użytku nie umieś cilibyśmy takiej instrukcji, ale jest to często stosowana praktyka podczas fazy testowej programu, g dyż pozwala zor i ento w ać si ę, w którym momencie zost ał wywołany konstruktor. Będę jej często używał w celach testowych . Kod w ciele konstruktora jest bardzo prosty. Przyp isuj e on argumenty przekazane do niego podczas wywoływani a do odpowiednich zmiennych składowych. W razie potrzeby możn a d odać jeszcze sprawdzanie popr awności argum entów i tego, czy nie mają wartości ujemnych. Tworząc prawdziwy program użytkowy, pewnie byśmy to zrobili, ale tutaj naszym głównym celem jest przyjrzenie s i ę sposobowi działania całego mechanizmu.
382
Visual C++ 2005.011 podstaw W obrębie funkcji mai n() zadeklarowaliśmy obiekt boxl, podając wartości początkowe dla zmiennych składowych w kolejności m_Length, m_Width oraz m_Hei ght. Znajdują się one w na wiasach po nazwie obiektu. Do tej inicjalizacji zastosowaliśmy notację funkcjonalną, która j ak już widzieliśmy w rozdziale 2. - może być również z powodzeniem stosowana do inicja lizacji zwykłych zmiennych typów podstawowych. Zadeklarowaliśmy także jeszcze jeden obiekt klasy CBox o nazwie cigar Box, który również ma wartości początkowe . Poj emność pudełka boxl liczona jest podobnie jak w poprzednim przykładzie za pomocą funkcji Vo l ume (), a wynik wysyłany na wyj ś ci e. Pojemność pud ełka cigar Box również zostaje wyświetlona. Rezultat działania programu jest następujący:
Konstrukt or Konst ruktor
z o st a ł wywoł a ny .
zo st ał wywoł a ny .
Poj emn o ś ć pu d e ł k a Poj emno ś ć pu d e ł k a
box1 = 33696
cigarBox = 40
Pierwsze dwa wiersze to inform acja o dwukrotnym wywołaniu konstruktora CBox() , jeden raz dla każdego zadeklarowanego obiektu. Dostarczony przez nas w definicji klasy konstruktor wywoływany jest automatycznie podczas deklaracji obiektu klasy CBox, a więc oba obiekty tej klasy inicjalizowane są wartościami p oczątkowymi podanymi w deklaracji. Wartości te zostały przekazane do konstruktora w postaci argumentów w tak iej samej kolejności , w j akiej zo stały napisane w deklaracji. Jak widać , poj emność pudełka boxl jest taka sama jak wcze śniej, a pojemnoś ć pudełka ci garBox wygląda podejrzanie, jakby była iloczynem jego boków i całe szczęście .
Konstruktor domyślny Dodajmy do naszego ostatniego programu
deklarację
obiektu box2, której
używali śmy
wcze
ś n i ej:
CBox box2 :
II Deklaracja obiektu box2 typu CBox.
Obiekt box2 w powyższym kodzie pozostawiliśmy bez wartości początkowych . Kiedy spró bujemy skompilować program z tą instrukcją, to otrzymamy następujący komunikat o błędzie :
error C2512: 'CBox' : no appropri at e default const ructor avail able Oznacza to, że kompilator poszukuje konstruktora domyślnego (czasami nazywanego kon struktorem bezargumentowym, ponieważ nie wymaga żadnych argumentów podczas wywc> ływania) dla obiektu box2, gdyż nie podaliśmy żadnych wartośc i początkowych dla zmiennych składowych. Konstruktorowi domyślnemu nie trzeba przekazywać żadnych argum entów. Będzie on wtedy konstruktorem nieposiadającym żadnych parametrów w definicji lub kon struktorem, którego wszystkie argumenty są opcjonalne. Ale przeci eż instrukcja ta była cał kowicie poprawna w programie Cw7_02.cpp. Czemu więc niejest teraz? W poprzednim przykładzie został u żyty konstruktor domy ślny dostarczony przez kompila tor ze względu na fakt , że my nie podali śmy żadnego konstruktora. Jako że w tym programie podaliśmy już konstruktor, kompilator uznał, że postanowiliśmy zatroszczyć się o wszystko sami i nie podal i śmy konstruktora domyślnego. W związku z tym , jeżeli nadal chcemy tworzyć deklaracje obiektów klasy Cbox, nie podając wartości początkowych, to musimy na własną
Rozdział 7.•
Definiowanie wlasnych lypÓw danych
383
rękę dołączyć konstruktor domyślny. Jak dokładnie wygląda konstruktor domyślny? W naj prostszym przypadku jest to po prostu konstruktor nieprzyjmujący żadnych argumentów nie musi on nawet nic robić :
CBox()
II Konstruktor domyślny.
II Calkowity brak instrukcji.
{}
Przyjrzyjmy
się
takiemu konstruktorowi w akcji.
EIIlmIIiI Dostarczanie konstruktora Ilomyślnego Do ostatniej wersji naszego programu dodamy nasz domyślny konstruktor, deklarację obiektu box2 oraz oryginalne przypisania dla zmiennych składowych obiektu box2. Musimy trochę rozbudować nasz konstruktor domyślny, aby było wiadomo, kiedy został wywołany . Poniżej znajduje się zmodyfikowana wersja programu: IICw7_05.cpp
VislJal C++ 2005. Oli pOlJslaw CBox box1(78.0.24.0.18. 0) : CBox box2: CBox cigarBox(8.0. 5.0. 1.0) :
II Deklara cja i inicj alizacj a obiektu box1. II Deklaracja obiektu box2 bez wartoś c i po czątkowej. II Deklaracj a i inicjalizacja obiektu cigarBox.
doub le boxVolume = 0.0:
II Przechowuj e pojemnoś ć pu de łka.
boxVol ume = box1 .Vo lume() ; cout « end l
II Obliczanie objętoś ci pudelka boxl.
·P o j em no ~ć p u d e ł k a
«
box 1 =
«
boxVolume:
box2 .m_Height = boxl. m_Height - 10: II Definicj a składowych box2.m_Length = boxl. m_Length / 2. O; Il obiektu box2 box2 .m_Width = O.25*box1.m_Lengt h: II w katego riach obiektu box l . cout
« « «
cout
«
endl . Poj emnoś ć pudełk a box2 = box2.Vo lume() : end l « «
· P oj emn o ~ ć p u de ł k a
cigarBox
=
cigarBox.Vol ume():
cout « sndl: ret urn O:
Jak to działa Teraz, gdy dos tarc zyli śmy własny konstruktor d omyślny, podczas kompil acji nie zos tały zgło szone żadne komunikaty o błędach i wszy stko d zi ała jak n ale ży. Wynik d zi ałania programu jes t następuj ąc y :
Konst ruktor Konstruktor Konstru ktor
z o st a ł wywo ł a ny . domyś lny z o st ał wywo ła ny. z os ta ł wywo ła ny .
Poj emn o ~ ć p u de ł k a
Poj em n o ~ ć p u d e ł k a Po jemno~ć p u d e ł k a
box1 = 33696 box2 ~ 6084 cigarBox = 40
J edyną c zynn o ś cią wykon ywaną
przez kon struktor d om y ślny j est wy św i etl eni e komunio katu. Jak w i d ać , zo st ał on w y w oł any w momencie zadeklarowania ob iektu box2. Objęto ś c i wszystkich trzech obiektów klasy CBox s ą poprawne, co oznacza , że reszt a programu d zi ała bez zarzutów. Na przykładzie tego programu dowiedziel i śmy s ię, że konstruktory można p rzeciążać podob nie j ak funk cje (patrz: rozd z iał 6.) Zd efiniow ali śmy przed c h w i lą dwa konstrukt ory, które różn iły s ię tylko li stą parametrów. Jeden z nich ma trzy param etry typu doubl e, a drugi nie ma żad nyc h parametrów.
Rozdział 7••
Definiowanie własnych typÓW lianych
385
Przypisywanie domyślnych wartości
parametrom umieszczonym wklasach
Kiedy opisywałem funkcje, pokazałem sposób podawania wartości domyślnych dla parame trów funkcji w jej prototypie. To samo możemy zrobi ć ze składowymi klasy, włącznie z kon struktorami. Jeżeli definicję funkcji składowej um ieścimy wewnątrz definicj i klasy, to wartośc i domyślne jej parametrów m o żemy podać w na gł ówku funkcji . Jeżeli natomiast w defini cji klasy podamy tylko prototyp, to domyślne wartości parametrów powinny zostać umieszc zone w protot ypie. Gdyby śmy nicję
chcieli, aby domyślni e każdy obiekt CBox miał wszystkie boki długości l , to defi klasy w ostatnim przykładzie zmienilibyśmy w na stępujący sposób:
cl ass CBox II Definicja klasy o zas ięgu globalnym.
{ publ tc : doubl e m_Lengt h : double m Widt h : double m He ight : II Definicja konstrukt ora. CBox( double l v ~ 1. 0 . double bv .
~
1.0 . doubl e hv
~
1. 0)
{ cout « endl « m_Lengt h ~ l v : m_Wi dt h = bv: m_He ight ~ hv :
"Konstr ukt or
zo s t ał
wywoł any . " : II Usta wianie wartości Ilzmienny ch składowyc h.
} II Defin icja konstruktora domyś ln ego. CBox ()
{ cout «
endl «
" Konst ru kt or
domyś l n y z os t a ł wywo ł a ny
} II Funkcja o bliczająca pojemność pudełka . doubl e Vol ume()
{
}:
Co
się
zgło si
stanie, jeżeli dokonamy tej zmiany w ostatnim przykładzie? Oczywi ści e kompilator inny komunikat o błędzie. Mi ędzy innymi także ten , który widać poniżej :
wa r ning C4520' ' CBox' : mu lti pl e defaul t const r uct or s specifi ed er r or C2668: ' CBox: :CBox ' · emoi quous cal l t o over loa ded func t i on
Oznacza to, że kompilator nie wie, który z tych dwóch konstruktorów powinien być wywo łany - ten, dla którego parametrów podaliśmy zbiór warto ści domyślnych, czy ten, który nie przyjmuje żadnych parametrów. Dzieje się tak, gdyż deklaracja obiektu box2 wymaga kon struktora bez parametrów, a teraz każdy z konstruktorów może zostać wywołany bez para metrów. Rozwiązaniem , które natychm iast się nasuwa, jest pozbycie się konstruktora nie przyjmującego żadnych parametrów. W rzeczywistości będzie to korzystne. Po usunięciu tego konstruktora składowe każdego obiektu klasy CBox zadeklarowanego bez podawania żadnych wartości początkowych zostaną automatycznie zainicjalizowane wartością I.
386
Visual C++ 2005. Od pOlIslaw
~ Dostarczanie wartości Ilomyślnych lila argumentów konstruktora Spójrzmy teraz na
przykładow y
prograin
ilu struj ący powyższe
zagadnienie :
II Cw7_D6.cpp
II Dostarczanie wartości domyślnych dla argume ntów konstruktora.
#i ncl ude usi ng st d: :cout ; usi ng std: :endl; class CBox
II Definicja klasy w
zas ięgzi
globalnym.
{
pu bl i c: doub le m_L engt h: doub le mWidth: doub le m_Hei ght :
II Dł ugosć pudelka w centymetrach. II Sze rokość pudelka w centy metrach. II Wys okoś ć p udelka w centymetrach.
II Defin icj a konstruktora.
CBox(double l v = 1.0. doub le bv = 1.0. double hv = 1.0) (
cout « end l « "Konst rukt or m_Length = l v: m_Widt h = bv; m_He ight = hv; obliczająca pojemnos ć
II Funkcja
zosta ł wywo ł any .
";
II Ustawianie wartoś ci II zmiennych składo wyc h.
pudelka.
do ub le Vol ume() { } };
i nt mai n() (
CBox box2:
II Deklaracja obiektu box2 - brak wartosci
domyślnej.
cout « endl « «
"Poj em no ś ć p ude ł k a
box2
=
box2 .Vo l ume () :
cout « end l ; ret urn O:
Jak lo działa Zdefiniowal i śmy tylko jedną, niezainicjalizowaną zmienną klasy CBox - box2, ponieważ to wystarczy nam do naszych celów demonstracyjnych. Program w tej wersji daje na stępujący rezultat:
Konst rukt or
zo s t a ł wywo łany.
Po j em n o ś ć pu d eł ka
box2
~
l
Rozdział 7••
Z tego wynika, wiając wartości
Definiowanie własnych typÓW danych
że
konstruktor z domyślnymi wartościami parametrów obiektów, które nie mają wartości początkowych.
działa
387
poprawnie, usta
Nie oznacza to jednak, że powyższy przykład jest jedynym, a nawet zalecanym sposobem implementacji konstruktora domy ślnego, Wielokrotnie zdarzy się, że nie będziemy chcieli przypisywać wartości domyślnych w taki sposób - wtedy będzie trzeba napisać oddzielny konstruktor domyślny . Może się nawet zdarzyć , że nie będziemy chcieli, aby w ogóle działał jakikolwiek konstruktor domyślny, mimo że mamy zdefiniowany inny konstruktor. Takie po dejście zapewnia, że wszystkie zadeklarowane obiekty klasy muszą mieć wartości jawnie okre ślone w ich deklaracji.
Używanie listJ inicjalizacJjnej wkonstruktorze W dotychczasowych przykładach zmienne składowe obiektów klasy inicjalizowaliśmyza przypisania. Istnieje także inna technika, której można użyć do tego celu lista inicjalizacyjna konstruktora. Sposób jej użycia zademonstruję na zmodyfikowanej wersji konstruktora klasy CBox: pomocąjawnego
II Defini cja konstruktora z użyciem listy inicjaliza cyjnej .
Sposób, w jaki ta definicja konstruktora została zapisana, sugeruje, że powinna ona znajdo w obrębie definicji klasy. W tym przypadku wartości zmiennych składowych nie są ustawiane za pomocą instrukcji przypisania w ciele konstruktora. Podobnie jak w deklaracji, są one określone jako wartości początkowe za pomocą notacji funkcjonalnej oraz występują w liście inicjalizacyjnej jako część nagłówka funkcji. Na przykład zmienna składowa m_Length jest inicjalizowana wartością l v. Ten sposób może być o wiele bardziej wydajny niż przypi sania, które stosowaliśmy wcześniej . Jeżeli tę wersję konstruktora wstawimy w miejsce kon struktora w poprzednim programie, to stwierdzimy, że działa ona tak samo . wać się
Należy zauważyć , że
lista inicjalizacyjna oddzielona jest od listy parametrów dwukropkiem, a poszczególne elementy listy inicjalizacyjnej rozdzielane są przecinkami. Ta technika ini cjalizowania parametrów w konstruktorze jest bardzo ważna, gdyż - jak się później prze konamy - stanowi jedyny sposób ustawiania wartości niektórych typów składowych obiektu . Technika z użyciem listy inicjalizacyjnej jest też bardzo często stosowana w bibliotece MFC.
Prywatne składowe klasy Posiadanie konstruktora ustawiającego wartości składowe obiektu klasy przy jednoczesnym zezwoleniu dowolnej części programu na dostęp do wnętrza obiektu zakrawa na absurd. To tak jakby znaleźć genialnego chirurga, jak dr Zbój, który swoje umiejętności szlifował przez
388
Visual C++ 2005. Od podstaw wiele lat, do zrobienia porządku w naszym brzuchu, a potem pozwolić tam zajrzeć lokalnemu hydraulikowi albo murarzowi. Bez wątpienia potrzebujemy jakiejś ochrony dla naszych skł a dowych klasy. Ochronę taką możemy sobie zapewnić, stosując słowo kluczowe private w definicji zmien nych składowych klasy. Do zmiennych składowych zadeklarowanych jako prywatne (pri vate) z reguły dostęp mają tylko funkcje składowe tej samej klasy . Jest jeden wyjątek od tej reguły, ale tym zajmiemy się później . Zwykła funkcja nie ma możliwości bezpośredniego dostępu do zmiennej składowej klasy. Pokazano to na rysunku 7.6.
Rysunek 1.&
Obiekt klasy
OK
public Zmienne składowe ~
I-
~
s
"<
o
o
:'l
'"C-
1J
i:
~: ~
OK
I
public Funkcje składowe
Zwykła
funkcja nienależąca do klasy
;:
I--
r
0
~
"'tro" o
~
"<
Błąd
I
Błądbraku~ dostępu
~
private Zmienne składowe
I-
f+--
~
,,'zr 3
;o' j j
"<,
~ Błąd
private Funkcje składowe
r-
e---
~
"'" ~
;: oo
" "'" e
Możliwość określenia składowych klasy jako prywatnych pozwala na oddzielenie interfejsu klasy od jej wewnętrznej implementacji. Na interfejs klasy w szczególności składają się pu bliczne funkcje składowe klasy, ponieważ umożliwiają one uzyskanie bezpośredniego dostępu do zmiennych składowych klasy, włącznie z tymi , które zostały określone jako prywatne. Mając wnętrze klasy określone słowem kluczowym pr i va te, możemy dokonywać później szych poprawek składowych choćby w celu zwiększenia ich wydajności , bez potrzeby doko nywania zmian w kodzie, który korzysta z tej klasy poprzez interfejs publiczny. Aby zabez pieczyć funkcje i zmienne składowe klasy przed niepowołanym dostępem, dobrą praktykąjest zadeklarowanie tych, które chcemy chronić, jako prywatne. Słowem kluczowym publ ic okre ślamy tylko to, co jest niezbędne do korzystania z klasy.
pri vat e: dou ble m_Lengt h: double m_Widt h: double m_Height:
II Długos ć p udelka w centymetra ch. II Szerokoś ć pudelka w centym etrach. II Wysokość pudelka w centymetrach.
}:
i nt mai n( ) {
CBox mat ch(2.2 , 1.1, 0.5) : CBox box2 : cout
« « «
II Deklaracj a obiektu match.
II Deklaracj a obiektu box2
II - brak wartośc i początko wych .
endl
" Pojemn o ś ć p ud e ł ka
mat ch
=
match.Volume():
II Us unięcie komentarza z poniższego wiersza spowoduje II box2.m_Length = 4.0;
bląd.
CDUt « end l
« «
" Pojemnoś ć p ude łk a
box2 = "
box2.Vol ume():
cout « endl :
ret urn O:
Jak to działa Definicja klasy CBox została podzielona na dwie c zę śc i . Pierwsza z nich to częś ć publiczna (pub l i c), która zawiera konstruktor oraz funkcję składową Vol ume( ). Druga natomiast została okreś lo n a jako prywatna (pr ivate ) i zawiera zmienne s kład owe klasy. Od tej pory do zmien nych tych dostęp mają wyłączn ie funkcje składowe tej klasy. Nie ma potrzeby modyfikować żadnej z funkcji skł adowych - cały czas mają one dostęp do zmiennych skład owych . J eżel i usuniemy komentarz w funkcji mai n( ) z wiersza zawi eraj ącego in strukcję przypisującą wartość
390
Visual C++ 2005. Od podstaw m_Length należącej do obiektu box2, to otrzym am y komunikat o błęd zi e ta jest rzeczywiście ni edo stępna . Spójrz na li stę s kład owych klasy CBox w panelu Class View, o ile nie przy s zło Ci to do głowy wcześniej . Zob aczysz, że obok każdej składowej znajduje się mała ikonka i n formuj ąca o stopniu jej d ostępności. Kłódka oznac za, że s kładowa jest prywatna.
zmiennej
s kład owej
potwierdzaj ący, że składowa
Należy zapam ięta ć, że
w tej chwili prywatnym zmiennym skladowy m obiektu
można
nadawać wartoś ci tylko za pomocą konstruktora lub f unkcji składowej. Nale ży upewnić
s ię, że j edynym
sposob em ustawiania lub modyfikowania wartości zmiennych składowych klasy oznaczonych jako p rywatnejest poprzez funkcje składo we.
Funkcje możn a również um ies zczać w pryw atnej sekcj i klasy. Funkcję taką można wywoła ć tylko poprzez inną funkcję składową tej samej klasy. Umieszczenie funkcji Vo l ume( l w sekcji pr i vate spowoduj e zgło szen ie przez kompilator błędu od instrukcji, które z niej korzystały w funkcji ma i n( l . Umieszczenie konstruktora w sekcji pryw atnej zablokuj e możliwość dekla rowania obiektów tej klasy. Powyższy
program da
nast ępuj ący
rezultat:
Konstr uktor zast al wywolany.
Konstr uktor zastal wywalany .
Poj emn o ś ć pudelka matc h ~ 1.21
P oje mn o ś ć pudelka box2 ~ 1
wynika, że klasa nadal działa tak, jak powinna, mimo ż e jej zmienne zadekl arowane j ako pryw atne. Główna różnica polega na tym, że są one teraz chronione przed nieautoryzowanym dostępem i modyfikacją.
Z danych na
wyjściu
składowe zo stały
Jeżeli
nie określimy inaczej , domyślnym atrybutem dostępu do składo wych klasy j est atry but pri vate. Możemy zatem umieś cić wszystkie składowe, które mają być prywatne, na samym początku definicji klasy i pozwolić, aby zostal im nadany domyślny atrybut dostępu. Lepiej j est j ednak podjąć ten wysiłek i jawnie określić rodzaj dostępu, co pozwoli na un iknięci e wszelkich wątpliwości co do naszych zamiarów.
Oczywiście, nie musimy okre ślać wszystkich zmiennych składowych jako prywatne . J eżeli wymaga tego funkcja pełniona przez naszą klasę, niektóre jej składowe możemy określić jako prywatne, a niektóre jako publiczne. Wszystko zależy od tego, co chcemy zrobi ć . Jeżeli niema żad ne go powodu , dla którego składowe klasy powinny być publiczne, to lepiej nadać im atry but pri vat e jako dodatkowy ś rod e k bezpieczeństwa. Zwykłe funkcje nie będą miały prawa dostępu do żad nyc h prywatnych zmiennych składowych klasy .
UZYSkiwanie dostępu do prywatnych zmiennych składOWYCh klasy Po głębszym zastanowieniu deklarowanie s kła dowych klasy jako pr i vat e jest raczej ekstre malnym podejściem. Bardzo dobrze, że chcemy je chroni ć przed niechcianą modyfikacją, ale nie ma potrzeb y trzymania ich w tajemnicy. Potrzebujemy więc ustawy o wolnym dostępie do informacji dla s kładowych prywatnych.
Rozdział 7.•
Deliniowanie własnych typÓW danych
391
Nie musimy jednak biec z tym do sejmu - takie coś już istnieje. Jedyne, co musimy zrobić, to napisać funkcj ę s kładową zwracającą wartość zmiennej składowej . Spójrzmy na poniższą funkcję składową klasy CBox:
i nll ne double CBox: :Get Lengt h() (
ret urn m_Length: W celu zademonstrow ania sposobu działan ia funkcję tę napisałem jako funkcję składową znajpoza kl asą. Określiłem jąjako funkcję inline, gdyż zyskamy na prędkości działania , bez zbytniego zwi ększenia ilości kodu. Zakł adając, że definicja funkcji znajduje się w sekcji publ i c klasy, moż emy jej używać w instrukcjach podobnych do poniższej:
dującą się
i nt len = box2 .GetLengt h() :
II Spra wdź
dlugoś ć
zmiennej skladowej.
Jedyne, co musimy zrobić , to napi sać podobną funkcj ę dla każdej zmiennej składowej klasy, którą chcemy udostępnić na zewnątrz . Dostęp do ich warto śc i nie będzie wtedy ujemnie wpły wał na bezp ieczeństwo klasy. Oczywiści e, jeżeli definicje tych funkcji wstawimy do definicji klasy, to domyślnie staną się one funkcjami wstawianymi.
Przyjaciele klasy Czasami może się zdarzyć, że z jaki egoś powodu chcemy, aby pewne wybrane funkcje, które nie są składowymi danej klasy, miały mimo wszystko dostęp do wszystkich jej składowych chcemy określić coś w rodzaju elitarnej grupy funkcji. Funkcje takie nazywają się funkcjami zaprzyjaźnionymi klasy, a definiuje się je za pomocą słowa kluczowego fr i end. W definicji klasy można umieśc ić tylko prototyp takiej funkcji lub jej całą definicję. Funkcje zaprzyjaź nione klasy, które są zdefiniowane wewnątrz klasy, automatycznie są także funkcjami inline.
Funkcje zaprzyjaźn ione nie są składowymi klasy, atrybuty dostępu nie mają na nie bowiem wpływu . Są one zwykłymi funkcjami globalnymi, które posiadają specjalne przywileje. Przypuśćmy , że powierzchnię
chcemy zaimplementować obiektu tej klasy.
funkcję zaprzyjaźnioną
klasy CBox,
obliczającą
~ Obliczanie powierzchni za pllmocą funkcji zaprzYiaźniOnei Sposób dzi ałania funkcji gramie:
zaprzyjaźnionej prześledzimy
na
poniższym przykładowym
II Cw7_oS.cpp II Tworzenie fu nkcj i zaprzyjaźn ionej klasy.
#include usi ng std : :cout : using st d: :endl : class CBox {
publ i c:
II Definicj a klasy w zas ięgu glo balnym .
pro-
392
Visual C++ 2005. Od podstaw II Definicja konstruktora.
II Deklaracja obiektu match. II Deklaracja obiektu box2 - brak
endl
« « «
matc h.Vol ume();
« « «
endl "Pole powierzchni p udełka match BoxSurface(match);
« « «
end l
« «
endl "Pole powierzchni BoxSurface(box2);
«
"Pojem ność p udełka
"Pojemność pudełka
match
box2
~
~
box2.Volume();
cout « endl ; return O;
pudełka
box2
~
"
wartości domyślnych.
Rozdział 7.•
Oeliniowanie własnych typÓW danych
393
Jak to działa Zadekl arowaliśmy funk cję
BoxSurface( ) jako zaprzyjaźnionąklasy CBox, pisząc jej prototyp poprzedzony słowem kluczowym fr ie nd. Jako że funkcja BoxSurface ( ) je st funkcją globalną, nie ma znaczenia, w którym miej scu definicji klasy um ieścimy dekl arację f rie nd , ale dobrze jest trzymać się zawsze tego samego miejsca, aby zachować jednolito ść. Jak widać , ja naszą definicję umieściłem po wszystkich s kład ow yc h publ i c i pri vat e klasy. Należy pamiętać, że funkcja zap rzyjaź n i o n a nie jest s kład ow ą klasy, a więc atrybuty dostępu nie m ają na nią wpływu .
Po definicji klasy następuje definicja funkcji . Zauwa ż, że atrybut dostępu do składowych obiektu określony został wewnątrz definicji funkcji BoxSurface() przy użyciu obi ektu klasy CBox jako parametru przekazanego do funkcji. Jako że funkcja zaprzyjaźniona nie jest skła dową klasy, do s kład owyc h nie możemy odno sić s i ę tylko za pomocą ich nazw. Przed każdą z nich musi znajdować się kwalifikator w po staci nazwy obiektu, dokładn ie tak samo jak w przypadku zwykłych funkcji, oczywiście z tym wyjątkiem, że zwykłe funkcje nie mają dostępu do prywatnych składowych klasy. Funkcja zaprzyjaźniona różni się od zwykłej funkcji tylko tym , że ma nieograniczony dostęp do wszystkich składowych klasy lub klas, z którymi j est zaprzyj aźniona. Wynik
działania tego
Konst rukt or Konst rukto r
programu jest
następujący:
zo st a ł wywo ł a ny .
z o s t a ł wywoła ny.
matc h = 1.21 mat ch = 8.14 P o j emn o ś ć pu de ł k a box2 = l Po le powierzchni pude ł k a box2 = 6 Poj emn o ść pu d e ł k a
Pole powie rzchni
pu deł k a
Wynik jest dokładnie taki , jakiego s ię spo d zi ewa l i ś my . Funkcja zaprzyjaźniona oblicza pole powierzchni obiektów klasy CBox z wartości prywatnych zmiennych składowych.
Wstawianie definicji lunkcjl zaprzyiaźnionych do klasy Definicję
funkcji
CBox, a kod
zaprzyjaźnionej wraz
działałby
zjej deklaracj ą mogliśmy umieścić w definicji klasy tak samo jak wcześniej. Definicja tej funkcj i w klasie wyg lądałaby na-
stępująco:
fri end double BoxSurface(CBox aBoxl {
return 2.0*(aBox .m_Lengt h*aBox .m_Width + aBox .m_Length*aBox .m_He ight + aBox .m_Helght*aBox .m_Wi dth); Jednak w ten sposób tracimy nieco na czytelności kodu. Mimo że funkcja nadal ma zasięg globalny, to nie jest to wcale takie oczyw iste dla osoby czytającej ten kod , ponieważ funkcja mogłaby być schowana w ciele definicji klasy.
394
VisualC++ 2005. Od podstaw
Domyślny
konstruktor kOIJiuiący
P rzypu ś ćmy , że zadek larowa liś m y
pon iższej
i za i n i cj a li zo wa l i ś my obiekt boxl klasy CBox za pomo cą
instrukcji :
CBox box1(78.0. 24 .0. 18.0) ; Chcemy tera z u tw orzy ć jeszcze j eden ob iekt tej samej klasy , który będzi e identy czny jak ten pierws zy. W takim przypadku drugi obiekt klasy zainicjalizowalibyśmy obiektem boxl. Sprawdźm y to:
~ Kopiowanie informacji pomiędzy P oniżs zy
program pokazuje
egzemplarzami
tę c zynność:
II Cw7_09.cpp Il lnicj alizowanie obiektu obiektem tej samej klasy.
#i ncl ude using std : :cout : using std : :endl : class CBox
II Defini cja klasy o zasięgu globalnym.
{
publ ic: II Definicja konstrukt ora.
CBox(double l v
~
1.0 . do uble bv
~
1.0. double hv = 1.0)
{
cout « endl « "Konstrukt or m_Length = lv: m_Width = bv ; m_ Hei ght ~ hv; II Funkcja
zos ta ł wyw oł a ny ." :
II Ustawianie wartoś ci
II zmiennych skladowy ch.
ob licz ająca poje mność pudelka.
double Vol ume() {
private : dou ble m_Length: doub l e m_Wldth; double m_Height ;
II Długos ć p udelka w centy metrach. II Szerokoś ć pudelka w centymetrach. II Wysokoś ć pudelka w centymetrach.
Po j emn o ś ć pu de ł k a Po j em n o ś ć p u de ł ka
boxl = 33696 box2 = 33696
Jak to działa Jasne jest, że program działa tak, jak sobie tego życzymy - oba pudełka mają te same wymiary. Jak jednak wynika z danych na wyjściu, konstruktor został wywołany tylko raz w celu utworzenia obiektu boxl. Pytanie brzmi: .jak został utworzony obiekt box2?". Mechanizm jego utworzenia podobny jest do mechanizmu, który został zastosowany, gdy nie zdefiniowali śmy żadnego konstruktora i kompilator dostarczył domyślny w celu utworzenia obiektu. W tym przypadku kompilator utworzył domy ślną wersję czegoś, co nazywa się konstruktorem kopiującym.
Konstruktor kopiujący wykonuje dokładnie tę samą czynność , którą my robimy tutaj - tworzy obiekt klasy i inicjalizuje go już istniejącym obiektem tej samej klasy. Konstruktor kopiujący w wersji domyślnej tworzy nowy obiekt, kopiując ten już istniejący pole po polu. Sposób ten sprawdza s i ę w przypadku prostych klas, takich jak nasza CBox, ale gdy mamy do czynienia z wieloma klasami zawierającymi wskaźniki lub tablice jako składowe, to metoda ta nie będzie spełniała naszych oczekiwań . Tak naprawdę w przypadku takich klas domyślny operator kopiowania może spowodować wystąpienie poważnych błędów w programie. W tej sytuacji musimy sami stworzyć własny konstruktor kopiujący klasy . Czynność ta wymaga zastosowania specjalnego podejścia, którym będziemy zajmować się do końca tego rozdziału i jeszcze w następnym.
Wskaźnik Ihis W klasie CBox napisaliśmy funkcję Vol ume ( ) w kategoriach nazw składowych klasy w definicji tej klasy. Oczywiście, każdy obiekt klasy CBox, który utworzymy, będzie zawierał te składowe, a wi ęc musi być jakiś mechanizm pozwalający funkcji odnosić się do składowych określonego obiektu, dla którego została ona wywołana . Kiedy wykonywana jest funkcja składowa, to automatycznie zawiera ona ukryty wskaźnik o nazwie t hi s, który wskazuje obiekt użyty w wywołaniu tej funkcji. A zatem kiedy podczas wykonywania funkcji Vo l ullle ( ) uzyskuje ona dostęp do składowej m_Length , to w rzeczywistości odno si się ona do t hi s->mJ ength, co stanowi pełne odniesienie do używanej skła dowej. Kompilator sam zajmuje s i ę dodaniem niezbędnego wsk aźn ika thi s do nazw składo wych w funkcj i.
396
Visual C++ 2005. Od podstaw Jeżeli istn ieje taka potrzeba, wskaźnik thi s możn a stosować jawnie w obrębie funkcji skła dowej. Może się to na przykład okazać przydatne, kiedy chcemy zwrócić wskaźnik do bieżą cego obiektu.
~ Jawne lIżycie wskaźnika 'Ihis Do klasy CBox dodamy tów klasy CBox.
funkcję
o
dostępie
publicznym,
porównującą pojemnoś ć
II Cw7_ l O.cpp II Używanie wskaźnika this.
#Hlcl ude using st d: :cout : using st d: :endl: class CBox
II Definicja klasy o zasięgu globalnym.
(
publ te : II Defini cja konstruktora.
CBox(do uble l v = 1.0. doub le bv = 1.0. double hv = 1.0) (
cout « end l « "Konst rukt or m_Length = l v: m_Width = bv: m_Height = hv:
zo stał wywo łany." :
II Ustawianie wartości
II zmiennych składowych.
II Funkcja obliczająca pojemność pudełka.
double Vo l ume( ) {
} II Funk cj a poró wnująca dwa pudełka, która zwraca true (1), IIjeźe li pi erwsze jes t większe n iż drugie, oraz fa lse (O) w przeciwnym przypadku .
II Deklaracja pudelka match. II Deklaracja pudelka cigar.
if (ci gar .Compare(mat ch)) cout « end l « " P u d eł k o mat ch jest mniejsze
ni ż
cigar":
dwóch obiek-
Rozdzial7.• Definiowanie własnych typÓW danych el se cout
« «
397
endl "Pu de łk o
matc h jest równe lub w ię k s z e od
pu de ł k a
cigar";
cout « end l ; ret urn O;
Jak to lłziała Funkcja składowa Compa re( ) zwraca wartość true, jeżeli występujący w jej wywołaniu w formie przedrostkowej obiekt klasy CBox ma pojemność więk szą niż obiekt CBox podany jako jej argument, oraz fa l se w przeciwnym przypadku. W instrukcji ret ur n do obiektu w formie przedrostkowej odnosimy się za pomocą wskaźnika thi s użytego z operatorem pośredniego dostępu do składow ych - > (w i dz i e l iś my go już wcześniej w tym rozdziale). Należy pamiętać, że
operatora b ezp ośredniego dostępu do s kłado wyc h używamy. gdy uzyskuj emy poprzez obiekty, a operatora pośredniego dostępu do składowych, kiedy dostęp uzyskujemy poprzez wskaźn ik do obiektu. W naszym przypadkujest to wskaź nik, więc użyliśmy operatora - >. dostęp
Operator - > d z iała w taki sam sposób ze wskaźnikami do obiektów klas jak w zastosowaniu ze strukturami. W powyższym programie użycie wskaźnika thi s pokazuje, że on istnieje i działa, ale w rzeczywisto ści jego jawne użycie nie było w tym przypadku konieczne. Jeżeli instrukcję r et urn w funkcji Compa r e( ) zmienimy na:
ret urn Vol ume()
>
xBox.Vo l ume();
to stwierdzimy, że program działa bez zmian . Wszelkie odniesienia do nazw składowych bez żadnych "ozdobników" są automatycznie traktowane jako składowe obiektu wskazywane przez w skaźnik t hi s. Funkcji Compa re( ) w obrębie funkcji mai n( ) użyliśmy w celu sprawdzenia stosunku obiektów mat ch i ci gar . Rezultat uruchomienia tego programu jest następujący:
objętości
Konst ruktor został wyw ołany .
Konst ruktor z os ta ł wywo łany .
P u de łko match jest mnl ej sze n i ż cigar Potwierdza to, że obiekt cig ar jest większy od obiektu ma tch . Definicja funkcji Compare( ) jako s kład ow ej klasy również nie była konieczna. Równie dobrze można było ją zapisać jako zwykłą funkcję przyjmującą argumenty w postaci obiektów . Zauważ, że z funkcj ą Vo l urne() już by się to nie udało, ponieważ potrzebuje ona dostępu do prywatnych sk ładowych klasy. Oczywiście , gdybyśmy zaimplementowali funkcję Cornpare( ) jako zwykłą funkcję, to nie miałaby ona d ostępu do wskaźnika th i s, ale nadal byłaby bardzo prosta: II Porównywanie dwóch obiektów klasy CBox - funkcj a w wersji zwyczajnej.
i nt Compare(CBox B1 , CBox B2) {
ret urn B1 .Vol ume( ) }
>
B2.Volume();
398
Visual C++ 2005. Od podstaw Funkcja w tej wersji przyjmuje oba obiekty w postaci argumentów i zwraca wartość true,jcżeli pierwszego jest większa niż drugiego. Funkcji tej użylibyśmy w tym samym celu co poprzednio w następującej instrukcji : pojemność
if (Compare(cl gar . matc h)) cout « endl « "Pu d eł k o match j est mn i ej sze ni ż cigar" ; els e caut « endl « "P ud e ł k o mat ch j est równe lub wię ksze ad
p ude ł k a
ci gar ";
Ta wersja wygląda nieco lepiej i jest odrobinę bardziej czytelna niż oryginalna, choć istnieje sposób, który nadaje się do tego o wiele lepiej, ale o tym dopiero w następnym rozdziale.
Stale obiekty klasy Funkcja Vo lume(), zdefiniowana przez nas dla klasy CBox, nie zmienia zawartości obiektu, dla którego została wywołana, nie robi tego także taka funkcja jak na przykład getHei ghW, zwracająca wartość zmiennej składowej m_Height . W poprzednim przykładzie funkcja Compa re ( ) również w żaden sposób nie wpłynęła na zawartość obiektów. Na pierwszy rzut oka może się to wydawać mało interesującą i bardzo nieadekwatną uwagą, ale tak nie jest. Jestto bardzo ważne spostrzeżenie. Pomyślmy o tym. Z pewnością nadejdzie taki moment, że zechcesz od czasu do czasu stworzyć obiekt klasy o stałej zawartości, podobnie jak zmienne typu pi albo ce ntymetrowNaMetr, które możemy zadeklarować jako s tał e typu doubl e. Przypu śćmy , że chcemy zdefiniować obiekt klasy CBox jako const (ponieważ jest to na przykład bardzo ważne pudełko o standardowych rozmiarach) . Definicja tego obiektu mogłaby wyglądać następująco: const CBax st andard(3. 0. 5.0 . 8.0) ; Mając już
zdefiniowane standardowe pudełko o wymiarach 3 x5 x8, nie chcemy, aby cokolwiek mogło mieć do niego zbyt duże prawa dostępu. W szczególności zależy nam na ochronie przed zmianą wymiarów pudełka. W jaki sposób można tego dokonać ? Już
tego dokonaliśmy . Jeżeli obiekt klasy zadeklarujemy ze słowem kluczowym const, to kompilator nie pozwoli na wywołanie dla niego żadnej funkcji składowej, która mogłaby zmie· nić jego zawartość. Aby to sprawdzić , możemy zmodyfikować deklarację obiektu ciqar z poprzedniego przykładu w następujący sposób: canst CBax cigar (8. 0. 5.0 .1 .0) ;
II Deklaracja obiektu cigar.
Próba skompilowania programu po wprowadzeniu tej zmiany zakończy się niepowodzeniem i zgłoszeniem przez kompilator poniższego komunikatu o błędzie : er ra r C2662: 'c ampare' : cannot convert ' t his ' pointer from 'const cl ass CBax' to 'c l ass CBax &' Conversion loses Qual i fi ers
Rozdział 7.•
Definiowanie własnych typÓW danych
399
Komunikat ten dotyczy instrukcji warunkowej i f, która wywołuje funkcj ę składową Campare( ) obiektu cigar. Obiekt zadekl arowany jako canst zawsze ma wskaźnik t hi s, który jest typu canst . W zw iązku z tym kompilator nie pozwoli na wywołanie żadnej funkcji składowej , która nie przyjmuje wskaźnika th i s przekazanego jako canst. Musimy się tylk o dowiedzie ć , co zrobić, aby wskaźnik thi s w funkcji składowej był stały .
Stale funkcje składowe klasy Aby wsk aźnik thi s w funkcji składowej był stały , należy tę funkcję zade kl arować jako canst w definicj i klasy. Spójrzmy , jak to się robi, na przykładzie funkcji CampareO, która jest skła dową CSax. Definicja klasy musi zostać zmodyfikowana w następujący sposób :
class CBox II Defin icj a klasy o zasięgu globalnym . (
publ ic: II Defini cj a konstrukt ora.
CBox(double l v = 1.0. double bv = 1.0. double hv = 1.0) {
cout « end l « "Konst rukto r m_Length = l v; mHidth ~ bv ; m_Height ~ hv;
zo stał wywo ła ny .",
II Ustawianie wartośc i II zmiennych sk łado wych.
} II Funkcja
obliczająca pojemność pudełka,
double Vol umeCl (
ret urn thi s->Vol ume() > xBox,Volume( ): }
pri va t e: doub le m_L ength : double m_Wid t h: double m_Hei ght :
II Długo ść p ude łka w centymetrach. II Szero koś ć pudełka w centym etrach. II Wys okoś ć pudełka w centym etrach.
}:
W celu określenia funk cji składowej jako canst wystarczy do jej nagłówka dod a ć słowo kluczowe const . Należy jednak pamiętać, że jest to dozwolone tylko w przypadku funkcji skła dowych klas, w przypadku zwykłych funkcji globalnych jest to niedozwolone. Deklaracja funkcji jako canst ma znaczenie tylko wtedy, gdy funkcja je st składową klasy . W efekcie otrzymujemy stały wskaźnik thi s, co z kolei oznacza, że nie można składowej klasy wstawić po lewej stronie przypis ania w obrębie definicji funkcji. Czynność taka zostałaby przez kompilator potraktowana jako błąd. Funkcja składowa typu const nic może wywołać funkcji skła dowej, która nie jest stała i należy do tej samej klasy, jako że potencjalnie mogłaby zmodyfikować obiekt.
400
VisIlai C++ 2005. Od podstaw Deklaruj ąc obiekt jako const , funkcje skł adowe dla niego wywoływane również mu s zą by ć zadeklarowane jako const . W przeciwnym przypadku pro gramu nie b ędzie można skompilować .
Oeliniowanie lunkcii składowei poza klasą funk cji składowej znajduj e si ę poza klas ą, w nagłówku tej funkcji musi klu czowe const, podobnie jak w deklaracji wewnątrz klasy. Wszystkie funkcje s kładowe , niemające wpływu na zawarto ść obiektu, dla którego zostały wywołane, powinny być deklarow ane jako const. Pamiętając o tym, definicja klasy CSox może wyg lądać
II Obliczanie pojemn oś ci pudelka . II Porównanie dwóch pudelek.
II Dlugoś ć pudelka w centymetrach. II Szerokość pudełka w centymetrach. II Wysokość pudelka w centy metrach.
}; Powyższy
kod zakład a, że wszystkie funkcje składowe zostały utworzone oddzielnie, włącz nie z konstruktorem. Zarówno funkcja vol unet ), jak i Compa reO zo s tały zadeklarowane jako co nst. Definicja funkcji VOl ume () znajduje się teraz poza kla są i wygląda n astępująco :
double CBox: :Vol ume() const { }
Definicja funkcji CompareO :
int CBox: ;Compare( CBox xBox) const {
ret urn t hi s->Vol ume() > xBox. Volume() : }
Jak widać, w obu definicjach znajduje się modyfikator const. Gdyby został pominięty, programu nie można by skompilować. Funk cja z modyfikatorem const jest inną funkcj ą niż bez niego , nawet jeżeli posiada taką s am ą nazwę oraz parametry. W rze czywistości w klasie możn a mieć dwie wersje jednej funkcji - z modyfikatorem const i bez niego. Czasamijest to bardzo przydatne. Jeżeli zosta ć
klasa została zadeklarowana w pokazany zadeklarowany oddzi elnie :
Tablice obiektów klasy Tablice obiektów klasy deklaruje się w identyczny sposób jak zwykłe tablice elementów jednego z typów wbudowanych. Każdy element tablicy obiektów klasy powoduje wywołani e domyślnego konstruktora.
Rm!!miI Tablice obiektów klasy Użyjemy definicji klasy CSox z poprzedniego programu, ale zmodyfikujemy ją w celu nia konstruktora domyślnego :
II Cw7_ 11.cpp II Używanie tablicy obiektów klasy .
#i nc lude usi ng std : :cout : usi ng st d: :endl : class CBox
II Definicja klasy o zasięgu globalnym.
{
pub l ic: II De inicia konstruktora .
CBox( double lv. double bv = 1.0. double hv = 1.0) cout « endl « "Konst rukt or m_Lengt h = l v: m_Width = by : m_Height = hv:
zo st a ł wywoł a ny." :
CBox()
II Ustawianie wartości II zmiennych składowych.
II Konstruktor domyślny.
{
cout
endl "Konst rukt or d o my ś lny zos ta ł m_Length = m_Width = m_Helght = 1.0: « «
VislJal C++ 2005. Od podstaw double m_W idth ; double m_He ight:
II Szerokość pudelka w centymetrach. II Wys okość pudelka w centym etrach.
}:
int main() {
(Box boxes[5]: ( Box cigarCS .O. 5.0. 1.0); cout
« « « «
II Dekl aracja tablicy obiektów klasy CBox. II Deklaracja obiektu cigar.
endl
"Poj em n ość p u d eł ka
boxes[3] = "
«
boxes[3J. Vol ume()
cigar = "
cigar .Volume() :
endl
"Po jemn oś ć pu de ł k a
«
cout « end l : ret urn O: W rezultacie uruchomienia tego programu otrzymujemy:
Konst rukt or Konstruktor Konst ruktor Konstrukt or Konst ruktor Konstruktor
d omy ś l ny z o st ał wywo ł any .
d o my ś lny z o sta ł wywoł any .
do my ślny zo s ta ł wywo ł any .
d omy ślny zo s t ał wywo ł any .
d omy ślny zos t ał wywo ł a ny .
z o s t ał wywo ł a ny .
P o j emn ość p u d e ł k a
Pojemn o ś ć p ude ł k a
boxes[3] = l
cigar = 40
Jak to działa Konstruktor przyjmujący argumenty zmodyfikowaliśmy w taki sposób, że podane są tylko dwie wartości domyślne. Dodaliśmy także domyślny konstruktor inicjalizujący zmienne skła dowe wartością l po uprzednim wysłaniu na wyjście informacji, że został wywołany. Możemy teraz zaobserwować, kiedy został wywołany dany konstruktor. Listy parametrów konstruktorów bardzo się tym razem różnią, a więc nie ma możliwości popełnienia pomyłki przez kompilator. Z danych na wyjściu wynika, że konstruktor domyślny został wywołany pięć razy - raz dla każdego elementu tablicy boxes . Drugi konstruktor został wywołany w celu utworzenia obiektu ci gar. Z tego, co widać na ekranie, jasno wynika, że konstruktor domyślny działa satysfakcjo nująco, jako że pojemność elementu tablicy wynosi l .
Składowe statyczne klasy Statyczne (static) mogą być zarówno zmienne, jak i funkcje składowe. Jako że kontekstem jest definicja klasy , użycie słowa kluczowego stat tc wiąże się z czymś więcej niż w przypad ku jego użycia poza klasą. Przyjrzymy się teraz statycznym zmiennym składowym.
Rozdzial7.• Definiowanie własnych typÓW danych
403
Statyczne zmienne składowe klasy
Rezultatem zdefiniowania zmiennej składowej jako statycznej jest możliwość współdzielenia jej przez wszystkie obiekty klasy, z tym że definiuje się ją tylko raz. Każdy obiekt otrzymuje własne kopie zwykłych zmiennych składowych klasy. Natomiast zmienna składowa statyczna jest tylko jedna, bez względu na to, ile obiektów zostało zdefiniowanych. Koncepcję tę przed stawiono na rysunku 7.7. Definicja klasy
Jedna kopia statycznej zmienn ej s kła d o wej wsp 6łdz ielona jest przez wszystkie obiekty klasy
Jednym z zastosowań statycznych zmiennych składowych jest zliczanie liczby istniejących obiektów . Statyczną zmienną składową możemy dodać do sekcji publicznej wcześniej zdefi niowanej klasy CBox za pomocą poniższej instrukcji:
st ati c int object Count : Tu pojawia
się
problem. Jak
II Licznik
istniejących
obiektów,
zainicjalizować statyczną zmienną składową?
Nie można jej zainicjalizować w definicji klasy, gdyż stanowi ona tylko szkielet obiektu i w związku z tym inicjalizowanie w niej wartości jest niedozwolone. Nie możemy jej inicja lizować w konstruktorze, ponieważ chcemy ją zwiększać za każdym jego wywołaniem w celu policzenia liczby obiektów. Nie możemy jej także zainicjalizować w innej funkcji składowej, ponieważ funkcja składowa skojarzona jest z obiektem, a my chcemy, aby inicjalizacja nastą piła przed utworzeniem jakiegokolwiek obiektu. Rozwiązaniem jest zainicjalizowanie takiej zmiennej poza klasą za pomocą następującej instrukcji:
404
Visual C++ 2005. Od podstaw i nt CBox: :obj ect Count =
o:
II Jnicj alizacj a statycznej zmi ennej skladowej klasy CBox.
instrukcji tej nie ma s łowa kluczowego stet tc. Musieliśmy natomiast odpowiednio nazw ę zmiennej składowej za pomocą nazwy klasy oraz operatora zasięgu, dzięki czemu kompilator " wie ", że odnosimy się do statycznej składo wej klasy. Gdybyśmy tego nie zrobili, utworzylibyśmy zwykłą zmienną globalną, niema jącą nic wspó lnego z klasą. Zwró ć u wagę, że w
zakwalifikować
~ Liczenie egzemplarzy Do kodu z poprzedniego listingu dodamy nia obiektów .
statyczn ą zm ienną s kłado wą oraz możl iwo ść
IICw7j 2.cpp
II Używanie staty cznej zmi ennej skladowej w klasie.
#incl ude
usi ng std: :cout:
using st drendl .
class CBox
II Definicja klasy o zasięgu global nym.
(
ub lic ; st ati c i nt object Count ;
II Liczba
istn iejących
obiektów.
II Definicja konstruktora.
CBox(double l v. double bv = 1.0. double hv = 1.0) cout « endl « "Konstruktor m_Length = l v: m_W idth = bv: mHei ght = hv:
objectCount++ ;
zos ta ł wywo ła ny .
CBox()
":
II Usta wianie wartośc i II zmiennych skladowy ch.
II Konstruktor domyślny,
(
cout
endl "Konst ruktor do myś lny zasta l wywo ła ny , ",
II Dlugos ć pudelka w centymetrach. II Szerokoś ć p udelka w centymetrach. II Wysokoś ć pudelka w centymetrach.
zlicza
Rozdzial7.• Definiowanie własnych typÓW danych int CBox: :obj ect Count
=
o:
405
II Jnicjalizacja skladowej statycznej klasy CBox.
int ma in() I
CBox boxes[5J: CBox cigar (S O. 5 O. 1.0): cout « end l « endl
« "Liczba obiektów (przez « CBox : :object Count :
cout
« « «
II Deklara cja tabli cy ob iektów klasy CBox. II Deklara cja pudelka cigar.
k l a s ę )
endl
"Li czba obiektów (przez obiekt)
boxes[2J.obJectCount :
cout « endl :
return O:
}
Rezultat
działania tego
programu jest następujący:
Konstruktor domy śl ny zo s tał wywo ł a ny .
Konstrukto r domyś lny zo st ał wywoł any .
Konst ruktor domy ś l ny zos ta ł wywoł a ny .
Konstruktor domyś lny zos t a ł wywolany.
Konst ruktor d omyślny zosta l wywolany.
Konstruktor zost al wywolany.
Li czba obiekt ów (przez k l a s ę ) = 6 Liczba obiektów (przez obiekt ) ~ 6
Jak to działa Powyższy kod pokazuje, że sposób odwołania do składowej statycznej Obj ectCount nie ma znaczenia (czy to przez samą klasę , czy przez obiekty tej klasy) . Wartość jest zawsze taka sama i równa liczbie utworzonych obiektów klasy. Te sześć obiektów to oczywiście pięć ele mentów tablicy Boxes oraz obiekt c i ga r . Interesujący jest fakt, że statyczne składowe klasy istnieją nawet wtedy, gdy nie ma żadnych składowych klasy. Tak jest w naszym przypadku, gdyż statyczną składową zainicjalizowaliśmy , zanim zostały zadeklarowane jakiekolwiek obiekty klasy.
Statyczne zmienn e składowe tworzone są automatycznie w momencie rozpoczęcia działa nia programu i inicjalizowane wartością O, chy ba że podamy jakąś inną wartoś ć po cząt kową. Jeżeli chcemy więc, aby statyczne składowe klasy miały wartości początkowe inne n iż O, musimy je sami zainicjalizować.
Slalyczne funkcje składowe klasy Deklarując funkcję składowąjako statyczną, uni ezależniamy ją
od wszystkich obiektów klasy . z wnętrza funkcji statycznej do składowych klasy, należy pamiętać o podaniu kwa lifikatorów w postaci nazw (tak jak w przypadku zwykłych funkcji globalnych, uzyskujących Odnosząc się
40&
lisual C++ 2005. Od podstaw dostęp
do publicznych
i może
być wywoływana,
składowych
klasy). Zaletą funkcji statycznej jest fakt, że istnieje ona nawet gdy nie istnieją żadne obiekty klasy.
W takim przypadku można używać tylko statycznych składowych klasy , gdyż tylko takie istnieją. Można zatem wywołać statyczną funkcję składową klasy w celu zbadania statycznych zmiennych składowych, nawet gdy nie mamy pewności, czy istnieją jakiekolwiek obiekty tej klasy. W związku z tym można określić, czy zostały utworzone jakiekolwiek obiekty klasy, a jeśli tak, to ile ich jest. Oczywiście po
zdefiniowaniu obiektów statyczna funkcja składowa ma dostęp zarówno do prywatnych, jak i publicznych składowych obiektów. Prototyp funkcji statycznej może wyglą
dać następująco :
stati c void funk cja(int n) ; Funkcję statyczną można wywołać
w odniesieniu do określonego obiektu za pomocą instrukcji
podobnej do poniższej :
aBox.f unk cja(lO); gdzie aBox jest obiektem klasy. Tę samą funkcję można również wywołać bez odniesienia do obiektu. W takim przypadku przybierze ona następującą postać:
CBox: :funkcja (l O); gdzie CBox jest nazwą klasy. Nazwa klasy i operator zasięgu klasy należy funkcja funkc j a ( ).
informują kompilator,
do której
Wskaźniki ireferencje do obiektów klasy Wskaźniki,
a w szczególności referencje do obiektów klasy są bardzo ważne w programo waniu obiektowym, zwłaszcza przy określaniu parametrów funkcji. Obiekty mogą zawierać bardzo duże ilości danych, w związku z czym użycie mechanizmu przekazywania przez war tość, podając jako parametry funkcji obiekty, może być bardzo mało wydajne i czasochłonne, ponieważ każdy taki argument-obiekt musi zostać skopiowany. Istnieją także pewne tech niki polegające na użyciu referencji, które są podstawą niektórych operacji z użyciem klas. Jak się niebawem przekonamy, nie można użyć konstruktora kopiującego bez użycia parame tru w postaci referencji.
Wskaźniki do obiektów Wskaźnik
do obiektu deklarujemy w taki sam sposób jak każdy inny wskaźnik. Na w poniższej instrukcji zadeklarowany został wskaźnik do obiektów klasy CBox:
CBox* pBox = o;
II Deklaracja
wskaźnika
do CBox.
przykład
Rozdzial7.• Deliniowanie własnych typÓW danych
407
Wskaźnika tego można teraz uży ć do przechowywania adresu obiektu klasy CBox, podając mu ten adres za pomocą zwykłej instrukcji przypisania z użyciem operatora przypisania, jak p on i żej:
pBox = &cigar : Jak
II Zap isanie adresu obiektu klasy CBox cigar do
ws kaźnika pB ox.
widzieliśmy, używając wskaźn ika
thi s w definicji funkcji s kład owej Compa r eO , funkcję przy użyciu wsk aźnika do obiektu. Funkcję Vo1ume() dla wsk aźnika pBox wywołać za pomocą następującej instrukcji :
możn a wywołać m ożemy
cout
«
pBox->Volume () :
II Wyswietl pojemnoś ć obiektu wskazywanego prz ez
wskaźn ik pB ox.
W powyższej instrukcji znowu u żyty został operator pośredniego dostępu do s kład o wyc h. Jest to typowa, stosowana przez większość programistów notacja tego typu operacji. Od tej pory będę j ej u żywał cały czas.
~ Wskaźniki do klas Zrobimy sobie dodatkowe ćw i czen ie z zastosowania operatora pośredniego do stępu do skła dowych. Jako podstawę wykorzystamy kod programu Cw7_ lO.cpp i wprowadzimy do niego pewne modyfika cje. II Cw7j 3.cpp
II Ćwicze nie zastosowa nia operatora pośredn iego dostępu do skladowych klasy.
#i nc l ude
using st d: :cout :
USlng st d: :endl :
class CBox
II Definicj a klasy o zas ięgu g lobalny m.
{
publ ic:
II Definicj a konstrukt ora.
CBox(double l v = 1.0. double bv = 1.0. double hv
~
1.0)
{
cout « endl « "Konstruktor m_Length = l v: m Wid t h = bv:
m_Height
=
zo stał wywo ła ny . " :
II Ustawianie wartości
II zmi ennych skladowych.
hv:
II Funk cja oh/iczająca pojemność pudelka.
doub le Vol ume() con st
{
II Funkcja po ró wnująca dwa pudelka, która zwra ca lnie (1),
II jeżeli pierwsze jest wieksz e niż drugie, oraz false (O) w przeciwnym przypadku .
int Compare(CBox* pBox) const
{
ret urn thl S->Volume() > pBox- >Vol ume ( ):
408
Visual C++ 2005. Od podstaw pri vate : doub le m_Lengt h; doub le mWid t h: doub le m=Height :
II Dlugość pudelka w centymetrach. II Szerokoś ć pudelka w centym etrach. II Wysokość pudelka w centymetrach.
}:
int mai n() CBox boxes[5] : CBox mat ch(2.2. 1.1. 0.5); CBox clgar (8 O. 5 O. 1. O). CBox* pBl ~ &c igar ; CBox* pB2 ~ O:
II Deklaracja tablicy obiektów klasy CBox. II Deklaracja obiektu match.
II Deklaracja obiektu cigar.
Il l nicj alizacj a ws kaźnika do adresu obiektu cigar.
Il l nicj alizacj a wskaźnika do klasy CBox wartos etą nul/.
cout « endl « "Adres obiekt u cigar t o " « pBl II Wyświetlanie adresów. « endl « " Poj em n ość pudełka cigar to .. « pBl->Vo l ume () : II Pojemn oś ć wskazywanych obiekt ów. pB2 ~ &matc h; i f( pB2->Compare(pBl) II Porównywanie p oprzez cout « endl « " P u dełk o mat ch jest wi ę ksze ni ż cigar"; el se cout « end l « " P u d e ł k o match Jest mn iej sze l ub równe cigar": pBl ~ boxes: boxes[ 2] ~ matc h; cout « endl «
" Poj emność p ude ł k a
boxes[2] t o "
wskaźniki.
II Ustawienie na adres tablicy.
II Ustawienie trzeciego elementu tablicy na match.
II Dostęp poprzez wskaźn ik.
« (pB l + 2)->Vol ume() ;
cout « endl : ret urn O; Wykonanie
powyższego
programu da rezultat podobny do poniższego :
Konst rukt or zos t a ł wywo ł a ny .
Konst rukt or z os t a ł wywoła ny .
Konstruktor zos t a ł wywoł any .
Konst ruktor zos tał wywo ła ny .
Konst rukt or zo stał wywołany .
Konstruktor zo s ta ł wywoła ny .
Konst rukt or zos t ał wywoła ny .
Adres obiektu cigar to 0012FE20
P oj e mność pu de ł k a cigar t o 40
P u de ł ko m atc h jest mniej sze l ub równe cigar
Pojemność pu de łk a boxes[2] to 1.21
Oczywiście
adres obiektu c i gar na każdym komputerz e może
być
inny.
Rozdzial7.• Definiowanie własnych typÓW danych
409
Jak to działa Jedyna zmiana, której dokonaliśmy w klasie, nie ma wielkiego znaczenia. Polegała ona na modyfikacji funkcji Compa re() w taki sposób, aby przyjmowała j ako argument wsk aźnik do obiektu klasy CBox. Dodatkowo, kiedy mamy j uż w iedzę na temat stałych funkcji s kł a dowych, dekl arujemy ją za pomocą słówa kluczowego const, ponieważ nie wpływa ona na zmianę zawarto ści obiektu. W funkcji ma i n() na różne sposoby użyte zo stały wskaźniki do obiektów klasy CBox. W obrębie funkcji main() deklarujemy dwa w skaźn iki do obiektów klasy CBox po deklaracji tabl icy Boxes oraz obiekty klasy Cbox: cigar i mat ch. Pierwszy wskaźnik pBI zo stał zainicjali zowany adresem obiektu ciga r, a drugi (pB2) wartością NULL. Wszystkie te użycia wskaźników są dokładnie takie same, jak gdybyśmy mieli do czynienia ze wskaźnikami do typów pod stawowych. Fakt, ż e używamy wskaźnika do typu, który sami stworzyliśmy , nie robi żadnej różnicy.
Wskaźnik pBI w połączen iu z operatorem pośredniego dostępu do s kład owyc h u żyty został do obliczenia pojemności obiektu przez niego wskazywanego. Wynik jest później wysyłany na wyjście . Następnie wskaźnikowi pB2 przypisujemy adres obiektu match i obu wskaźników używamy do wywołania funkcji Compare(). Jako że argument funkcji Compare() jest wskaźni kiem do obiektu klasy CBox, funkcja ta w wywołaniu funkcji Vol ume O dla obiektu wykorzy stuje operator pośredniego dostępu do składowych .
pBI podczas korzystania z niego przy wyborze składowej mo ustawiamy wskaźnik pBI na adres pierwszego elementu tablicy typu CBox - boxes. W tym przypadku wybieramy trzeci element tabli cy i obli czamy jego pojemność . Jest ona taka sama jak pojemność obiektu mat ch. Aby
pokazać , że
na
wskaźniku
żemy zastosować arytmetykę adresową,
Z danych na wyjśc iu wynika, że konstruktor obi ektów klasy CBo x został wywołany siedem razy : pięć dla elementów tablicy Boxes plus po jednym razie dla obiektów ciga r i match. Mówiąc ogólnie, pomiędzy używaniem wskaźnika
takiego jak np. doubl e, nie ma
do obiektów klasy i do typu podstawowego,
ża d nej różnicy .
Referencie do obiektów Referencje stają s ię naprawdę przydatne, kiedy używamy ich z klasami . Podobn ie jak w przy padku wskaźników, nie ma żadnej różnicy pom iędzy stosowani em referencji do obiektów klas a stosowaniem ich do zmiennych typów podstawowych. Na przykład referencję do obiektu ci gar możemy utworzyć za pomocą poniższej instrukcji:
CBox&rciga r
=
cigar :
II Definicja ref erencji do obiektu cigar.
Aby użyć referencji do obliczenia pojemności obiektu ci gar , wystarczy użyć tylko nazwy refe rencji w miejscu, gdzie powinna pojawić się nazwa obiektu:
cout
«
rcigar.Volume();
II Wyś lij p oprzez II ob iektu ciga r.
ws kaźn ik na wyjście pojemność
410
Visual C++ 2005. Od podslaw Jak prawdopodobnie pamiętasz, referencje działająjak aliasy obiektów, do których w związku z czym używa się ich dokładnie tak samo ja k nazw oryg inalnych.
się odnoszą,
Implemenlacia konslruklora kopiUiącegO Znaczenie referencji jest naprawdę widoc zne dopiero w kontekś cie argumentów do funkcji i wartoś c i przez nie zwracanych, a w szcz egó l no ś c i funkcji będących składowymi klas. Na dobry początek powróćmy do kwestii konstruktora kopiującego . Chwilowo odkładamy na bok kwestię , kiedy potrzebujemy napisać własny konstruktor kopiujący, a koncentrujemy się na tym, jak go napi sać . Analizując problem , posłużę się klasą CBox j ako przykładem. Konstruktor kop iujący to konstruktor, który tworzy obiekt, inicjalizując go innym istn iejącym obiektem tej samej klasy . W związku z tym musi on akceptować jako argumenty obiekty tej klasy. Jego prototyp moglibyśmy napisać następująco:
już
CBox(CBox init B); Spójrzmy, co
się
stanie, gdy
wywołamy
taki konstrukto r.
Przypuśćmy , że
mamy
następującą
dekl arację:
CBox myBox = cigar : Wygeneruje ona
następujące wywołanie
kon struktora kopiującego:
CBox : :CBox(cigar): Wydaje się , że kod jest w pełn i właśc iwy , dopóki nie zdamy sobie sprawy, że argument jest przekazywany przez wartość. Oznacza to, że przed przekazaniem obiektu ci gar kompilator musi zrobić jego kopię W związku z tym kompilator wywołuje konstruktor kopiujący w celu utworzenia kopii argumentu użytego do wywołania konstruktora kopiującego . Niestety, jako że obiekt ten przekazywany jest przez wartość , to wywołanie również wymaga zrobienia kopii argumentu, a więc zostaje wywołany konstruktor kopiujący i tak c ały czas. Rozwi ązaniem
- jestem pewien , że udało Ci się do tego doj ść samodzielnie - jest u życie parametru referencyjnego typu const . Prototyp konstruktora możemy zapisać następująco :
CBox(const CBox& init B): Teraz argument konstruktora kopiującego nie musi być kopiowany. Jest on użyty do zaini cjalizowania parametru referencyjnego, a więc nie następuje żadne kopiowanie. Jak pami ętam y z częś ci, w której była mowa o referencjach, jeżeli parametr funkcji je st referencją, to argu ment podczas wywołania funkcji nie jest kopiowany. Funkcja ta uzyskuje bezpośredni do stęp do zmiennej argumentu. Kwalifikator const zapewnia, że argument ten nie zostanie w żaden sposób zmodyfikowany przez funkcję. Jest to jeszcze j edno ważne zastosowanie kwalifikatora const. Zawsz e po winno się dekla rować parametr ref erencyjny funkcji jako const . chyba że funkcja ma go zmodyfiko wać.
Implementacja takiego konstruktora kopiującego
może wygl ąda ć następuj ąco:
Rozdzial7.• Definiowanie własnych typÓW danych
411
CBox : :CBox(const CBox&initB) {
m_Length = initB .m_Length:
m_W idt h = ini t B. m_Widt h:
m_Height = initB .m_Height :
Definicja konstruktora kopiującego narzuca umieszczenie go poza definicją klasy. W związku z tym przed nazwą konstruktora musi znajdować się kwalifikator w postaci nazwy klasy z dodatkiem operatora zasięgu. Każda składowa tworzonego obiektu inicjalizowana jest odpowiednią składową obiektu przekazanego jako argument. Oczywiście, do ustawienia war tości obiektu z takim samym powodzeniem możemy użyć listy inicjalizacyjnej. Przypadek ten nie jest przykładem tego, kiedy możemy potrzebować użyć konstruktora . Jak już widzieliśmy, domyślny konstruktor kopiujący działał bez zarzutów z obiektami klasy CBox. Kwestie, po co i kiedy używać własnego konstruktora kopiującego, poruszę w następnym rozdziale. kopiującego.
Programowanie wC++/CLI Język
C++/CLI posiada własne typy struct i cl ass. W rzeczywistości język ten pozwala na dwóch różnych typów st ruct i c l ass, które mają różne właściwości. Są to typy value st ruct i val ue cl ass oraz ref st ruct i ref cla ss. Każda para słów stanowi odrębne słowo kluczowe (value str uct , re f struct, value cla ss oraz ref cl ass) oznaczające co innego niż st ruct i cl ass . val ue i ref samodzielnie nie są słowami kluczowymi. Podobnie jak w natywnym CH, jedyną różnicą pomiędzy strukturami i klasami jest to, że składowe struktury są domyślnie publiczne, a składowe klasy prywatne. Największą różnicą pomiędzy klasami wartości (lub strukturami wartości ) oraz klasami referencji (lub strukturami referen cji) jest to, że zmienne typów klas wartości zawierają własne dane, natomiast zmienne dostępu do typów klas referencyjnych muszą być uchwytami, a zatem muszą zawierać adres y. definicję
Warto zwrócić
uwagę , że
funkcje składowe w C++/CLI nie mogą być deklarowane jako const. natywnym CH i C++/CLI jest to, że wskaźnik t his w niesta tycznej funkcj i składowej typu klasowego T j est wewnętrznym wskaźnikiem typu i nt e r i or_pt r, a wskaźnik t hi s w typie klasowym referencyjnym Tjest uchwytem typu T". Należy o tym pamiętać przy zwracaniu wskaźnika t hi s z funkcji w C++ /CLI lub podczas zapisywania go do zmiennej lokalnej. Sąjeszcze trzy inne ograniczenia, które mają zastoso wanie zarówno do klas wartości, jak i klas referencji: Następną różnicą pomiędzy
• Klasa wartości lub klasa referencji nie może zawierać pól w natywnym C++ lub typów klas w natywnym C++.
będących
tablicami
• Nie można używać funkcji zaprzyjaźnionych. • Klasa wartości lub referencji nie bitowymi .
może zawierać składowych będących
polami
412
Visual C++ 2005. 0(1 podstaw Jak już dowiedzieliśmy się w rozdziale 4., nazwy typów fundamentalnych, takie jak i nt i da ubl e, są skrótowym określeniem typów klasy wartości w programach CLR. Podczas deklaracji elementu danych typu klasy wartości pamięć dla niego zostanie przydzielona na stosie, ale obiekty klasy wartości można tworzyć również na stercie za pomocą operatora genew. W takim przypadku zmienna używana do uzyskiwania dostępu do takiego obiektu musi być uchwy tem. Na przykład: dauble pi ~ 3.142: i nt A lucky = 9cnew int(7); dauble A twa = 2.0 : Każdej
o
II Zmienna pi przechowywana jest na stosie .
/r tucky jest uchwytem i wartość 7 przechowywana jest na stercie.
II twa jest uchwytem i wartość 2. Oprzechowywana jest na stercie.
z tych zmiennych można użyć w działaniach arytmetycznych, ale należy pamiętać uchwytu za pomocą operatora * w celu uzyskania dostępu do wartości . Na
wyłuskaniu
przykład:
Cansa l e: :Wri tel i net l "2pi
~
{O}". *twa*pi l:
równie dobrze nasze działanie mogliśmy zapisać pi**twa i wynik byłby po prawny, ale lepiej jest w takim przypadku zastosować nawiasy i napisać pi*(*two), gdyż zwięk szają one czytelność kodu .
Zwróć uwagę, że
Deliniowanie typÓW klas wartości Nie będę opisywał typów value struet (strukturalnych) i typów value elass (klasowych) oddzielnie, ponieważ jedyna różnica pomiędzy nimi polega na tym, że składowe strukturalne domyślnie są publiczne, a składowe klasowe prywatne . Klasa wartości ma być względnie pro stą klasą umożliwiającą definiowanie nowych prymitywnych typów, których można używać w sposób podobny do typów fundamentalnych. Aby jednak móc w pełni korzystać z tych moż liwości, trzeba najpierw zaznajomić się z zagadnieniem przeciążania operatorów, o którym będzie mowa dopiero w następnym rozdziale. Zmienna typu klasy wartości tworzona jest na stosie i przechowuje wartość bezpośrednio, ale - jak już widzieliśmy---do typów skalarnych na stercie CLR możemy odnosić się za pomocą uchwytu śledzącego. Spójrzmy na przykład definicji prostej klasy II Klasa
reprezentująca
wartości:
wzrost.
(a lue class Hei 9ht private: II Zapisuje w ost w metrach i centymetrach.
t
i nt metry: i nt .centymetry:
publi c:
II Tworzenie
rostu z
wartości w
centymetrach.
Hei ghU i nt cm) {
metry = cmllOO:
centymetry ~ cm%100:
} II Tworzenie wzrostu z metrów i centymetrów.
HeighUint m. int cm) : netrytm) . centymetry(cm){}
};
Rozdzial7.• Deliniowanie wlasnych typÓW danych
413
Powyższy
kod definiuje klasę wartości o nazwie Hei ght. Ma ona dwa pola prywatne typu i nt, wzrost w metrach i centymetrach. Klasa posiada dwa konstruktory - jeden tworzący obiekt klasy Hei ght z liczby centymetrów podanej jako argument, a drugi obiekt klasy Hei ght z metrów i centymetrów podanych jako argumenty. Drugi z nich powinien jeszcze sprawdzać, czy podana jako argument liczba centymetrów jest mniejsza niż 100, ale pozo stawiam to'Czytelnikowi do własnego dostosowania. Aby utworzyć zmienną typu Hei ght,
które
zapisują
możemy posłużyć się następującą instrukcją :
Height t all = Height Cl, 80):
II Wzrost wynosi l metr 80 centym etrów.
Powyższa
instrukcja tworzy zmienną ta ll zawierającą obiekt klasy Height reprezentujący l metr 80 centymetrów. Utworzenie tego obiektu wymagało wywołania konstruktora z dwoma parametrami.
He ight baseHei ght: Powyższa instrukcja tworzy zmienną baseHei ght , która automatycznie zostanie zainicjali zowana wartością O. Klasa Height nie posiada konstruktora bezargumentowego i ze względu na fakt, że jest to klasa wartości, nie można go dostarczyć w definicji tej klasy. Konstruktor bezargumentowy zostanie dołączony automatycznie do klasy wartości podstawowych i zaini cjalizuje on wszystkie pola wartości do wartości równej O oraz wszystkie pola, które są uchwy tami do nullptr. Tego konstruktora nie można zastąpić własnym. Wartość zmiennej base He i ght zostanie utworzona właśnie przez ten domyślny konstruktor.
Istnieje jeszcze kilka innych • W definicji klasy nie
ogran iczeń dotyczących zawartości
można umieszczać
konstruktora
• Operatora przypisania w klasie wartości nie operatorów będzie mowa w rozdziale 8.).
klasy
wartości :
kopiującego.
można przesłonić
(o
przesłanianiu
Obiekty klasy wartości są zawsze kopiowane poprzez kopiowanie pól, a przypisanie jednego obiektu klasy wartości do drugiego wykonywane jest w ten sam sposób. Przeznaczeniem klas wartości jest reprezentowanie prostych obiektów definiowanych za pomocą ograniczonej ilości danych. W związku z tym w przypadku obiektów niepasujących do tego opisu lub w przypad kach, w których te ograniczenia sprawiają problemy, należy do reprezentacji obiektów używać klas referencyjnych. \ Przetestujmy naszą klasę wartości Heigh t:
~ Definiowanie iuiywanie klasy wartości Poniżej
znajduje
się
kod
ćwiczenia
#i ncl ude "st dafx.h" usi ng namespace System : II Klasa
reprezentująca
wzrost.
zastosowania klasy
wartości
skalarnych Hei ght:
414
Visual C++ 2005. Od podstaw val ue class Hei ght
{
pri vat e:
II Zap isyw anie wzrostu w metrach i centymetrach.
i nt metry. i nt centymetry : publ ic: II Tworzenie wzrostu z
wartoś ci
w centymetrach.
Hei ght(i nt cm) (
met ry = cm/lOO: cent ymet ry = cm% l OO: II Tworzenie wzrostu z metrów i centymetrów.
Hei ght(i nt m. i nt cm) : met ry(m). cent ymet ry(cm) {l
l: int main(array<Syste m: :Stri ng
Ą> Ą a r g s )
(
Height myHeight = Hei ght (1.70):
H eig h t yourHeight = He ight (160): Hei ght hisHei ght = *yourHei ght : Ą
Consol e: : \~ r i t e L i ne(L"M6j wzrost t o {O)" . myHeightl:
Console: .WriteL i ne(L"Twój wz rost t o (O)". yournet ght) :
Console : :WriteLi ne(L" Jego wzrost t o (Ol ". hisHeight ) :
ret urn o:
Uruchomienie tego programu da
następujący
rezultat:
Mój wzrost t o Height
Twój wz rost to Height
Jego wzrost to Height
Jak lo działa Wynik jest raczej mało ciekawy i pewnie spodziewaliśmy się czegoś więcej, ale wrócimy do tego trochę później. W funkcji mai n( ) utworzyliśmy trzy zmienne za pomocą następujących instrukcji: Height myHeight = Height(1 .70) :
He i ę h t " yourHeight = Hei ght( 60):
Height hisHeight = *yourHeight:
Pierwsza zmienna jest typu Hei ght , a więc obiektowi reprezentującemu wzrost l metr 70 centymetrów została przydzielona pamięć na stosie. Druga zmienna jest uchwytem typu He;g ht "', a więc obiektowi reprezentującemu wzrost l metr 60 centymetrów została przydzie lona pamięć na stercie CLR. Trzecia zmienna jest jeszcze jedną zmienną stanowiącą kopi ę obiektu, do którego odnosi s i ę obiekt your Hei ght. Jako że yourHeight jest uchwytem, przed przypisaniem go do zmiennej hi sHei ght musimy go najpierw wyłuskać . W wyniku tego hi s Hei ght zawiera kopię obiektu, do którego odnosi się yourHeight . Zmienne klasy wartości zaw sze zawierają unikalny obiekt, a więc dwie takie zmienne nic mogą odnosić się do tego samego
Rozdział7 .•
Definiowanie własnych typÓW danych
415
obiektu. Przypisywanie jednej zmiennej typu klasy wartości do innej zaws ze związane jest z kopiowaniem. Oczywiście kilka uchwytów może odnosić się do jednego obiektu , a przypisa nie wartości jednego uchwytu do drugiego jest po prostu skopiowaniem adresu (lub wartości nul l ptr) zjednego uchwytu do drugiego. W wyniku tego oba obiekty wskazują ten sam obiekt. Dane wysyłane są na wyj ście za pomocą trzech wywołań funkcji Consol e : :Wri tel i ne( ). Nie stety nie zostały wysłane wartości obiektów klasy wartości , a po prostu nazwa klasy . Jak to się stało? Oczekując , że zostaną wyświetlone wartości , byliśmy optymistami. Skąd kompilator miał wiedzieć, w jaki sposób je zaprezentować? Obiekty klasy Hei ght zawierają dwie wartości. Która z nich powinna zostać zaprezentowana na ekranie? W klasie musi być do stępny mecha nizm udostępniania danej wartości w danym kontekście.
Funkcja ToSlringlJ wklasie Każda
klasa defini owana w C++/CLl ma funkcję ToStri ng( ) (więcej na ten temat powiem rozdziale, kiedy będę omawiał dziedziczenie), która zwraca uchwyt do łańcucha reprezentującego obiekt klasy. Kompilator wywołuje funkcję ToStri ng() dla obiektu, kiedy stwierdza, że potrzebna jest łańcuchowa reprezentacja obiektu. Funkcję t ę można wywołać w razie potrzeby jawnie. Na przykład : w
następnym
double pi = 3.142 : Console : :WriteLi ne(pi .ToSt ri ng()) : Powyższa instrukcja wysyła na wyj ście wartość zmiennej pi w postaci łańcucha. Łańcuch ten jest dostarczony przez funkcję ToSt ri ngO zdefiniowaną w klasie System: :Double . Oczywiście ten sam wynik otrzymalibyśmy bez jawnego wywołania tej funkcji.
Funkcja ToSt ri ng( ) w domyślnej wersji , którą otrzymujemy w klasie Hei ght, wysyła na wyj śc ie tylko nazwę klasy, ponieważ nie ma sposobu dowiedzenia się z góry, która wartość po winna zostać zwrócona jako łańcuch dla obiektu naszego typu klasowego. Aby funkcja Con so l e : :Wri t el i ne () wysłała na wyj ście właściwą warto ść w poprzednim przykładzie, należy dodać funkcję ToSt r ing () do klasy Height, która zaprezentuje wartość obiektu w takiej formie, w jakiej chcemy. Poniżej
znajduj e
II Klasa
s ię
klasa z
reprezentują ca
dodan ą funkcją ToSt
r i ng ( ):
wzrost.
va l ue class Height
{
pri vate :
II Zapisuj e wzrost w metra ch i centymetrach.
int metry :
i nt centymetry :
publ ic
II Tworzenie wzrostu z
wa rtoś ci
w centymetrach.
Hei ght (i nt cm) {
met ry = cm/lOO :
centymet ry = cm%100:
} II Tworzenie wzros tu z metrów i centymetrów.
He ight (i nt m. i nt cm ) : met ry(m ) . cent ymet ry(cm){ }
416
Visual C++ 2005. Od podstaw II Tworzenie
łańcu ch o wej
reprezent acji obiektu.
vir tual Str ing ToStri ng() overrlde
A
{
ret urn met ry
+
L" met r
"+
centymet ry
+
L" centymetrów" ;
};
Kombinacja słowa kluczowego vi r t ual znajduj ącego się przed typem zwra canym funkcji ToSt r i ng() oraz słowa kluczowego over r i de znajdującego się po li ście parametrów funkcji wska zuje, że ta wersja funkcji ToSt ri ng ( ) przesłania domyślną wersję tej funkcji w klasie. Dużo więcej na ten temat powiemy sobie w rozdziale 8. Funkcja ToSt r ing( ) w nowej wersji wysyła na wyjście łańcuch wyrażający wzrost w metrach i centymetrach. Jeżeli dodamy tę funkcję do definicji klasy w poprzednim przykładzie , to rezultat uruchomien ia programu będzi e następujący :
MÓJ wz rost t o l metr 70 centymetrów
Twój wzrost t o l met r 60 centymet rów
Jego wzrost t o l met r 60 centymetrów
Teraz wynik jest j uż bliższy spodziewanemu. Z danych na wyj ściu widać , że funkcja Wri t e Li ne( ) całkiem dobrze radzi sobie z obiektem na stercie CLR, do którego odnosimy się poprzez uchwyt yourHei ght,jak również z utworzonymi na stosie obiektami myHei ght i hi sHei ght.
Pola Iileralowe Współczynnik 100, które go używali śmy do konwersji metrów na centymetry i odwrotnie , jest trochę problematyczny. Stanowi on przykład tak zwanej ,,magicznej liczby", czyli liczby, której znaczenia lub pochodzenia czytaj ący kod musi się w jak i ś sposób domy śl ić. W tym przypadku oczywiste jest, co znaczy liczba 100, ale w wielu innych przypadkach pochodzenie stał ej liczbowej wcale nie jest takie jasne. W C++/CLI dostępne jest narzędzie zwane polem literaiowym (ang. litera l field) , służące do wprowadzania nazwanych stałych do klasy, co rozwi ązuje ten problem . Poniższy kod prezentuje, w jaki sposób można pozbyć s ię mag icznej liczby z kodu wjednoargumentowym konstruktorze w klasie Heigh t :
val ue class Height
{
private;
II Zap isuje wzros t w metrach i centymetrach.
int met ry;
int centymet ry:
l it eral int cmNaMetr ; 100:
publ i c :
II Tworzenie wzrostu z
warto ści
w centymetrach.
HeightC i nt cm) met ry = cm / cmNaMetr ;
centymetry = cm %cmNaMet r ;
}
II Tworzenie wzrostu z metrów i centymetrów.
Height (i nt m. i nt cm) : metry( m) , centymetry (cm) {} II Tworzen ie reprezentacj i
łańcu chowej
obiektu.
vi rtual Stri ng ToSt ri ng( ) overr ide A
{
Rozdzial7.• Definiowanie własnych typÓW danych. ret urn met ry": L" metr
"+
centymet ry
+
417
L" cent ymetrów";
}
};
Teraz konstruktor używa nazwy cmNaMetr zamiast liczby 100, dzięki czemu nie ma wątpliwości, co się dzieje w kodzie . . Warto ść pola literałowego można zdefiniować w kategoriach innych pól literałowych, pod warunkiem że nazwy pól, których mamy zamiar użyć do określenia tej wartości, zostały zde finiowane pierwsze. Na przykład :
va l ue class Height
{
II Jakiś kod...
l it eral int i nchesPerFoot ~ 12 ;
l it eral double mi l l lmet ersPerlnch = 25.4:
l it eral double mill imet ersPerFoot = inchesPerFoot *mi lli met ersPerl nch:
II Jakiś kod...
W powyższym kodzie zdefiniowaliśmy wartość pola literałowego mi 11 imetersPe rFoot jako iloczyn dwóch pozostałych pól literałowych. Gdybyśmy przenieśli definicję pola mi 11ime tersPe rFoot przed którekolwiek z pozostałych dwóch pól, to kodu nie można by skompilować .
Deliniowanie IYPÓW relerencyjnych Klasa referencyjna ma możliwości podobne do klasy w natywnym C++ oraz nie ma ograni czeń właściwych klasie wartości. W prz eciwieństwie jednak do klasy w natywnym C++, klasa referencyjna nie posiada domyślnego konstruktora kopiującego ani domyślnego operatora przypisania. Jeżeli chcemy, aby nasza klasa obsługiwała który ś z tych operatorów, musimy w tym celu jawnie dodać odpowiedni ą funkcję - jak tego dokonać , dowiemy się w następnym rozdziale . Klasę referencyjną definiujemy za pomocą słowa kluczowego ref c1ass - oba słowa roz dzielone co najmniej jedną spacj ą reprezentują pojedyncze słowo kluczowe. Poniżej znajduje się klasa CBox z programu Cw7_07, zdefiniowana ponownie jako klasa referen cyjna.
Console: :Writ ell neCL"Konst rukt or bezargument owy
z o stał wyw oł any ,
} II Defini cja konstruktora p rzy
użyciu
listy inicj alizacyjn ej .
BoxCdoub le l v. doub le bv. doub le hv ): Length Cl vl , Widt hCbv). Height Chv) Console: :W r it eLineCL" Konst rukt or } II Funkcja
obliczającapojemność pu delka.
double Vol umeC)
zosta ł wywo ł any ." ) ;
") ;
418
Visual C++ 2005. Od podstaw {
return Length*Wi dth*Height:
}
pri vate : double Length: double Width : doub le He ight :
II D/ugość pudełka w centymetrach. II Szerokość pudełka w centym etrach. II Wysokość pudełka w centymetrach.
}:
Warto zwrócić uwagę, że sprzed nazwy klasy usunąłem przedrostek C oraz przedrostek m_ sprzed nazw pól, gdyż notacja ta nie jest zalecana dla klas pisanych w C++/CLI. Dla para metrów funkcji i kon struktorów klas w C++/CLl nie można podawać wartośc i domy ślnych , a więc czynności te w klasie Box wykonać musi konstruktor bezargumentowy. Konstruktor bezargumentowy zainicjalizował wszystkie trzy prywatne pola klasy wartośc iami l . O.
~ Stosowanie typU referencyjnego W poniższym kodzie zastosowano
klasę
Box, o której
mówiliśmy wcześniej.
#l ncl ude "stdafx.h" using names pace System : ref class Box (
} II Definicja konstruktora przy użyciu listy inicjalizacyj nej.
Box Cdou ble lv, double bv, doub le hv) : Lengt hCl v) , Widt hC bv ) , HeightC hv) Conso le : :WriteLi neCL"Konst rukt or II Funkcj a
zos t a ł wywo ła ny .
"):
ob liczają ca pojemnoś ć p udelka.
double Volume()
{
ret urn Lengt h*Wldt h*Height :
}
pri vat e: double Length: double Width: double Height :
II D/ugość pudełka w centymetrach, II Szerokość pudełka w centymetrach. II Wysokość pudełka w centymetrach.
}:
i nt mai nCarray<Sy stem : :Stri ng {
A>
A
args)
Rozdzial7.• Box" asox:
Dełiniowalliewłasnych typÓW danych
419
II Uchwyt typu Bor".
Box newBox ~ gcnew Box(10. 15. 20):
aBox = gcnew Box; II ln icj alizacj a domyślnym i wartościami klasy Box.
Console : :WriteLi ne( L"Domy śln a po jemno ś ć obiektu klasy Box wynos i (O r .
aBox->Vol ume( »;
Console ; ;Wr iteLi net L"Poj emno ś ć nowego obiekt u kl asy Box wynosi (Ol".
newBox- >Vol ume (» ;
ret urn o: A
Rezultat
działania
tego programu jest następujący:
Konst ruk tor z o s ta ł wywoł a ny .
Konst rukt or bezargument owy zosta ł wywo łany .
Domyś lna pojemność ob iekt u klasy Box w ynosi 1
P o j emn o ś ć now ego obiektu klasy Box wynos i 3000
Jak to działa Pierwsza instrukcja w funkcji mai n() tworzy uchwyt do obiektu klasy Box.
Box aBox: A
II Uchwyt typu Box/:
Powyższa
instrukcja nie tworzy żadnego obiektu, tylko uchwyt śledzący aBox. Zmienna aBox zainicjalizowana wartością nul l ptr, a więc nic jeszcze nie wskazuje. Z kolei zmienna typu klasowego zawsze zawiera jakiś obiekt. domyślnie została
Następna
instrukcj a tworzy uchwyt do nowego obiektu klasy Box.
Box newBox = gcnew Box(10 . 15. 20 ): A
Konstruktor przyjmujący trzy argumenty zostaje wywołany w celu utworzen ia na stercie obiektu klasy Box, ajego adres przechowywany jest w uchwycie newBox. Jak wiadomo, obiekty typu re f cl ass zawsze tworzone są na stercie CLR i odnosi się do nich zawsz e za pomocą uchwytów. Tworzymy obiekt klasy Box, wywołując konstruktor bezargumentowy oraz przechowując jego adres w zmiennej aBox.
aBox = gcnew Box: Wartości
Na
II Inicjalizacja za pomocą Box.
pól Length , Wi dt h oraz Hei ght
zakończenie wysyłamy
na
zostają
ustawione na l . O.
wyjście pojemności
obu utworzonych obiektów.
Console: :WriteL ine(L "Domyślna po j emn o ść obiekt u klasy Box wynosi [D}".
aBox->Vol ume(»;
Console: : W r it eL i n e( L "Po jemność obiekt u klasy Box wynos i (Ol " . newBox->Volume(» :
Ze względu na fakt, że aBox i newBox s ą uchwytami, w celu obiektów, do których się odnoszą, używamy operatora - >.
wywołania
funkcji Vo l umeO dla
420
Visual C++ 2005. Od podstaw
Właściwości
klasy
Właściwość
jest s kładową klasy wartości lub klasy referencyjnej, do której dostęp uzyskuje w taki sam sposób jak do zwykłego pola, ale nie jest ona polem . Główną różnicą pomiędzy właściwością i polem jest to, że nazwa pola odno si się do lokalizacji przechowującej dane, a nazwa właściwości nie - wyw ołuje ona funkcję . Wartość właściwości odszukuje się i ustawia za pomocą funkcji dostępowych, odpowiednio get ( ) i set O . A zatem używając na zwy właś ciwości w celu pozyskania jej wartości, tak naprawdę wywołujemy dla niej funkcję dostępową get () , a gdy używamy nazwy właściwości po prawej stronie instrukcji przypisania, wywołujemy funkcję setO. Właściwość definiująca tylko funkcję getO zwana jest właści wością tylko do odczytu, ponieważ funkcja set O, która ustawia wartości, jest niedostępna. Właściwość może mieć także zdefiniowanątylko funkcję set() i w takim przypadku nazywa się wlaściwością tyłko do zapisu. się
Klasa może mieć dwa rodzaje właściwości: właściwości skalarne oraz właściwości indek sowane . Właściwości skalarne to pojedyncze wartości, do których dostęp uzyskiwany jest za pomocą nazwy, natomiast właściwości indeksowane to zbiory wartości, do których dostęp uzyskuje się za pomocą indeksu umieszczanego w kwadratowych nawiasach po nazwie wła ściwości. Klasa St r i ng ma właściwość skalarną Lengt h, która zwraca liczbę znaków w łańcu chu. Dostęp do właściwości Lengt h obiektu klasy St r i ng o nazwie st r uzyskamy za pomocą wyrażenia st r-> Length , ponieważ str jest uchwytem. Oczywiście, w celu uzyskania dostępu do właściwości o nazwie MyProp obiektu klasy warto ści przechowywanego w zmiennej val użylibyśmy wyrażenia val . MyPr op, podobnie jak w przypadku uzyskiwania dostępu do pól. Właściwość łańcucha Length jest przykładem właściwości tylko do odczytu, ponieważ nie ma ona zdefiniowanej funkcji set ( ) - nie można ustawić długości łańcucha, gdyż obiekty klasy Str i ng są niezmienne. Klasa St ri ng pozwala również na dostęp do poszczególnych znaków łańcucha w postaci właściwości indeksowanych. Dostęp do trzeciej właściwości indeksowanej łańcucha o nazwie st r uzyskalibyśmy za pomocą zapisu str [2J, który odpowiada trzeciemu znakowi łańcucha. Właściwości mogą być
skojarzone z określonym obiektem i jemu właściwe. W takim przy egzemplarzy. Właściwość Lengt h obiektu jest przykładem właściwości egzemplarza. Właściwość można także określić słowem kluczowym st at i c, w którym to przypadku właściwość ta będzie skojarzona z klasą, a jej wartość będzie taka sama dla wszystkich obiektów. Przyjrzyjmy się właściwościom trochę dokładniej . padku zwane
są właściwościami
Oeliniowanie właściwości skalarnych Właściwość skalarna ma pojedynczą wartość i definiuje się ją w klasie za pomocą słowa kluczowego propert y. Funkcja get ( ) właściwości skalarnej musi mieć taki sam typ zwracany jak typ właściwości , a funkcja set () musi mieć parametr tego samego typu co właściwość. Poniżej znajduje się przykładowa właściwość w klasie wartości Hei ght , którą widzieliśmy już wcześniej:
val ue class Height
{
pri vate:
II Zapisuje wzrost w stop ach i calach.
l
nt feet. :
Rozdział 7.•
Definiowanie własnych typÓW danych
421
i nt t nches:
l it eral int inchesPerFoot = 12:
l itera l double inchesToMet ers ~ 2.54/ 100;
pub l i c :
II Tworzenie wzrostu z cali.
Height(i nt ins)
{
feet ~ ins 1 inchesPerFoot :
inches = ins % inchesPerFoot:
}
II Tworzenie wzrostu ze stóp i cali.
He ight (int ft . mt t ns: II Wzrost w metrach jako
feet (ft ). tnchesCins ri}
wlasciwos ć .
prope rty double met ers
{
II Zwraca
wartość właściwości.
double get ()
{
ret urn inchesToMete rs*(feet*inchesPerFoot+i nches):
}
II Tutaj znajdowałaby s ię defini cjafunkcji seu) ... II Utwórz
reprezentację łańcuchową
obiektu.
vl rt ual Stri ng ToSt ring( ) override
A
{
ret urn feet + L" feet
" + t nches +
L"
i nches":
}
}:
Od tej pory klasa Hei ght zawiera właściwość o nazwie met er s. Definicja funkcji get () tej właściwości znajduje się pomiędzy nawiasami umieszczonymi po jej nazwie . Moglibyśmy również umieścić tutaj funkcję set () tej właściwości, gdyby taka istniała . Należy zwrócić uwagę , że po nawiasach klamrowych, w których zawiera się definicja funkcji get () i sett ), nie ma średnika . Funkcja get( ) właściwości meter s używa nowej składowej typu literałowego i nchesToMeters, która służy do konwersji wzrostu w calach na wzrost w metrach. Uzyskanie dostępu do właściwości metres obiektu typu Height udostępnia wartość wzrostu w metrach . Poniżej znajduje się przykład:
Hei ght ht = Height (6. 8): II Wzrost 6 stóp i osiem cali.
Console: 'Wr iteLine(L" W zrost wynosi {O} met rów" . ht ->met ers) :
Druga z powyższych instrukcji wyrażenia
wysyła
na wyjście
wartość
obiektu ht w metrach za
pomocą
ht ->meters.
Funkcji getO i set () nie musimy definiować wewnątrz klasy. Definicje ich można umieścić poza definicją klasy w pliku o rozszerzeniu .cpp . Na przykład definicja właściwości met er s w klasie Height mogłaby wyglądać następująco:
value class He ight
{
II Kod jak
wcześniej. ..
publ i c: II Kodjak
wcześniej...
422
Visual C++ 2005. Od podstaw II Wzrost w metrach.
property double meters {
double getO : II Defini cja funk cji sett} II Kod j ak
II Zwraca właś ciwości zn ajdowałaby s ię
wartoś ć właściwości.
tutaj...
wcześn ie). ..
};
Funkcja get ( ) dla właściwości meters jest teraz zadeklarowana, ale jej definicja nie znajduje się w obrębie klasy Hei ght, a więc musimy ją dostarczyć ze źródła zewnętrznego . W definicji funkcji get ( ) w pliku Height .cpp musi znaleźć się odpowiedni kwalifikator w postaci nazwy klasy oraz nazwy właściwości . W związku z tym definicja wygląda następująco:
Height : :met ers; .qet O
{
ret urn inchesToMet ers*(feet*lnchesPerFoot+inches ) :
}
Kwalifikator Hei ght informuje, że funkcja ta należy do klasy Heig ht , a kwalifikator meters, że należy ona do właściwości met ers w tej klasie. Właściwości można oczywiście definiować także przykład
takiej
dla klas referencyjnych.
Poniżej
znajduje się
właściwości :
ref class Wei ght
{
pri vate:
int l bs:
int oz:
pub l te :
propert y int pounds
{
int get O { ret urn l bs: } vord set( int va lue) { lbs = va lue:
}
property i nt ounces
{
i nt get O { return oz; }
void set (int va l ue) ( oz = va l ue:
):
W powyższym kodzie właściwości pound s i ounces umożliwiają dostęp do pól prywatny ch ei ght możemy nadać wartości , a następnie uzyskać do nich l bs i oz. Właściwo ściom obiektu W dostęp w następujący sposób :
W eight wt = gcnew W eight :
wt->pounds = 162:
wt ->ounces = 12:
Console: :Writ eLi ne(L"W eight t s {O} lbs (l) oz.". wt ->pounds. wt->ounces ):
A
Zmienna uzyskująca dostęp do obiektów typu re f cl ass zawsze jest uchwytem , a więc aby uzy skać dostęp do właściwości obiektu typu referencyjnego, należy używać operatora o>.
Rozdział 7••
Definiowanie wlilsnJch tJPÓW danJch
423
Uproszczone właściwości skalarne Można zdefi niować właściwość ska larną
klasy bez podawania definicj i funkcji get ( ) i set ( ) W celu zdefi niowa nia takiej właściwośc i nal eży opuśc ić klamry zaw ierające definicje funkcji get ( ) i set r ) oraz deklaracj ę właściwości zakończyć średnikiem . P on i ż ej znaj duje się przy kład o w a klasa ska larna z uproszczonymi właściwościam i skalarnymi:
nazywa si ę to
u p ro sz c zo n ą właściwością ska larną.
value class Point (
pub l ic: property int x: property int y:
II II
Właśc i woś ć
Właśc iwość
uproszczona. uproszczona.
vlrtual Stri ng ToSt ring() override
A
{
return L"("
+ X+
L". "
+
y
+
L")":
II Zwraca "(x,y)".
}
): Do myś l ne definicje funkcji get( ) i set ( ) s ą dostarczane automatyczni e dla ka żd ej uprosz czonej właściwośc i skalarnej. Zwraca ona wa rtość właściwośc i oraz ustawia j ej warto ść na argument typu okreś lo nego dla właści wości. Przestrzeń prywatna została zaalokowana w celu umieszczenia wartości właści wośc i w miejs cu n i ed o s tępnym z zew nątrz.
Spójrzmy na kilka właści wośc i skalarnych w akcji .
~ Uzywanie właściwości skalarnych W kodzie tym użyte
zostały
trzy klasy - dwie klasy w artośc i oraz jedna klasa referencyjna.
II Cw7_ 16.cpp: main projectfile.
II Używanie właśc iwości skalarnych.
#i ncl ude "st dafx.h" usi ng namespace Syst em: II Klasa
definiująca
wzrost oso by.
va l ue class Height (
pri vate: II Zapisuje wzrost w stopac h i calach.
int feet :
int inches :
l i t eral int inches PerFoot = 12:
l it eral doub le inchesToMet ers = 2.54/100 :
publi c: II Tworzy wzrost z
wartości
zmi ennej inches.
Height (int ins) {
feet = ins / inchesPerFoot :
inches = i ns %inchesPerFoot ;
424
Visual C++ 2005. Od podstaw
II Tworzy wzrost z
wartości
zmiennych feet i inches.
He ight( l nt ft . int ins) : feet(ft). lnche s(in s){} II Wzrost w metrach.
property dou ble mete rs
II
Właściwość
skalarna.
{
II Zwraca
wartość właściwości.
doub le get()
{
ret urn inchesToMeters*( feet* inchesPer Foot+i nches ),
}
II Definicja funkcji sett)
II Tworzenie
łańcuchowej
właściwości znajdowałaby się
tutaj...
reprezentacji obiektu .
virtua l String ToString () override
A
{
return feet + L" stóp "+ inches + L" cale":
}
}: II Klasa
definiująca wagę
osoby.
va lue cl ass Weight {
private : int lbs:
int oz:
lit eral l nt ouncesPerPound = 16:
literal dou ble lbsToKg = 1 0/2.2:
publlC: Wel ght(i nt pounds , i nt ounces) {
lbs = pounds.
oz ~ ounces :
property int pound s
II
Właściwość
skalarna.
II
Właściwość
skalarna.
II
Właściwość
skalarna.
(
i nt get( ) { return lbs: }
voi d set (int value) { l bs = value;
property i nt ounces (
i nt get () ( ret urn oz; }
vOld set (i nt value) { oz = value;
property doubl e kilograms {
double get() { return l bsToKg*C1 bs + oz/ounces PerPoundJ;
Rozdzial7.• Definiowanie własnych typÓW danych vi rtual Str i ng A ToString() override
{ ret urn lbs + L" funtów " + oz + L" uncjl": l
};
II Klasa defin iująca
osobę.
ref class Person (
pri vate:
He ight ht :
We ight wt :
publ i c:
property St ri ngA Name :
II Uproszczo na
wlasc iwosć
ska larna.
eight w) : ht th ) . wt (w)
Person(St ri ng A name . Hei ght h. W (
Name = name: Height getHeight() { ret urn ht :
W ei ght getWeight(){ ret urn wt ;
};
i nt main(array<Syste m: :St ri ng A> Aargs) (
Weight hl sWeight = Weight (185 . 7);
He ight hi sHeight = Height (6. 3) :
PersonA him = gcnew Person(L"F red" , hisHeight . his Wei ght):
W eight herW eight ~ Weight( 105, 3):
Height herHeight = Height(5 . 2) ;
PersonA her ~ gcnew Person(L"Freda ", herHeight , herW eight):
Console; :Wri t el i neCL"To Jest (O}", her->Name): Console : :WriteLi ne(l "Ona waży {O :F2 } kil ogramów. ", her ->get Weight() .ki lograms ): Console : :WriteLi ne(l "Ma wzrostu {O}. czyli {l:F2} metrów." . her->get Height() ,her->getHe ight( ) .met ers) : Console : :WriteLi ne(L"To jest (Ol ", him->Name) ;
Console: :Wri t eL i ne(l"On waży (O} ," , him->get W eight () :
Console: :WriteLine(L"Ma wzrostu {O} . czyl i {l :F2} met rów." ,
him- >getHeight ( ),him- >getHeight () .meters): ret urn O:
Wynik dział ania tego programu jest następujący: To jest Freda
Ona waży 47,73 kilogramów .
Ma wzrostu 5 stóp 2 cale , czyli 1,57 met rów .
To j est Fred
On waży 185 funt ów 7 uncJ i .
Ma wzrostu 6 st óp 3 cale , czyli 1.91 met rów .
425
426
Visual C++ 2005. Od podstaw
Jak to IJziala Dwie klasy wartości Hei ght i Wei ght d efin iują wzrost i wagę osoby. Klas a Person ma dwa pola typu Height i Weight, które przechowują wzrost i wagę osoby . Imi ę osoby przechowy wane j est w uproszczonej właściwości Name, ni eposiadającej jawnej definicj i funkcji get() i set ( ). W związku z tym właściwość ta posi ada domyślne funkcje get ( ) i set ( ). Dwie pierwsze instrukcje w funkcji mai n() definiują obiekty Hei ght i Widt h, za rych następnie defin iujemy mężczyznę - hi m:
pomo cą
któ
Wei ght hi sWei ght = WeightC185 . 7) :
He i ght hi sHei ght = Hei ght C6. 3);
Person" hi m = gcnew Per sonCL"Fred". hi sHei ght . hi sWei ght ) ,
Hei ght i Wei ght są klasami wartości , a więc zmienne tych typów przechowują w arto ści bez p ośrednio . Perso n jest kl asą referencyjną, a więc h i mjest uchwytem. Pierwszy argument prze kazywany do konstruktora klasy Person jest literałem łańcuchowym, a więc komp ilator tworzy z niego obiekt klasy Str i nq, który następnie przek azuje jako argument. Drugi i trzec i argu
ment to obiekty klasy wartości i tworzymy je w dwóch pierwszych instrukcjach. O czywiście ich kopie są przesyłane jako argumenty ze względu na mechanizm przekazywania przez wartoś ć dla argumentów funkcji . Wewn ątrz konstruktora klasy Perso n przypi sanie ustaw ia w artość parametru Name, zaś wart o ści dwóch pól ht i wt są ustawiane za pomocą listy inicj alizacyjnej. Jedynym sposobem ustawienia wła ściwo ści jest niejawne wywołanie jej funkcji set ( ) . Wła ś c i w oś ć nie może b yć inicj alizowan a w li ście inicjal izuj ąc ej konstruktora. Podobne trzy instrukcje jak dla obiektu him zostały napisane dla obiektu her. Spośród dwóch obiektów klasy Per son, znajduj ących się na stercie, najpierw wysyłamy na ekran informacje o niej Cher ) za pomocą poniż szych instrukcji: Console :Wr iteLi neCL"To je st ( O)" . her ->Name);
Console : :Wr iteLi neCL "Ona wa ży {O :F2} ki log r amów .".
her ->get Wel ght C) .ki l ogr ams) ;
Console :Wr iteLi neCL"Ma wzrostu (O} . czy l i { l:F2} metrów. " .
her ->get Hei ght C) .her ->get Hei ght C) .met er s) :
W pierwszej instrukcji uzyskujemy dostęp do właściwości Name obiektu wskazywanego przez uchwyt her za pomocą wyrażeni a her - >Name. W rezultacie otrzymujemy uchwyt do łańcucha zwróconego przez funkcję właściw o ści get( ) , a więc do łańcu ch a typu St ri nq". W drugiej instrukcji uzyskujemy d o stęp do właściwości ki l ogr ams pola wt obiektu wska zywanego prz ez her za pomocą wyrażenia her ->getWei ght ( ) . k i l ogr ams. C zę ść wyrażenia her ->getWei ght zwraca kopię pola wt i s łuży do uzyskania dostępu do właściwoś ci ki l ogr ams. W ten sposób wartość zwrócona przez funkcję get () dla właściwo ś ci kil ogr ams staje się war tością drugiego argumentu funkcji Wr i t el i neO . W trzeciej instrukcji wyjściowej drugi argument stanowi wynik wyrażenia her ->get Wei ght O, które zwraca kopię pola hL Aby odpowiednio dostosować dane wyjściowe, kompilator wywo łuje funkcję ToSt ri ng () dla obiektu, dzięki czemu wyrażenie to jest równoznaczne z wyraże niem her- >get Wei ght ( ) .ToSt ri ng( ), wi ęc - jeśli chcemy - to możemy to sami zapisać w ten sposób. Trzecim argumentem funkcji Wr i t el i ne( ) jest właściwo ść meters obiektu Hei ght, która jest zwracana przez funkcję get Hei ght ( ) zastosowaną do obiektu her klasy Per son.
Rozdział 7.•
Deliniowanie własn~ch
I~pów dan~ch
427
Pozostałe trzy instrukcje wyjściowe wysyłają na wyjście informacje o obiekcie him w podobny sposób jak przy her. W tym przypadku waga osoby została utworzona za pomocą niejawnego wywołania funkcji ToSt r i ng( ) dla pola wt obiektu hi m.
Definiowanie właściwości indeksowanych Właściwości
indeksowane to zbiór wartości właściwości w klasie, do których dostęp uzyskuje za pomocą indeksów podawanych w nawiasach kwadratowych, podobnie jak w przypadku elementów tablicy. Do tej pory używaliśmy właściwości indeksowanych łańcuchów, ponie waż klasa Stri ng udostępnia znaki łańcucha w postaci właściwości indeksowanych . Jak już się orientujemy, jeżeli st r jest uchwytem do obiektu klasy Str i ng, to wyrażenie str [4] daje dostęp do wartości piątej właściwości indeksowanej, co odpowiada piątemu znakowi w łańcuchu . Właściwość, do której dostęp uzyskuje się poprzez podanie indeksu w nawiasach kwadra towych po nazwie zmiennej odnoszącej się do obiektu, nazywa się właściwością indeksowaną domyślną. Właściwość indeksowana posiadająca nazwę zwana jest nazwaną właściwością się
indeksowaną,
Poniżej
znajduje
się
klasa
zawierająca domyślną właściwość indeksowaną:
ref class Name (
pri vat e: II Przechowuje imiona jako elem enty tablicy. array<Stri nq" >" Names ; public: Name( .ar ray- St.r i nq">" nenes ) Names(names) {} II
Wiasciwos ć
indeksowana zwracająca dowolne
imię.
property Str i ng default [i nt] A
( II Odzyskuj e
wartość
wlasciw osci indekso wanej.
Stri ng get (i nt i ndex) A
(
i f (i ndex >~ Names->Lengt h) t hrow gcnew Excepti on (L"Za ret urn Names[i ndex] ;
duż y
l ndeks");
l: Przeznaczeniem klasy Name jest przechowywanie imion osób w postaci tablicy. Konstruktor przyjmuje arbitralną liczbę argumentów typu St r tnq", które następnie zapisywane są w polu Names, dzięki czemu obiekt Name może zawierać dowolną liczbę imion . Właściwość
indeksowana w tym przypadku to domyślna właściwość indeksowana, ponieważ jest określone za pomocą słowa kluczowego default. Gdybyśmy w tym miejscu podali wprost imię, to byłaby to właściwość indeksowana nazwana. Nawiasy kwadratowe znajdujące się po słowie kluczowym def ault wskazują, że rzeczywiście jest to domyślna właściwość indeksowana, a znajdująca się pomiędzy nimi nazwa typu - w naszym przypadku i nt określa typ wartości indeksów , którego należy użyć podczas poszukiwania wartości właści wości. Typ indeksu nie musi być liczbowy, a w celu uzyskania dostępu do wartości właściwo ści indeksowanych można mieć więcej niżjeden parametr indeksowy. imię
428
Visual C++ 2005. Od podstaw Jeżeli
do właściwości indeksowanej dostęp uzyskuje się za pomocą pojedynczego indeksu, funkcja get ( ) musi mieć parametr określający ten indeks, który jest takiego samego typu co typ podany w nawiasach kwadratowych po nazwie właściwości. Funkcja set() w takim przy padku wymaga dwóch parametrów: pierwszym jest indeks, a drugim nowa wartość, na którą ma zostać ustawiona właściwość o indeksie podanym w pierwszym parametrze. Przyjrzyjmy
się właściwościom
indeksowanym w praktyce.
~ Uzywanie domyślnei właściwości indeksowanei W poniższym kodzie
wykorzystałem
i nieco
rozszerzyłem klasę
Name :
#lncl ude "stdafx.h" using namespace System : ref class Name (
privat e:
array<StringA>A Names:
publ i c:
Name ( . . .array<Strin gA>A names) II
Właś ciwość
skalarna
Names (names ) {}
określająca liczbę
imion.
property int NameCount
{
int get () {ret urn Names->Length : }
}
II
Właściwość
indeksowana zwracająca imiona.
property Str ingA default[ int J (
St ri ngA get(int i ndex)
(
if(i ndex >= Names->Length )
throw gcnew Exception (L "Za return Names[indexJ:
d u ży
indeks "),
}
}:
int ma ln(arr ay<System: :Stri ng A> Aargs) [
Name A myName
~
gcnew Name(L"Ebenezer". L"lsaiah". L"Ezra".
l.'Tru ęo" .
L"Whelkwh i stle"). II Tworzenie listy imion.
for(int i = O : i < myName->NameCount ; i++ ) C ons ol e: :Writ eLin e(L"lmię numer {O } to {l )". i+l, myName[i]); ret urn O;
Rozdział 7.
Rezultat
działania powyższego
numer numer numer numer numer
Imi ę Imi ę I mi ę I mi ę
I mi ę
l 2 3 4 5
to to to to to
• Deliniowanie własnych typÓW danych
429
programu jest następujący :
Ebenezer
Isai ah
Ezra
Inigo
W hel kwh i st le
Jak lo działa Klasa Name w tym przykładzie zasadniczo niczym się nie różni od swojej poprzedniej wer sji. Jedyną różnicąjest dodana właściwość skalarna o nazwie NameCount , która zwraca liczbę imion w obiekcie Name. W funkcji mai n( ) najpierw tworzymy obiekt Name zawierający pięć imion: Name myName
Lista parametrów konstruktora klasy name zaczyna się od elipsy, a więc konstruktor przyj muje dowolną liczbę argumentów. Argumenty podane podczas wywoływania konstruktora przechowywane będą w tablicy names. W związku z tym inicjalizacja pola Names elementami tablicy names powoduje, że pole Names odnosi się do tablicy names. W poprzedniej instrukcji do konstruktora przekazaliśmy pięć argumentów, a więc pole Names obiektu, który wskazuje uchwyt myName, jest tablicą pięciu elementów. Dostęp listę
do właściwości obiektu myName uzyskujemy w imion zawartych w obiekcie:
pętli
f or , za
pomocą
której tworzymy
for (i nt i ~ O ; l < myName->NameCount ; i++ )
Console : :WriteLi ne(L"Name {O} t s ( l }" . i +1. myName[i]) :
Pętlę
pomocą wartości właściwości NameCount . Bez niej nie wiedzieli ile imion powinno zostać wyświetlonych. Ostatni argument funkcji Wr iteLi ne() we wnątrz pętli uzyskuje dostęp do właściwości indeksowanej o indeksie i . Jak widać , uzyskanie dostępu do domyślnej właściwości indeksowanej wymaga jedynie podania indeksu w nawia sach kwadratowych po nazwie zmiennej myName. Z danych na wyjściu wynika, że właściwości indeksowane działają należycie .
for kontrolujemy za
byśmy,
indeksowana jest tylko do odczytu, ponieważ klasa Name zawiera tylko get () dla tej właściwości . Aby umożliwić zmianę właściwości, możemy dodać defi funkcji set () dla domyślnej właściwości indeksowanej, jak poniżej:
Nasza
właściwość
funkcję nicję
ref class Name
{
II Kodjak II
wcześniej. ..
Właściwość
indeksowana
zwracają ca
imiona.
property String default [ int ] A
(
Stri ng get (int index) A
(
if(index >~ Names->Length) throw gcnew Exception(L" Za retur n Names[i ndex] ;
du ży
indek s") ;
430
Visual C++ 2005. Od podstaw void set(i nt index. St r ing name ) A
(
if( i ndex >~ Names- >Length)
th row gcnew Except ion(L"Za Names[index] = name ;
d u ży
indeks") :
}: Mając możliwość ustawić wartość
ustawiania warto ści właściwości indeksowanych, w funkcji mai n( ) ostatniej właś ciwości indeksowanej:
możemy
NameA my Name = gcnew Name(L"Ebenezer ". L"Isaiah" . L"Ezra".
L"Ini go". L" Whel kwhi st l e") ;
myName[myName->NameCount - 1] = L"Oberwur st" : II Zmian a ostatniej właś ciwości indekso wanej.
II Tworzenie listy imion.
for (i nt i ~ O ; i < myName->NameCount . i++)
Console : :WriteLi ne ( L"Imi ę nume r {O} to (l)" . i+l. myName[i ]) ;
Dodając
ten fragment kodu w danych wyjściowych nowej wersji programu, zobaczymy, że zaktualizowane przez ostatnią instrukcję przypisującą war właściwości w indeksie lTIyl~ a me ->NameCount -1.
ostatnie tość
imię rzeczywiście zostało
Możemy także dodać
do klasy
nazw aną właściwość indeksowaną:
ref class Name ( II Kod j ak poprzednio... II
Właściwość
indekso wana
zwracająca inicjały.
property wcha r_t Init i als[ i nt] (
wcha r_t get (lnt index) (
l f (index > ~ Names->Lengt h)
throw gcnew Exception(L"Za ret urn Names[i ndex][O]:
d u ży
indeks");
}; Właściwość
indeksowana ma nazwę Ini tial s, ponieważ zwraca ona pi erwszą literę imienia przekazanego za pomocą indeksu. Nazwaną właściwość indeksowaną przekazuje się w podob ny sposób jak właściwość indeksowanądomyślną, ale zamiast słowa kluczowego def ault stawiamy nazwę właściwości. Ponowne skompilowanie i uruchomienie programu da I mi ę
I mi ę I mi ę Imi ę
I m ię
numer numer numer numer numer
1 2 3 4 5
to to to to to
Ebenezer Isaiah Ezra Inigo Oberwurst
Irucja ł y : E. 1. E. 1. O.
następujący
rezultat:
Rozdział 1.
• Definiowanie własnych typÓW danych
431
Inicjały z o s tały wy świetlone dz ięki
w
pętli
uzyskaniu dostępu do nazwanej właściwo śc i indeksowanej for. Z danych na ekranie wynika, że wszystko d zi ała jak n ależy.
Bardziei zlożone wlaściwości indeksowane Jak ju ż
w spomin ałem , właściwo ści
indeksowane można zdefiniow a ć w taki sposób, że aby trzeba poda ć więcej ni ż jeden indeks oraz indeksy te muszą być liczbami. Poni żej znajduje się przykładowa klasa zawi erająca takie właściwosci: uzy skać dostęp
enumclass II Klasa
do ich
wartości ,
Day { Pon i edz i a ł e k .
defin iują ca
wtorek.
Ś rod a .
Czwa rte k.
P i ąte k .
Sobot a. Niedziela} :
sklep.
ref class Shop
{
publ ic:
property Str i ng A Dpening[Day . St ringAJ {
II Godziny otwarcia sklepu.
St ri ng A get (Day day. St ringA AmOrPm)
{
switch(day)
(
case Day: :Sobota: lf (AmOrPm == l "rano") ret urn l" 9:00": el se ret urn l "14 :30 ": break: case Day: ' Nl edzie la : ret urn l "zamkniet e": break: defau lt : i HAmOrPm = = l" rano ") ret urn l "9:30": else ret urn l "14:00" : break:
II Godziny otwarcia w sobotę : II rano j est od 9:00. II popoludnie zaczyna s ię o 2:30. II Godziny otwarcia w n iedzielę : II zamkn ięte cały dzień .
II Godziny otwarcia II od pon iedziałku do piątku : II rano j est od 9:30. II popołudnie zaczyn a s ię 2.00.
};
W klas ie repre zentującej sklep znajduje się właściwość indeksowana określ ająca god ziny otwarcia sklepu. Pierwszy indeks jest wartością wyliczeniową typu Day identyfikującą dzień tygodnia, a drugi jest uchwytem do łańcucha określającego , czy jest rano, czy wieczór. War tość właściwoś ci Openi ng obiektu Shop możemy wysłać na wyjście w następujący sposób :
ShopA shop ~ gcnew Shop :
Consol e: :Writ el i net shopc-Cpent ng[Day: :Sobota . l "po
po ł u d n i
u"J) :
Pierwsza z tych instrukcji tworzy obiekt o nazwie Shop, a druga wyświetla godziny otwarcia sklepu w sobotę po połudn iu . Jak widać , obie wartości indeksowe stawia się w nawiasach kwa dratowych i rozdziela przecinkiem. Rezultatem pierw szej instrukcji jest łańcuch " 14:30".
432
Visual C++ 2005. Od podstaw Jeżeli
potrafisz zn aleźć dla nich zastosowanie, możesz w klasie zd efiniow ać indeksowan e z trzema indeksami lub nawet więk s z ą ich liczbą.
wo ści
także właści
Właściwości statyczne Właściwości
statyczne są podobne do statycznych składowy ch klasy , ponieważ również deje dla klasy i są takie same dla wszystkich obiekt ów tej klasy. Aby zdefiniowaś, właściwoś ć jako staty cz ną, należy do j ej definicji dodać słowo kluc zowe st at i c. Poniżej znajduje się przykładowa definicja właściwości statycznej w klasie Length , którą widzieliśmy się
finiuje
ju ż wcześniej :
val ue cl ass Lengt h { II Kod jak poprzednio...
publ ic: stat i c property
S t r i ng
Ą
Units
( St ri n g
Ą
get () { ret urn l"met ry i cent ymet ry": }
}
}:
Jest to prosta właściwość statyczna, która udostępnia jednostki przyjmowane przez klasę jako łańcuchy . Do stęp do właściwości statycznej uzyskujemy, dod ając do jej nazwy kwalifikator w postaci nazwy klasy, podobnie jak do każdej innej statycznej składowej klasy : Consol e : :Wr iteline(L" Jednostk i w kl asi e: (O}. " . l engt h: .Uni t s ) :
Statyczne właściwośc i klasy i stnieją bez względu na fakt, czy z o s tały utworzone jakie ś jej obiekty, czy nie. Odróżnia je to od właściwości egzemplarzy, które są specyficzne dla każdego obiektu typu klasowego. Oczywiście, mając zdefiniowany obiekt klasy, dostęp do właści wo ści statycznej można uzyskać przy u życiu nazwy zmiennej. Mając na przykład obiekt klasy Length o nazwie l en, wartość właściwości statycznej Uni ts możemy wysłać na wyjście za pomocą następującej instrukcji : Console: :Wr i t el i ne(L"Jednostki klasy (O}.". l en .Um t s ) :
W celu uzyskania dostępu do właściwości statycznej w klasie referencyjnej poprzez uchwyt do obiektu tego typu należałoby użyć operatora - > .
Zarezerwowane nazwy właściwości Mimo że właściwo ści nie są tym samym co pola, ich wartości muszą jednak być gdzieś przechowywane, a zatem musi być jaki ś sposób określania lokalizacji tych danych. Wewnątrz właściwości tworzone są nazwy dla potrzebnych lokalizacji przechowujących dane . Nazwy te są zarezerwowane w klasie zawierającej te właśc iwo ści , a więc nie można ich używać do innych celów . Jeżeli
w klasie zdefin iujemy
będzie używa ć
właściwo ść skalarną lub indeksowaną o
nazwie NAM E, to nazwy zarezerwowane w tej klasie , w związku z czym nie można ich do innych celów. Obie nazwy są zarezerwowan e bez względu na to, czy obie
get_NAME oraz set _NAME
będą
Rozdział 7. •
Definiowanie własnych typÓW danych
433
funkcje właściwości zostały zdefiniowane. Po zdefiniowaniu domyślnej właściwości indeksowej w klasie zarezerwowane zostają nazwy get_ It emoraz set _Item. Prawdopodobieństwo występowania zarezerwowanych nazw zawierających znak podkreślenia jest ważnym powodem do unikania tego znaku we własnych nazwach w programach C++/CLI.
-
Pola inilonly
Pola literałowe są wygodnym sposobem wprowadzania stałych do klasy , ale ograniczone są faktem, że ich wartość musi być znana w momencie kompilacji programu. W klasach w C++/ CLI dostępne są pola i niton1y, które są zmiennymi inicjalizowanymi w konstruktorze. Poniżej znajduje się przykładowe pole i niton1y w szkieletowej wersji klasy Length: va l ue class Length (
pri vat e: int feet : int inches : publ te : i nit only i nt inchesPerFoot :
II Pole initonly .
II Konstruktor. Length(i nt ft .
i nt ins ) : feet(ft) . i nchest t ns ) . inchesPerFoot (12)
II Inicj a/iza cj a p ól. II Ini cj a/iza cja p ola initonly.
{} }:
W tym kodzie pole i nit on1y nosi nazwę i nchesPerFoot i jest inicjalizowane w liście inicjalizującej konstruktora . Mamy tutaj przykład niestatycznego pola i niton1 y. Każdy obiekt będzie miał własną kopię, podobnie jak w przypadku zwykłych pól, feet oraz i nches. Oczywiście największą różnicą pomiędzy polami typu i nit on l y i zwykłymi polami jest to, że nie można zmienić wartości pola i nit on1y - jest ona stała od momentu jej inicjalizacji . Należy zwrócić uwagę, że nie można podać wartości początkowej dla niestatycznego pola i niton l y podczas jego deklaracji. Oznacza to, że wszystkie tego typu pola muszą być inicjalizowane w konstruktorze. Niestatycznych pól i ni t on1y nie musimy można to zrobić w jego ciele: Length(i nt ft. i nt i ns) feet ( ft), i nches(i ns ) .
inicjalizować
w
liście
inicjalizacyjnej konstruktora -
II Inicj a/izacja pól.
{
inchesPerFoot
=
12:
II Inicja/izacja pola initonly.
}
W powyższym fragmencie kodu pole zostało zainicjalizowane w ciele konstruktora. Normalnie niestatycznego pola i niton1 y przekazalibyśmy do konstruktora jako argument zamiast w postaci literału, jak zrobil i śmy tutaj, ponieważ najważniejszą cechą takich pól jest to, że są one specyficzne dla danego egzemplarza. Jeżeli podczas pisania kodu znana jest wartość, to równie dobrze można użyć pola literałowego .
wartość
434
Wisual C++ 2005. Od polista w Pole i ni t onl y w klasie można również zdefiniować jako statyczne, w którym to przypadku jest dostępne dla wszystkich składowych klasy i jeżeli jest ono zarazem publiczne, to dostęp do niego można uzyskać, dodając do jego nazwy kwalifikator w postaci nazwy klasy. Pole i nchesPerFoot byłoby o wiele bardziej przydatne, gdyby było statycznym polem i nitonly - jego wartość na pewno nie powinna zmieniać się w różnych obiektach. Poniżej znajduje Się nowa wersja klasy Length z użyciem statycznego pola ini to nly:
va l ue class Length {
privat e: int feet : i nt inches: publte : i ni t only stati c int i nchesPerFoot
=
12:
II Staty czne p ole initonly .
II Konstruktor.
Length(i nt ft . i nt i ns ) : feet (ft) . inches(i ns )
II Inicjaliza cja pól.
{}
}:
Teraz pole i nchesPerFoot jest statyczne, a jego wartość została określona w deklaracji, a nic w liście inicjalizacyjnej konstruktora. Należy pamiętać , że w konstruktorze nie można ustawiać wartości pól statycznych. Zastanawiając się nad tym trochę głębiej , dojdziemy do wniosku, że jest to całkiem logiczne, ponieważ pola statyczne są dostępne dla wszystkich obiektów klasy i w związku z tym ustawianie wartości takiego pola za każdym razem, gdy wywoływany jest konstruktor, nie byłoby na miejscu. Wygląda
na to, że mamy z powrotem pola i nitonly, które można inicjalizować tylko podczas procesu kompilacji, chociaż równie dobrze tę czynność mogłyby wykonać pola literałowe. Istnieje jednak jeszcze jeden sposób inicjalizowania statycznych pól i nitonl y w trakcie działania programu - poprzez konstruktor statyczny.
Konstruktor statyczny Konstruktor statyczny deklaruje się za pomocą słowa kluczowego stat ic. Jego przeznaczeniemjest inicjalizacja statycznych pól i statycznych pól initonly. Konstruktor statyczny nie ma żadnych parametrów i nie może mieć listy inicjalizacyjnej . Konstruktor statyczny jest zawsze prywatny, bez względu na to, czy znajduje się w publicznej, czy prywatnej części klasy. Konstruktor statyczny można definiować dla klas wartości i klas referencyjnych. Konstruktora statycznego nie można wywołać bezpośrednio - jest on wywoływany automatycznie przed wywołaniem zwykłego konstruktora. Wszystkie pola mające wartości początkowe określone w ich definicjach są inicjalizowane przed wykonaniem konstruktora statycznego. Poniżej znajduje się przykładowa inicjalizacja pola i nit onl y w klasie Length za pomocą konstruktora statycznego:
value class Length {
privat e : int feet : i nt inches :
Rozdział 7.•
Deliniowanie własnych typÓW danych
435
II Konstruktor statyczny.
st at ic Lengt h() ( inchesPerFoot
=
12:
publ i c:
init only sta t ic int inchesPerFoot : II Konstruktor.
...
II Statyczne p ole initonly.
Length(int f t , int i ns ) feet r f't) . i nchest tn s)
II Jnicjalizacja p ól.
{}
Użycie konstruktora statycznego w tym przykładzie nie jest w niczym lepsze od jawnej inicjalizacji pola i nc hesPe rFoot , ale należy pamiętać o znaczącej różn icy - teraz inicjalizacja odbywa się w trakcie działania programu, dzięki czemu wartość może zostać wprowadzona ze źródła zewnętrznego .
na temat kla s w języku C++ . Do końca książki o nich coraz więcej . Poniżej znajduje się lista najważniejszych poruszanych w tym rozdziale:
b ędziemy dow iadywać się zagadnień
•
Klasa umożl iwia definiowanie własnego typu danych. Może ona odzwierciedl ać dowolny typ obiektów, których wymaga ro związanie danego problemu .
•
Klasa może zawierać zmienne składowe i funkcje składowe. Funkcje składowe klasy mają zawsze wolny dostęp do zmiennych składowych tej samej klasy. Zmienne składo we klasy w CH/CLI nazywają się polami.
•
Obiekty klasy tworzy s ię i inicjalizuje za pomocą funkcji zwanych konstruktorami. Wywoływane są one automatycznie w momencie napotkania deklaracji obiektu. Konstruktory można przeładowywać w celu uzyskania obiektów inicjalizowanych na różne sposoby.
•
Klasy w programach w C++/CLI mogą być klasami lub klasami referencyjnymi (ang. refclasses).
•
Zmienne typu klasy wartości przechowują dane bezpośrednio , natomiast zmienne odnoszące się do obiektów należących do klasy referencyjnej są zawsze uchwytami.
•
Dla klasy w C++/CLI można statyczne składowe klasy.
•
Składowe klasy można określić jako publiczne (p ubl i c) i wtedy są do stępne dla wszystkich funkcji w programie. Można także określić je jako prywatne (pr i vate) i wtedy dostęp do nich mają tylko funkcje składowe lub zaprzyjaźnione klasy .
•
klasy można zdefiniować jako statyczne. Istnieje tylko jeden egzemplarz statycznej, który jest współdzielony przez wszystkie egzemplarze klasy, bez względu na liczbę utworzonych jej obiektów.
Składowe
każdej składowej
zdefiniować
wartości
(ang . value classes)
konstruktor statyczny
inicjalizujący
436
Visual C++ 2005. Od podslaw •
Każdy
•
W niestatycznych funkcjach składowych typu klasy wartości wskaźnik th i s jest wskaźnikiem wewnętrznym, natomi~st w klasie referencyjnej jest on uchwytem.
•
Funkcja składowa zadeklarowana jako const posiada wskaźnik const t his, a zatem nie może modyfikować składowych obiektu klasy, dla którego została wywołana . Funkcja ta może wywoływać tylko te funkcje składowe, które zostały zadeklarowane jako const .
•
Dla obiektu klasy zadeklarowanego jako const składowe zadeklarowane jako const.
•
Funkcji składowych klas jako const .
•
Stosowanie jako argumentów funkcji referencji do obiektów pozwala na zaoszczędzenie czasu przy przekazywaniu do funkcji złożonych obiektów.
•
Parametr konstruktora kopiującego, który jest konstruktorem obiektu zainicjalizowanego istniejącym obiektem tej samej klasy, musi być określony jako referencja typu const .
niestatyczny obiekt klasy zawiera dla którego została wywołana funkcja.
wartości
wskaźnik
th i s,
wskazujący bieżący
można wywoływać
oraz klas referencyjnych nie
obiekt,
tylko funkcje
można deklarować
• ' W klasie wartości nie można zdefiniować konstruktora kopiującego, ponieważ kopiowanie obiektów klasy wartości zawsze odbywa się metodą pole po polu.
Ćwiczenia Kod źródłowy wszystkich listingów w tej ze strony www.helion.pl.
l
książce
oraz rozwiązania do ćwiczeń
można pobra ć
o nazwie Sampl e zaw ierającą dwie jednostki danych typu Napisz program deklarujący dwa obiekty typu Sampl e o nazwach a i b. Ustaw wartości dan ych należących do obiektu a, a następnie sprawdź, czy można je przekopiować do b za pomocą prostego przypisania.
Zdefiniuj
s t ruktu rę
całkow itego.
2. Do struktury Sampl e z poprzedniego
ćwiczenia dodaj składową typu char* o nazwie s Ptr. Po wprowadzeniu danych do a utwórz dynamicznie bufor łańcuchowy zainicjalizowany łańcuchem "Witaj świecie! " oraz ustaw wskaźnik a . sPt r na ten łańcuch . Skopiuj a do b. Co się dzieje, gdy zmieniasz zawartość bufora znakowego wskazywanego przez wskaźnik a . s Pt r, a następnie wysyłasz na ekran zawartość łańcucha wskazywanego przez b. sPt r? Wyjaśnij, co się dzieje. Jak można to obejść?
8. Utwórz
funkcję, którajako argument przyjmuje wskaźnik do obiektu klasy Sample oraz wysyła na ekran wartości składowych przekazanego do niej w ten sposób obiektu klasy Samp l e. Przetestuj tę funkcję , rozszerzając program stworzony w poprzednim ćwiczen iu .
Rozdział 7. •
Definiowanie własnych typÓW danych
437
.. Zdefiniuj klasę o nazwie CRecord z dwiema składowymi prywatnymi przechowującymi imiona o długości do 14 znaków oraz liczbę całkow itą. Zdefin iuj funkcję składową klasy CRecor d o nazwie getRecord( ), która będzie ustawiała wartości składowych , wczytując dane z klawiatury , oraz funkcję składową put kecordt ), aby wywołujący program mógł wykryć, kiedy została psdana liczba o warto ści zerowej . Przete stuj swoją klasę w funkcji mai nt ), która wczytuje i wysyła na wyj ście obiekty klasy CReco rd do momentu podania liczby zerowej.
I. Napisz
klasę o nazwie CTrace, której można u żyć do wy świetlania w trakcie wykonywania programu, kiedy następuje wej śc ie do poszczególnych bloków i wyj ście z nich. Klasa powinna wysyłać na ekran komunikaty podobne do poniższych :
do funkcj i 'fI' do bloku ' if ' wyjśc i e z bloku 'if' wyj śc ie z f unkcj i 'fI ' we jś cie
wejśc i e
8.
Znajdź sposób na automatyczną kontrolę wcinania wierszy w poprzednim ćwiczeniu, tak aby dane na ekranie prezentowały się następująco:
do funkcji 'fl' do bloku ' ; f ' wyj ści e z bloku ' if' wyjśc ie z f unkcji ' fl ' weJ ś c ie
wejście
7. Zdefiniuj klasę reprezentującą stos liczb całkowitych . Stos ten jest zbiorem elementów pozwalającym
na dodawanie i usuwanie elementów tylko z jednej strony oraz dz iała na zasadzie "pierwszy do, ostatni na zewnątrz". Jeżeli na przykład stos zawiera liczby 10,4 , 16,20, funkcja pop() zwróciłaby 10, a stos zawierałby lic zby 4, 16, 20. Uruchomienie pusht IS) dałoby w wyniku stos 13,4, 16,20. Aby dostać się do elementu, który nie jest na samej górze, należy wpierw usunąć wszystkie znajdujące się nad nim elementy. W klasie powinny być zaimplementowane funkcje pop ( ) i push( ) oraz funkcja pr i nt( ) do sprawdzania zawartości stosu. Listę elementów przechowuj wewnętrznie w postaci tablicy. Napisz program sprawdzający, czy klasa działa poprawnie.
.. Co się stanie z Twoim rozwiązaniem z poprzedniego ćwiczenia, gdy spróbujesz za pomocą funkcji pop( ) usunąć ze stosu więcej elementów, niż on zawiera? Potrafisz znaleźć dobre wyjście z tej sytuacji ? Czasami przydałaby się funkcja pozwalająca na dostęp do liczby na samej górze bez jej usuwania. Zaimplementuj do tego celu funkcję peek( ). .. Powtórz ćwiczenie 4., ale jako program konsolowy CLR z referencyjnych.
użyciem
klas
438
Visual C++ 2005. Od podsław
...
8
Więcei na temat klas
W rozdziale tym poszerzymy wiedzę na temat klas. Dowiemy się , jak można sprawić, aby zachowanie obiektów było bliższe zachowaniu typów podstawowych w C++. W rozdziale tym dowiesz się : • Czym
są destruktory
klas oraz kiedy i do czego
są one
potrzebne.
• W jaki sposób implementowany jest destruktor klasy. • Jak w wolnym obszarze przydzielać pamięć składowym klas w natywnym c++ oraz jak je usuwać , kiedy nie sąjuż potrzebne. • Kiedy zachodzi
konieczność
napisania konstruktora
kopiującego
klasy.
• Czym są unie i do czego służą. • Jak
sprawić,
• Czym • Jak
aby obiekty klasy
są szablony
działały
z operatorami C++, takimi jak + czy
*.
klas oraz jak się je definiuje i ich używa .
przeładowywać
operatory w klasach w C++/CLI.
Destruktory klas Mimo że w tytule napisane jest "destruktory klas", podrozdział ten poświęcony jest również dynamicznemu przydzielaniu pamięci. Przydzielając pamięć składowym klasy w obszarze pamięci wolnej, jesteśmy zobowiązani do użycia destruktora, oczywiście w połączeniu z kon struktorem. Poza tym - jak dowiemy się później w tym rozdziale - dynamiczne przydzie lanie pamięci składowym klasy pociąga za sobą konieczność napisania własnego konstruktora kopiującego.
440
Visual C++ 2005. Od podstaw
Czym iesl deslruklor
Destruktor to funkcja ni szcząca obiekt, kied y nie jest już potrzebny lub znajdzie s i ę poza zas ięg iem . Destruktor j est wywoływany automatycznie w chwili, gdy obiekt znajduje s i ę poza zasięgiem . Niszczenie obiektu polega na zwolnieniu pamięci zajmowanej przez jego s kładowe (z wyjąt k ie m składowych statyczn ych , które is t n i ej ą, nawet gdy nie istni ej ą ż a d n e obiekty klasy ). Destruktor danej klasy jest jej funk cją s kła d ową o takiej samej nazw ie jak ona, kt órą charakteryzuje znajdujący się z przodu znak tyldy (-). Destruktor klasy nie zwraca żadnej war tości i nie ma zdefiniowanych parametrów. Prototyp destruktora dla klasy CBox przedstawia si ę nast ępująco:
-CBox () :
II Prototyp destruktora klasy.
Ze względu na to, że destruktor nie ma parametrów, w jednej klasie jeden destruktor. Określenie wartości
może znaj dować si ę
tylko
zwracanej lub podanie parametrów destruktora jest błędem.
Deslruklor domyślny Wszystkie obiekty tworzone przez nas do tej pory były niszczone automatycznie przez destruk tor domyślny klasy. Jest on generowany przez kompilator zawsze wtedy , gdy programista nie zdefin iuje własnego destruktora. Destruktor domyślny nie usuwa obiektów ani składowych obiektów, którym została przydzielona pam ięć w obszarze wolnej pamięc i przez operator new. Jeżeli w konstruktorze skladowym klasy dynamicznie zostało przydz ielone miejs ce w pamięci, to konieczne jest zdefiniowanie własnego destruktora jawnie używającego operatora del ete, zw alniającego pamięć przyd zieloną przez konstruktor za pomocą operatora new, podobnie jak w przypadku zwykłych zmiennych. Przydałoby się trochę praktyki w pisaniu destruktorów, a w i ęc spój rzmy na poniższy listing.
~ Prosty destruktor Aby zdobyć rozeznanie, kiedy wyw oływany jest destruktor klasy, mo żemy go sie CBox. Poniżej znajduje s i ę kod zawierający klasę CBox z destruktorem. II Cw8_01.cpp
II Klasa z j a wnym destrukt orem.
#i ncl ude
uSlng st d: :cout:
us m q st d: :endl :
cla ss CBox
II Defin icj a klasy o zasi ęgu globalny m.
(
publ i c: II Definicja destrukt ora.
- CBox( ) (
caut « "Dest rukt or zast al wywal any." « end l :
umieścić
w kla
Rozdzial8.•
Więcej na lemal klas
441
II Definicja kons truktora .
CBox( double l v = 1.0. doub le wv = 1.0. double hv = 1.0): m_Length(l v) . m_W idt h(wv) , m_Height (hv ) cout « endl
II Funkcja
«
"Konstruktor
zo s tał wywo ł any .
obliczająca pojemność pudełka.
doub le Vo lume () const
{
return m_Lengt h*m_W idt h*rn_He ight:
}
II Funkcja porównująca dwa pudelka, zwracająca wartość true,
IIjeżeli pi erwsze jest większe od drogiego, oraz fa lse w przeciwnym przypadku.
i nt compare(CBox* pBox ) con st (
return thi s->Volume() > pBox->Volume( ): pri va t e :
doub le m_Length: doub le m_Widt h: double m_Height:
II Deklaracja tablicy zawierającej obiekty klasy CBox .
II Dek laracja obiekt u cigar.
II Dek laracja obiektu match.
II Inicjalizacja wskaźnika do adresu obiek tu cigar.
II lni cjalizacja wskaźnika do CBox wartoś cią null.
cout « endl
« «
" P oJ emn o ść p u d e ł ka
pB l ->Vo lume( ):
pB2 = boxes: boxes[2] = match: cout « endl
« «
cigar wynosi ..
II Pojemność wskazywanego obie ktu.
II Ustawienie na adres tablicy .
II Ustawienie trzeci ego elementu na
wartoś ć
obiektu matc h.
boxes[2] wynosi ..
2)->Volume(): II Uzyskiwanie dostępu poprzez wskaźnik.
"P o jemn o ś ć p ud e ł k a
(pB2
+
cout « endl :
return O:
Jak to działa Jedynym zadaniem destruktora klasy CBox jest wyś w i etle n i e komunikatu informuj ąc eg o , zo s tał on wywołany. Wyn ik d zi ałan ia powyżs zeg o programu jest nas tępuj ący :
Konst ruktor Konstrukt or Konstrukt or
z o s ta ł wywo ł any .
zos ta ł wywo łany .
z os ta ł wywo łany .
że
442
Visual C++ 2005. 011 podstaw Konstruktor Konstrukt or Konstru kt or Konstru kt or
z o s ta ł wywo łany.
zo st ał wywo łany.
z os t a ł wywo ł a ny .
zost a ł wywo ł a ny.
P o j em n o ś ć pu d e ł k a Po j em n o ś ć p u de łka
Dest ruktor Dest ruktor Destruktor Dest ruktor Dest rukt or Destruktor Destruktor
cigar wynosi 40
boxes[ 2] wynosi 1.21
zo s t a ł wywo ł any .
zo st a ł wywo ła ny .
zo s ta ł wywo ła ny.
z o s t ał wyw o ł any .
z ost a ł wywo ł any.
z ost a ł wywo ł any.
zo s tał wywo ł any.
Na końcu programu destruktor został wywołany po jednym razie dla każdego istniejącego obiektu. Każdemu wywołaniu konstruktora na początku towarzyszy wywołanie destruktora na końcu . W tym przypadku nie ma potrzeby jawnego wywoływania destruktora. Kiedy obiekt klasy wychodzi poza zasięg , kompilator powoduje automatyczne wywołanie destruktora dla klasy. W naszym programie wywołania destruktora mają miejsce po zakończeniu wyko nywania funkcji matn: l, co sprawia, że istnieje duże prawdopodobieństwo, iż błąd w destrukto rze spowoduje załamanie programu w chwili , gdy funkcja main( l bezpiecznie zakończy już działanie.
Destruktory idynamiczne przydzielanie pamięci Często spotykaną czynnością jest potrzeba dynamicznego przydzielania pamięci składowym klasy. Do przydzielania pamięci składowym obiektu można używać operatora new w konstruk torze. W takim przypadku należy liczyć się z tym, że odpowiedzialność za jej zwalnianie poprzez dostarczenie odpowiedniego destruktora , gdy obiekt nie jest już potrzebny - spoczy wa na nas. Zdefiniujmy najpierw prostą klasę, w której możemy to zrobić.
Przypuśćmy, że
chcemy zdefiniować klasę, w której każdy obiekt stanowi pewnego rodzaju komunikat, na przykład łańcuch tekstowy. Klasa ta powinna maksymalnie efektywnie wyko rzystywać zasoby pamięci , a więc zamiast definiować składową w postaci tablicy elementów typu char, mogącej przechowywać łańcuchy o największ ej wymaganej długości , pamięć dla komunikatu w obszarze wolnej pamięci przydzielać będziemy w momencie utworzenia obiektu. Poniżej znajduje się definicja tej klasy: II Listing 08.0/.
class CMess age {
pri vate: char* pmessage; pub l ic:
II Wska źnik do obiektu zawierającego
II Funk cja wyswietlajqca komunikat.
void Show lt () const
{
cout
«
endl
«
pmessage;
}
II Definicja konstruktora.
CMessage(const char* te xt = "Komu nikat {
do myślny")
łańcuch
tekstowy.
Rozdzial8.•
Więcei na
temat klas
443
pmessage = new char [ st r l en(tex t) + 1J: II Przydz ielenie pamięci dla tekstu . strc py( pmessage, t ext ): II Skopiowanie teksiu do nowej lokal izacj i.
} - CMessage() :
II Prototyp destruktora.
};
zdefiniowana tylko jedna zmienna składowa - pmessage, która jest zarazem do łańcucha tekstowego. Została o na zdefiniowana w prywatnej sekcji klasy , nie ma do niej dostępu z zewnątrz.
W klasie
została
wskaźnikiem
a
więc
W sekcji publicznej klasy znajduje się funkcja składowa Showlt ( l , której zadaniem jest wy świetlanie zawarto ści obiektu klasy CM essage na ekranie . Zdefiniowany został także konstruk tor oraz prototyp destruktora klasy - - CMessage( l , o którym za chwilę . Konstruktor klasy wymaga jako argumentu łańcucha, ale jeżeli żaden nie zostanie przekazany, to używa łańcucha domyślnego określonego w parametrze. Konstruktor sprawdza długość łańcucha podanego jako argument, wyłączając końcowy znak NULL za pomocą bibliotecznej funkcji st r l en( ). Aby konstruktor mógł użyć tej funkcji, potrzebne jest dołączenie pliku nagłówkowego za pomocą dyrektywy #i ncl ude. Konstruktor określa niezbędną do przechowania łańcucha liczbę bajtów wolnej pamięci , dodaj ąc l do wartości zwróconej przez funkcję st r-len t ). Oczywiście, jeżeli przydzielanie pamięci nie p owiedzie s ię, to zostanie zgłoszony wyjątek, ktory spowoduje zamknięcie programu. Aby z takiej sytuacji wyjść w bardziej elegancki sposób, można ten wyjątek przechwycić w bloku konstruktora (zagadnienia zw iązane z obsługą błędów braku pamięci zostały opisane w rozdziale 6.). Mając ju ż przydzieloną pamięć
dla łańcucha za pomoc ą operatora new, używamy zdefiniowanej w pliku nagłówkowym funkcji bibliotecznej st rcpyr i, aby do obszaru przydzielonej mu pamięci skopiować łańcuch przekazany jako argument do konstruktora. Funkcja strcpy( l kopiuje łańcuch określony przez drugi argument wskaźnikowy do adresu za wartego w pierwszym argumencie wskaźnikowym. również
Potrzebujemy teraz destruktora, który zwalniałby pamięć przydzieloną dla komunikatu. Jeżeli go nie dostarczymy, to nie będzie sposobu na zwolnienie pamięci przydzielonej obiektowi. Użycie tej klasy w takiej postaci jak teraz w programie z dużą liczbą tworzonych obiektów klasy CMessage spowodowałoby stopniowe zajęcie c ałej wolnej pamięci i w końcu załamanie pro gramu . Często zdarza s ię to w takich warunkach, w których nie jest to wcale oczywiste. Mo głoby się na przykład wydawać, że przy tworzeniu tymczasowego obiektu klasy CMe s sage w funkcji wielokrotnie wywoływanej w programie obiekty te są niszczone w momenc ie zwra cania przez funkcj ę wartośc i . Tak się dzieje, ale p amięć w obszarze wolnym i tak nie jest zwal niana. A zatem za każdym wywołan iem funkcji coraz więcej pamięci zostaje zajęte przez usu nięte obiekty klasy CMes sage . Kod destruktora klasy CMess age przedstawia
s ię na stępująco:
II Lisiing OB,02.
II Destruktor zwalniają cy pamięć przydzieloną za pomocą ope ralora new.
CMessage: :-CMessage( ) { cout « "Dest rukt or
z os t a ł wywoł a n y ."
II Tylko po lo, aby
ś ledz ić,
co s ię dziej e.
444
Visual C++ 2005. Od podstaw «
endl :
delet e[ ] pmessage:
II Zwolnienie pamięci przydzielonej II wskaźnikowi.
Ze względu na to, że definicja destruktora znajduje się na zewnątrz klasy, konieczne było podanie kwalifikatora w postaci nazwy tej klasy - CMessage. Jedyne, co robi destruktor, to wyświetlanie komunikatu, dzięki któremu wiemy, co się dzieje, a następnie zwolnienie pa mięci wskazywanej przez wskaźnik składowy pmes sage za pomocą operatora del ete. Zauważ, że po operatorze de l ete zostały umieszczone nawiasy kwadratowe, które potrzebne są ze względu na to, iż usuwana jest tablica (typu char ).
~ Zastosowanie klasy CMessage Prze ćwic zymy
zasto sowanie klasy CMess age na
poniższym
krótkim
przykładzie:
II CwS_02.cpp II Użycie destru ktora do zwolnienia pam ięci . #i ncl ude II Dla strumienia wejścia-wyjścia. #i ncl ude II Dla funkcji strlen() i strcpy().
using std : :cout : using std : :endl : II Wstaw tutaj defi n icję klasy CMessage (listing CwS_Ol) . II Wstaw tutaj t nt (
II Ręczn e usunięcie obiek tu utworzonego za p om o cą operatora new.
ret urn O: Nie zapomnij w miejsce komentarzy wstawić odpowiednich partii kodu zawierających defini cje klasy CMes sage i destruktora, gdyż bez nich programu nie będzie można skompilować .
Jak lo działa Na początku funkcji main () zadeklarowaliśmy i zainicjalizowaliśmy obiekt klasy CMess age o nazwie motto w standardowy sposób. W drugiej deklaracji zdefiniowaliśmy wskaźnik do obiektu klasy CMess age o nazwie pMoraz za pomocą operatora new przydzieliliśmy pamięć obiektowi klasy CMessage, wskazywanemu przez ten w skaźnik. Wykonanie operatora new spo woduje wywołanie konstruktora klasy CMes sage, który z kolei ponownie wywołuje operator
Rozdzial8.•
Więcej na temat klas
445
new w celu przydzielenia pamięci dla komunikatu tekstowego wskazywanego przez należący do klasy wskaźnik pmessag e. Skompilowanie i uruchomienie tego programu da następujący rezultat: Lepiej późno n i ź wca le.
Wszyscy s ą sobie równ i .
Dest rukt or zos t a ł wywo ł a ny .
Na ekranie mamy informację, że destruktor został wywołany tylko jeden raz, mimo że utwo rzone zostały dwa obiekty kla sy CMes sage. Wcześniej wspominałem, że kompilator nie jest odpowiedzialny za obiekty utworzone w obszarze wolnej pamięci. Kompilator wywołał de struktor dla obiektu motto, ponieważ jest to normalny obiekt automatyczny, mimo że pamięć dla tej składowej została przydzielona przez konstruktor w obszarze wolnej pamięci. Obiekt wskazywany przez wskaźnik pMjest inny . Pamięć dla tego ob iektu została przydzielona w ob szarze wolnej pamięci, a więc musi zostać zwolniona za pomocą operatora del ete. W tym celu należy usunąć komentarz sprzed instrukcji return w funkcji matn t ): II delete p M;
II Ręczne
usunięcie
obiektu utworzonego za pomocq ope ratora new.
Ponowne skompilowanie i uruchomienie programu da
następujący
wynik:
Lepi ej późno ni ż wca le .
Wszyscy są sobie równ l
Dest ruktor zost a ł wywoł a ny.
Destrukto r z o stał wywoła n y.
Teraz nasz destruktor został wywołany dwa razy. Jest to pod pewnym względem zaskakujące. Operator del ete usuwa pamięć przydzieloną za pomocą operatora new w funkcji main( ) zwalnia tylko pamięć wskazywaną przez wskaźnik pM. Ze względu na to, że wskaźnik pM wska zuje obiekt klasy CI~e s sa g e, operator de l ete wywołuje także destruktor, aby zwolnić pamięć zajmowanąprzez składowe obiektu. A więc za każdym razem, gdy do usunięcia obiektu utwo rzonego dynamicznie za pomocą operatora new używamy operatora de l ete, wywoływany jest destruktor klasy dla tego obiektu przed zwolnieniem pamięci przez niego zajmowanej.
Implementacja konstruktora kopiującego Gdy przydzielamy dynamicznie pamięć składowym klasy, w obszarze wolnej pamięci czają się na nas demony. W przypadku klasy CMessage domyślny konstruktor kopiujący jest zupełnie niewystarczający. Przypuśćmy, że napisaliśmy następujące instrukcje: CMessage mottol( "Promi eniowanie zabi j a twoj e geny. ") ; CMes sage motto2(mottol) ; II Wywolanie domyśln ego konstruktora kopiujqcego. Efektem działania domyślnego konstruktora kopiującego jest skopiowanie adresu przecho wywanego we wskaźniku będącym składową klasy z obiektu mottol do obiektu motto2, ponie waż proces kopiowania zaimplementowany przez domyślny konstruktor kopiujący polega na prostym skopiowaniu wartości przechowywanych w składowych oryginalnego obiektu do nowego obiektu. W konsekwencji tylko jeden łańcuch tekstowy jest współdzielonyprzez dwa obiekty. Pokazano to na rysunku 8.1 .
zostanie zmodyfikowany za pośrednictwem któregokolwiek z tych dwóch będą także w tym drugim, ponieważ dzielą one ten sam łańcuch. Jeżeli obiekt mottol zostanie zniszczony, to wskaźnik w motto2 będzie wskazywał zwolniony obszar pamięci , który może zostać wykorzystany do czegoś innego. To na pewno spowoduje chaos. Oczywiście, ten sam problem pojawi się w momencie zniszczenia obiektu rnotto2. W ta kim przypadku obiekt mottol zawierałby wskaźnik do nieistniejącego łańcucha tekstowego. obiektów, zmiany widoczne
Rozwiązaniem tego problemu może być dostarczenie konstruktora kopiującego w miejsce konstruktora domyślnego. Jego implementacja mogłaby znaleźć się w sekcj i publicznej klasy, jak widać poniżej:
CMessage(const CMessage& initM )
II Definicja konstruktora kopiującego.
( II Przydzielenie pam ięci dla
lań cu ch a
tekstowego.
pmessage = new char[ str len(inl t M.pmessage)
+ l ]:
II Kopiowanie tekstu do noweg o obszaru pamięci.
st rcpy(pmessage. i nit M. pmessage): Pamiętamy z poprzedniego rozdziału, że aby uniknąć nieskończonej spirali wywołań konstruk tora kopiującego, parametr musi być określony jako stała referencja. Zdefiniowany powyżej konstruktor kopiujący wpierw przydziela odpowiednią ilość pamięci dla łańcucha w obiekcie in itM, przechowującego adres w składowej nowego obiektu, a następnie kopiuje ten łańcuch z obiektu inicjalizującego. Teraz nowy obiekt jest identyczny ze starym, ale całkowicie od niego niezależny .
Rozdział 8.• Więcej na temat
klas
447
sobie j ednak, że możesz już czuć się bezpiecznie i że nie musisz zawracać sobie konstruktorem kopiującym dzięki zainicjalizowaniu jednego obiektu klasy CMessage innyrn jej obiektem. W mrokach obszaru wolnej pamięci czai się jeszcze jeden demon, który tylko czeka na okazję do zadania C i ciosu w najbardziej nieoczekiwanym momencie. Przyj rzyjmy się poniższej instrukcji:
Nie
myśl
głowy
CMessage t hought( "Dobrze w domu DisplayMessage(t hought ) ;
by ć
z
mamą." ) ;
II Wyw oianiefunkcji w celu wysiania
II komun ikatu na wyjś cie.
gdzie funkcja Di s pl ayMes s age ( ) jest zdefiniowana
następująco;
void Di splayMessage(CMessage l ocalMsg) (
cout
« «
end l « " C h c ę wsm powiedzi e ć . localMsg.Showlt ( ) ;
że :
"
retur n;
Czyż nie wygląda to doskonale? Co mogłoby tutaj być źle? Ten błąd to katastrofa i nic więcej! To, co robi funkcja Di spl ayMessa ge() , w rzeczywistości jest niedorzeczne. Problem dotyczy parametru. Parametr jest obiektem klasy CMes sage, a więc argument w wywołaniu przekazy wany jest przez wartość. Przy użyciu domyślnego konstruktora kopiującego kolejność wyda rzeń jest następująca:
l
Tworzony jest obiekt thought z pamięcią dla komunikatu "Dobrze w domu być z mamą. " , przydzieloną w obszarze wolnej pamięci.
2.
Wywołana zostaje funkcja Di s pl ayMessage ( ) oraz - ponieważ argument przekazany jest przez wartość - tworzona jest kopia obiektu l ocal Msg za pomocą domyślnego konstruktora kopiującego. Od tej chwili wskaźnik w kopii wskazuje na ten sam łańcuch w obszarze pamięci wolnej co obiekt oryginalny.
8. Pod koniec wykonywania funkcj i obiekt lokalny wychodzi poza zasięg, a
więc
zostaje wywołany destruktor klasy CMessage . Powoduje to usunięcie obiektu lokalnego . (kopii) poprzez zwolnienie pamięci wskazywanej przez wskaźnik pmessage.
4. W czasie zwracania wartości z funkcji Di s pl ayMes sage( ) wskaźnik w oryginalnym obiekcie th ought nadal wskazuje obszar pamięci, która dopiero co została wyczyszczona. Przy następnej próbie użycia oryginalnego obiektu (lub nawet jeśli z niego nie skorzystamy, ponieważ musi on zostać wcześniej czy później usunięty) program będzie zachowywał się w dziwny i tajemniczy sposób. Wszelkie wywołania funkcji przekazujących przez wartość obiekty klas zawierających skła dowe definiowane dynamicznie powodują problemy. W związku z tym możemy podać stupro centową złotą zasadę:
Przydzielając dynamicznie pamięć składowej klasy w natywnym C++, zawsze implementuj konstruktor kopiujący.
448
Visual C++ 2005. Od podstaw
Dzielenie pamięci
pomiędzy
zmiennymi
z czasów, gdy 64 K stanowiło dużą ilość pamięci , w C++ istnieje moż na współdzielenie tego samego obszaru pamięci przez więcej ni ż jedną zmienną (ale oczywiście nie w tym samym czasie). Twór ten nazywa się unią i istnieją cztery podstawowe sposoby jego wykorzystania:
Jako relikt
przeszłości
liwość pozwalająca
•
D zięki użyciu unii mo żna sprawić, że zmienna A będzie zajmowała blok pamięci w jednym miejscu programu, który jest następnie wykorzystywany przez zmienną Binnego typu, ponieważ zmienna Anie jest już potrzebna. Nie zalecam takiego zastosowania unii, gdyż związane z tym ryzyko spowodowania błędu jest o wiele więks ze niż korzyści. Ten sam efekt mo żna u zyskać, przyd zielając pamięć dynamicznie.
• W programie może dojść do sytuacji, w której potrzebna jest duża tablica danych, ale przed wykonaniem programu nie wiadomo, jakiego typu będą to dane. Stanie się to jasne dopiero po wprowadzeniu ich z zewnątrz. Takiego zastosowania unii również nie polecam, ponieważ to samo możemy osiągnąć za pomocą kilku wskaźników różnego typu i dynamicznego przydzielania pamięci. • Trzeci sposób zastosowania unii może czasami się przydać - kiedy chcemy zinterpretować te same dane na dwa lub więcej różnych sposobów. Może mieć to miejsce w przypadku, gdy dysponujemy zmienną typu l ong i chcemy ją potraktować jako dwie wartości typu s hort . System Windows czasami pakuje po dwie wartości typu short do pojedynczego parametru typu l ong przekazywanego do funkcji. Innym przykładem jest sytuacja, gdy chcemy potraktować blok pamięci zawierający dane liczbowe jako łańcuch bajtów, aby je gdzieś przen ieść. • Unii można także u żyć jako sposobu przekazywania obiektu lub wartości, gdy nie wiadomo z góry, jakiego typu one będą. Unia może przechowywać dane dowolnego typu.
Definiowanie unii Unię definiuje się za pomocą słowa kluczowego union. Jej konkretnym przykładzie:
unio n share LD {
doubl e dval :
l ong lval:
}: Powyższy
definicję najłatwiej zrozumieć
na
II Wspoldzielenie pamięci przez typy fang i double .
kod definiuje unię typu shareLD, która może przechowywać zmienne typu l ong i doubl e, zajmujące ten sam obszar pamięci . Nazwa typu unii najczęściej zwana jest etykietą. Instrukcja ta jest podobna do definicji klasy w tym , że nie zdefiniowaliśmy jeszcze egzem plarza unii, a więc na razie nie mamy jeszcze żadnych zmiennych. Po zadeklarowaniu typu unii można za pomocą deklaracji definiować jej egzemplarze. Na przykład :
Rozdział 8.
Więcej
•
na lemal klas
449
shareLD myUnion; Powyższy
kod definiuje egzemplarz typu unii s har eLD, którą zdefiniowaliśmy Egzemplarz ten mogliśmy również zdefiniować wewnątrz instrukcji definiującej umo n
shareLD
Wspołdzi eleni e pamięci
II
wcześniej. samą unię:
przez typy long i double.
{
double dval :
long lval :
} myUn ion : W celu odwołania się do składowej unii używamy operatora bezpośredniego dostępu do skła dowej (kropki) z nazwą egzemplarza unii , podobnie jak przy uzyskiwaniu dostępu do skła dowych klasy. Poniższa instrukcja ustawiłaby zmienną typu l ong o nazwie l val na wartość 100 w egzemplarzu unii MyUnion:
myUn ion .l va l = 100:
II
Używanie sk łado wej
unii .
Późniejsze użycie
w programie podobnej instrukcji inicjalizującej zmienną typu doubl e o nazwie dval powoduje nadpisanie zmiennej l val. Największy problem związany z wyko rzystaniem unii do przechowywania danych różnego typu w tym samym obszarze pamięci spowodowany jest sposobem jej działania - trzeba znaleźć jakiś sposób określenia, która składowa jest aktualnie wykorzystywana. Zazwyczaj dokonuje się tego poprzez utrzymywanie dodatkowej zmiennej, która służy jako wskaźnik typu przechowywanej wartości. Unia nie jest ograniczona tylko do dwóch wartości . Ten sam obszar pamięci może współdzielić kilka różnych zmiennych. Ilość pamięci zajmowanej przez unię jest równa ilości pamięci po trzebnej do przechowywania jej największej składowej . Załóżmy na przykład, że zdefiniowa l iśmy następującą unię :
union sha reDLF {
double dva l :
long l val :
f loat fval:
} ui nst = {1.5 }: Egzemplarz unii shar eDLF zajmuje osiem bajtów, co uwidoczn ione
Rvsunek 8.2
- - -- - 8 bajtów -
-
-
-
zostało
Q:gJ-------,-------cq :,
Ival
,, ,,
:,
., ..
~
:
fval
,,------~y-----~ dval
na rysunku 8.2.
450
VisIlai C++ 2005. 011 pOIlstaw
w powy ższym przykładzie zdefiniowali śm y egzempl arz unii ui nst oraz jej etykietę . plarz
zo stał równ i eż
zainicj alizowany
warto ś ci ą
Egzem
l .5.
W dekla racji egze mplarza unii zainicjalizowa ć
można
tylko j ej pierwszą składową.
Unie anonimowe Unię można zad ekl arow a ć ta kż e
bez podawania nazw y jej typu. Egzemplarz takiej unii j est deklarowany automatycznie. Przypu ś ćm y , że zadeklarowali śm y p on iższ ą unię : union {
char* pva 1;
double dval :
i ong lval :
}: P owyższa
instrukcja definiuje zarówno unię bez nazwy, jak i jej egzemp larz, równ ież bez nazwy. Dzięki temu do zmiennych w niej zawartych możn a odwoływa ć s i ę za pomo cą samych nazw , jakie zostały im nadane w unii - pva l , dva l , l val . Taki spos ób definicji unii może być wygo dniejs zy od standardowego z pod an ą nazwą typu, ale trzeba uw ażać , aby nie pomi e s zać zwykłych zmiennych ze zmiennymi s k ła d o wy m i unii. Składo we takiej unii nadal współdz i el ą ten sam obsz ar pamięci . Aby zi l ust ro w ać, jak d z iała po wyżs za anonimowa unia przy uży c iu s kła do wej typu doub Te, można na pi s ać poni żs zą instrukcję :
dva l
~
99.5:
II
Użycie s k ładowej
anonimowej linii.
Jak widać , nic nie odróżnia zmiennej dva l od zwykłych zmiennych. U żywaj ąc unii anonimo wych, m ożna przyjąć pewne konwencj e nazewnicze, dzięki którym skł adowe uni i będą lepiej widoczne, a co za tym idzie - istnieje mniejsze ryzyko, że nasz kod zos tanie ź le zrozumiany.
Unie wklasach i strukturach Egzemplarz uni i można umie ści ć w klasie lub strukturze. Je żeli zami erzasz prze chowywa ć dane różnego typu w ró żnym czasie, zazwyczaj koniec zne jest utrzym ywanie zmiennej skła dowej klasy wskazuj ącej rodzaj przechowywanej wartości w unii. Użyci e unii jako s kładowych klas lub stru ktur nie przynosi zazwyczaj wielkich korzyści.
Przeładowywanie operatorów Przeładowywanie
operatorów je st bardzo wa żn ą techniką, g dy ż um ożliw ia ona w spółpracę standardowych opera torów C++, takich jak + lub *, z obiektami typów dany ch stworzonymi przez p rogrami stę . Pozwala ona na napisanie funkcji redefin iując ej dany operator w taki sposób, aby wykon ywał on okreś lone czy nności , kiedy jest u żywany z obiektami jakiej ś klasy. Na
Rozllzial8. •
Więcej na temat klas
451
przykład moglibyśmy przedefiniować operator > w taki sposób, że kiedy zostanie użyty z obiek tami klasy (Box, któ rą widzieliśmy w poprzednim rozdziale, zwróci warto ś ć t rue, jeżeli pierw szy z nich ma wi ęk szą p ojemność ni ż drug i.
Przeładowywanie
nie pozwala na definiowanie własnych operatorów ani na zm ianę priorytetów operator ma tę samą kolejność wykonywania podczas oblicza nia warto ści wyrażenia co je go odpowiednik bez przeładow ywani a . Ta be lę pierw s zeń stw a operatorów można zn a l eźć w rozdzi ale 2. tej ksi ążki lub w bibliotece MSDN .
już i stn i ejących . Przeładowan y
Mimo
że
Poniżej
nie wszystkie operatory można przeładowywać, ograniczenia nie znajduje s i ę lista operatorów, których nie można przeładowywać :
Operator z a S l ę g u
Operat or warunkowy Operat or b ezpośr edn i eg o do s tę p u do sk ł a dow ej
Ope rat or size-of Operat or wy łu s kan i a wsk aźnlka do s k ł a dowej kl asy
są
zbyt dotkliwe.
? :
si zeof
*
Ze wszystkimi pozostałymi możemy s i ę bawi ć , co daje nam c ałkiem spore możliwoś ci . Oczy wiście , dobrze jest upewnić się, że nasze wersje operatorów standardowych są spójne z ich normalnym użyciem albo przynajmn iej ich sposób działan ia jest wy starczająco intui cyjny . Nie b yłoby zbyt sensownym posun ię c iem utworzenie dla klasy przeładowanego operatora +, który mnożyłby jej obiekty . Sposób dzi ał ania operatorów przeł adowanych najłatwiej zrozu mieć na przykład zie , a wi ę c zaimplementuj emy teraz to, o czym przed chwilą m ówiłem operat or w iększości > dla klasy Cbox.
ImlJlementacia przeładowanegO operatora W celu zaimp lementowania przeł adowanego operatora dla klasy należy napisać s p e cj a l n ą funkcję. Zakładając, że jest ona s kładową klasy CBox, deklaracja funkcji przeładowuj ąc ej ope rator > w obrębie tej klasy wyg ląd a na stępująco :
cl ass CBox {
publ i c: bool ope rat or>(CBox&aBox) const ;
II Prz eladowanie opera tora
większośc i .
II Reszta definicji klasy ...
};
W p owyższym kodzie słowem kluczowym jest oper ator. Słowo to w połąc zen iu z symbolem lub nazwą operatora, w naszym przypadku >, definiuje funkcj ę operatora . Nasza funkcja na zywa s ię operatorc-O . Funkcję operatora można nap i sać , umie szczając , bąd ź nie, spacj ę po m i ędzy sło wem kluczowym oper ato r a samym operatorem , dopóki nie poj awiaj ą się żadne dwuznacznoś ci. Wątpli wo ś ci co do znaczeni a mogą pojawić s ię przy u życiu operatorów w postaci nazw, a nie symboli, takich jak np. new czy del ete. Gdyby śmy napisali operato rnew lub oper at ordel et e, to p owstałyby zwykłe funk cje, gdyż są to prawidłow e nazwy funkcji. A więc pisząc definicję funkcji jednego z tych operatorów, po słowie kluczowym opera t or zawsze należy wst awi ć sp acj ę . Z au w aż , że funkcj a oper ator>( ) zadeklarowana zo s tała jako const , ponieważ nie modyfikuje ona zmiennych składowych klasy .
452
Visual C++ 2005. Od podstaw W zdefiniowanej funk cji oper ator >() prawy operand operatora zdefiniowany jest przez pa rametr funkcji . Lewy natomiast jest zdefiniowany niejawnie przez wskaźnik thi s. Jeśli mamy wi ęc poni żs zą in strukcj ę w arunkow ą i f : if(boxl cout
>
«
box2)
endl « "boxl j est
box2";
w i ęk s z y n iż
wyrażenie znajdujące się
w naw iasach po s łowi e kluczowym i f spowoduje funkcji operatora i jest równo znaczne z poniższym w ywołaniem funk cji :
wywołanie naszej
boxl .operat or>(box2) ; Powiązania pomiędzy zostały
RYSunek 8.3
obiektami klas y CBo x w przedstawione na rysunku 8.3.
wyrażeniu
a parametrami funk cj i operatora
if{ boxl > box2 )
Argum en t fun kcji
1
!
bool CBox::operator>(const CBox& aBox) const
l Obiekt w skazyw any przez w sk a źn ik thts ł
return (this ->VolumeOl > (aBox.VolumeOl;
Spójrzmy teraz, w jaki sposób
działa
kod funk cj i ope r at or> ( ):
II Funkcj a opera tora większośc i porównująca II pojemności dwóch ob iektów klasy CBox.
bool CBox: :opera to r>(const CBox& aBox) const
{
ret urn t hi s- >Vol ume( ) > aBox.Vol ume () ;
Podali śmy do funkcji parametr w postaci referencji w celu unikni ęcia niepotrzebnego kopio wania w momencie jej wywołan ia . Ze względu na fakt, że funkcja nie zmienia zawarto ści obiektu, dla którego zostanie wywołana, zadeklarowaliśmy jąjako stałą. Gdyby śmy tego nie zrobili, nie moglibyśmy używać naszego operatora do porównywania obiektów typu const klasy CBox. Wyrażenie re turn oblicza za p omoc ą funkcji Vo l ume () pojemność obiektu klasy CBox, wska zywanego przez wskaźnik t hi s, a następnie porównuje otrzymany wynik z pojemnością obiektu aBox przy u życiu operatora >. Podstawowy operator > zwraca wartość typu ca ł kow i tego (nie logiczną) , a wi ęc jeżeli pojemność obiektu klasy CBox wskazywanego przez wsk aźnik t hi s je st większa niż pojemność obiektu aBox przekazanego jako argument referencyjny, zostaje zwrócon a warto ść l , a w przeciwnym przypadku o. Wartość zwrócona z operacji porówny wania zostanie automatycznie przekonwertowana do typu zwracanego funkcji operatora, czyli do typu logicznego .
Rozdział 8.• Więcej na temat klas
~ Przeładowywanie P rzećwi c zymy
operatorów
zastosow anie funkcji oper at or> ( ) na przykładow ym programie:
II CwS 03.cpp II Ćwi;;en ie zastosowa nia przeładowan ego opera tora większosc i .
pri vate: dou ble m_Lengt h: deub le m_Widt h: double m_Hel ght : }: II Fu nkcja op eratora większości porównująca
II poj emnosoi dwóch obiektó w klasy Clłox.
boo l CSox: 'ope rator>(const CBex& aBox) const (
retu rn t hi s->Vo l ume() > aBox.Velume( ): int mai n( ) (
CBex smal l Bex (4.0. 2.0, 1.0) ;
CBex medi umBox(10.0. 4.0. 2.0) ;
CBex bigBox(30 .0. 20 .0. 40 .0):
lf( mediumBox > smal l Bex )
cout « end l
II Dlugos ć pu delka w centymetrach. II Szero koś ć pude lka w centyme trach. II Wysokoś ć p ude lka w centymetrac h.
453
454
Visual C++ 2005. Od podstaw «
" Pu d e ł k o
mediumBox jest
wię k s z e
niz smal l Box." ·
lf (medl umBox > blgBox) cout « endl « " Pu d eł k o mediumBox j est wi ę ksz e niz bigBox. "; else cout « endl « "Pu d ełk o medlUmBox nie jest w i ęks ze ni ż bigBox" ; cout « endl ; ret urn O;
Prototyp funkcji oper at or >( ) znaj duj e się w sekcji publicznej klasy. Jako że definicja funkcji znajduje s ię ju ż poza k l asą, nie stanie s ię ona dom yślnie funkcją inline. Jest to całkowi ci e z a leż n e od progr ami sty . Równie dobrze mogliśmy definicję tę wstawić do definicj i klasy w miejsce prototypu funkcji. W takim przypadku nie b yłoby potrzeby używania przed funkcją kwa lifikatora w postaci CSox ; .. Jak zapewne pamiętasz , jest to konieczne zawsze wtedy , gdy funkcja składowa jest zdefiniowana poza obrębem definicji klasy, gdyż informuje kompilat or, do której klasy dana funkcja nal e ży . W funkcji mai n( ) znaj d ują s i ę dwie instrukcj e warunkowe i f, w których został użyty operator > ze s kład o wym i klasy. Jego użycie powoduje automatyczne wywołanie operatora przełado wanego . J eżeli chce my m i eć tego potw ierd zenie , to możemy dodać instrukcję wyjściow ą do funkcj i op eratora. Wyn ik działania tego programu jest następujący:
Konst ruktor z o s t a ł wywo ł a ny .
Konst ruktor zosta ł wy woł a ny .
Konstruktor zos t ał wy wo ł any .
P ud e ł k o med i umBox j est wi ę k s z e n i ż sm al l Box.
P u d e ł k o med i umBox nie j est w i ęk s z e n iż bigBox.
Destruktor z o sta ł wywo ł any .
Dest ruktor z o sta ł wywoł any .
Dest ruktor zo sta ł wywo ł any
Z danych na ekrani e wynika, że instrukcj e warunk ow e i f z funkcją operatora. A więc wydaje się, że przedstawienie w kategoriach obiektowych jest rozsądnym pomy słem .
dzi ałają p rawidłowo
rozwiązan i a
w połączeniu problemów klasy CSox
Implementacja pelnej obSlUgi operatora Nadal jest wiele rzeczy, który ch nie możemy zrob i ć przy u życiu naszej funkcj i operator-O. Definicja rozwiązania problemu w kategoriach obiektów klasy CSox mogłaby równie dobrze zawiera ć następujące instrukcj e:
if( aBox > 200 ) II Rób coś...
Rozdział 8.• Więcej na
lemaIklas
455
Nasza funkcja nie por adzi so bie z czymś takim. Prób a u życi a wyra że nia porównuj ącego obiekt klasy CBox z wartości ą li czbową zako ńczy s ię zgłoszeniem przez kom pi lator komunikatu o błę dzie. W celu umo żliw ien ia wykonyw ani a takich operacj i należał ob y napisać funkcj ę ope r a t or >( ) w wersji przeł ad owanej. Dod ani e o bsłu g i wy ra ż e n ia, które przed c hw i lą w i dz i e liś my , j est bar dzo funk cj i w ewnątrz kl asy wyglądałaby n a stępująco :
łatw e .
Dekl ar acj a
II Porównanie obiektu klasy CBox ze s ta lą.
bool ope rato r>Cconst double&val ue) const : Defin icja ta powinna p oj awić s ię w definicji klasy . Prawy ope ra nd operatora > odpowi ada par ametrowi funk cji . Obiekt klasy CBox, który jest tutaj lewyrn operande m, przekazany zostaje niej awnie w postaci w skaźnika t hi s . Implementacja tego przeładow aneg o operatora j est instrukcj i w ciele funkcj i: II Funkcj a poró wnują c a obiekt klasy CRox ze
równi eż łatwa .
Wymaga on a tylk o jednej
s talą .
boo l CBox: .operat or>Cconst double&val ue) const (
retu rn t his->VolumeC) > va l ue: P ro ś ci ej już Z
ch yb a by ć nie mo że ? A le nadal mam y pewne pr obl emy z u ży ciem operatora > obi ektami klasy CBox. Równie dobrze możemy z ec h c i eć napisać instrukcj ę podobną do
pon i ższej :
if (200
>
aBox)
II Rób coś ... M o żesz powiedzi e ć , że
da si ę to zro b i ć , impl em entując funkcj ę operatora operator« ) pr zyj prawy arg um ent typu doub l e, a na stępn ie przepi suj ąc p o w yż s z ą i n s t ru k cję w taki sposób, aby jej używała - i masz rację . Rzecz ywi ści e , imp leme ntacja operatora < może być wy magan a do por ównywani a obiektów klasy CBox, ale implementacja o bsługi typu obiektu nie powinna w ża de n sztuczny spo sób ograni cz a ć sposobu, w j aki możn a u żywać obi ekt ów w wyra żeniach . Ich użycie powinno być jak najbardziej naturalne. Probl emem jest kwesti a, j ak tego dokon a ć . muj ącą
S k łado wa
funk cj a ope rato ra zaw sze dostarcza lew y arg um ent w post aci wskaźnik a t hi s . w tym przyp adku lewy argument jest typu doubl e, nie można tej funkcji zaimp lemen tow ać jako fu nk cji s kła do wej . P ozostaj ą nam dwa wyj ści a: zwykła funkcja lub funkcj a za przyjaźniona . Ze w zględu na fakt, że nie pot rzebujemy do stęp u do prywatnych s kł ad o wy c h klasy, nie musimy stosować funkcji zaprzyjaźn io nej , a w ięc przełado wany opera tor > mo żem y za i m p leme nto wać z lewym argumentem typu doubl e j ako zwy kł ą funkcj ę . Pr ototyp tej funk cji - umi eszczonej oczyw i śc i e poza defini cj ą klasy , poni eważ nie jest on a funkcj ą s kła dową
Jako
że
wygląda następuj ąco:
bool ope rat or>Cconst doubl e& val ue. const CBox&aBox); Impl em ent acj a
wyg l ąda n a stępuj ąc o :
456
Visual C++ 2005. Od podstaw II Funkcja
porównują ca s ta lą
z obiektem klasy CBox.
bool operato r>(cons t doubl e& val ue . const CBox&aBox) (
ret urn val ue > aBox.Vol ume( ) : Jak ju ż wiemy , zwykła funkcja (a także zaprzyj aźn i o n a w takim przypadku) uzyskuje dostęp do składowych obiektu za p omocą operatora be zpo średniego dostępu do składowej oraz na zwy obiektu. Oczywiście , zwykła funkcj a ma do stęp tylko do składowych znajdujących się w sekcji publicznej . Funkcja s kł adow a Vol ume( ) jest publiczna, a więc możemy ją tutaj bez problemu użyć. Jeżeli
w klasie nie byłob y publ icznej funkcji Vol umst ), to do uzyskan ia bezpośredniego do do prywatnych składowych m ogl ibyśmy użyć funkcji zaprzyjaźnionej . Innym wyjśc iem byłoby dostarczenie zestawu funkcji skład owych zwracaj ących wartości prywatnych zmien nych składowych oraz użycie ich w zwykł ej funkcj i w celu implem enta cji porównywania. stępu
Rm!II!!UI Pelne Jlrzeładowanie operatora> Wszystko, o czym mówiliśmy do tej pory, złożymy w jedną całość, aby
zobaczyć, jak to działa :
II Cw8_04.cpp
II Implementacj a pełnego p rze łado wania operatora w iększos ci.
#i ncl ude II Dla strumienia wejścia-wyjścia.
using st d: .cout: us i nq st d: .endl : class CBox
II Defini cj a klasy o zas ięgu globalnym.
(
pub l l C: II Definicj a konstruktora.
CBox(double l v = 1.0. doub le wv = 1.0. double hv = 1.0) : m_Lengt h(l v) . m_Widt h(wv) . m_He ight (hv) cout « end l « "Konstr ukto r II Funkcja
obliczająca pojemnos ć
zos ta ł wywo ł a ny
pu delka.
doub le Volume() const ( ) II Funkcja operato ra większosci porówn ująca II pojemnoś ci obiektów klasy CBox.
bool operator>(const CBox& aBox) const (
ret urn t his->Vol umeO > aBox .Vol umeO : II Funkcja p o ró wn ująca obiekt klasy CBox ze sta łą .
bool operat op (const doubl e&val ue) const {
Rozdzial8.•
Więcej na
temat klas
457
ret urn thi s->Vol ume() > value: II Definicja destrukto ra.
II Długoś ć p ude łka w centymetrach. II Szerokość pudełka w centymetrach. II Wys okość pudelka w centymetrach.
}:
i nt operator- rconst double&va l ue. const CBox& asox) : II Prototyp funkcji. mt
ma i n()
{
CBox sma11 Box (4. O. 2.O. 1. O):
CBox medi umBox(lO. O. 4.0. 2.0):
i f (mediumBox > sma l lBox)
cout « endl
« "Pud eł k o medl umBox jest
w ięk s ze
ni t sma ll Box
i f (mediumBox > 50.0)
cout « endl
«
" P ojemn oś ć p u d e ł k a
els e
cout « endl
« " P o j emn o ś ć i f( lO .O> sma l l Box )
cout « endl
« "Po j emn o ś ć else
cout « endl
« "Po j emn o ś ć
medi umBox j est
wi ęk sza
ni t 50 . '"
pud ełka
mediumBox nie jes t
pude ł k a
sma11Box jes t mni ej sza
p u d e łk a
sma llBox nie jes t mniej sza
w i ęk s z a
ni ż
ni z 50 .";
10.
ni ż
lO .
cout « endl :
ret urn O:
II Funkcja porówn ująca stałą z obiektem klasy CBox.
i nt ope rator >(con st double& va lue . const CBox&aBox)
{
return value > aBox .Volume () ;
Jak lo działa Zwróć uwagę ,
w którym miejscu znajduje się prototyp funkcji oper at or>( ) w zwykłej wersji. Musi ona zn ajd o wa ć się po defin icj i klasy , po nieważ odnos i się do obi ektu klasy CBox na liście parametrów . Jeżel i um i e śc im y ją przed d efi n icją klasy , to programu nie b ęd zi e m o żn a s kompi lować .
458
Visual C++ 2005. Od podstaw Istnieje sposób na umieszczeni e j ej na początku programu po dyrektywie #i ncl ude: za pom oc ą niekompletnej deklaracji klasy. Powinna ona znaj do w ać się przed prototypem i wy gląd a na st ępująco :
class CBox: mt operato r-tconst doubl e& val ue . CBox& aBox) :
II Niekompletna deklaracja klasy. II Prototyp fu nkcji.
Powyższa niekompletna deklara cja klasy wskazuj e komp ilatorowi, że CBox j est klasą, co wystar cza, aby po zwolił on na poprawne przetworzenie prototypu funkcji . Jest to możliwe, poni eważ wie, że CBox j est zdefiniowanym przez u żytkownika typem, który zostanie określony p ó źn i ej .
Mechanizm ten j est t akż e ni ezb ędny w sy tuacjach, gdy mam y dwi e kla sy i każda z nich zawiera skład ow ą w postaci w skaźn ika do obiektu tej dru giej kla sy. Ka żda z nich wymaga, aby ta druga b ył a zadeklarowana j ako pierwsza. Tego typu sytu ację patową można ro zw i ą zać za pom ocą niek ompletnej dekl aracj i klasy. Rezultat
d ziałani a p owyżs ze go
programu j est
n astępuj ąc y:
Konstru ktor zo s t a ł wywo ł a ny .
Konst ruktor zost a ł wywoła ny .
P u de ł ko medlum Box Jest wi ę k s z e n i ż smal l Box.
P O jem no ś ć p u d eł k a m edi umBox j est wi ę k sza n iż 50
P O j emn o ś ć p ud e ł k a sma l l Box Jest m nlejSZa n i ż 10 .
Dest rJktor zo s t a ł wywo ł a ny .
Dest rJkt or zos tał wywo ł a ny
Po komunikatach o wywołaniu konstruktora w związ ku z tworzen iem ob iektów sma l iBox i med: umBox zn aj d uj ą się wiers ze wy słane z instrukcji warunkowych 'j f - każda z nich dz iała tak, jak się spodziewaliśmy. Pierwsza z nich wywołuje funkcję operatora, która jest składową kla sy i działa z dwoma obiektam i klasy CBox. Druga natomiast wywołuje funkcję składową z parametrem typu doubl e. Wyrażenie w trzeciej instruk cji warunkowej i f wywołuje funkcj ę operatora, kt ór ą zai mp l e m e n towa l iś m y jako zwy kłą fu nkcję . Tak się składa , ż e obie funkcj e operatora, które są s kła d o wy m i klasy, m ogliśmy zdefin iować j ako zwykłe funkcje , ponieważ wymagają one do stępu tylko do funkcji s kład o w ej Vo lume( ), która je st pub liczna.
W p odobny sposó b jak przedstawiony po wyżej można za implem en to wać dowolny opera tor porównania. R óżnice pomiędzy nimi powinny do tyczyć mniej znaczących szcz egółów, a ogólne podejś cie pozostaje takie samo.
Przeładowywanie Jeżeli
operatora przypisania
nie dost arczymy dla klasy funkcji przeład owane g o operatora przyp isania, to kornpi lator dostarczy jego domyślną wersję. Funkcja ta w w er sji domyśln ej po prostu wykonuje proces kopiowani a wszystkich s kła d ow yc h , podobny do tego, który wyk onywany jest przez dom yślny konstru ktor kopiujący . Nie można jednak myli ć domyślnego kon struktora kop i ują cego z domyślnym operatorem przypi sania. D om yś lny kon struktor kopiuj ący wywoływany je st przez deklar acj ę obiektu klasy, który jest inicjalizowany już i stn iejąc ym obiektem tej klasy
Rozdział 8.• Więcej na
temat klas
459
lub za p om o c ą pr zekazan ia do funkcj i j a k iegoś obiektu przez w art ość . Nato m iast domy ślny operator przypisania wywoływany jest, kiedy zarówno po prawej, jak i po lewej stronie instruk cji przypisania zn ajduj ą s ię obiekty tej samej kla sy. W przypadku klasy CBox d omyślny operator prz ypis ania działa bez zarzutów, ale w przypadku klas z awi e rających obszary p ami ę ci dla s kła dowyc h alokowan ych dynami cznie n ale ży u w a ż nie przyjrzeć s ię ich wy magan iom . Pomini ę cie op erato ra przypisania w taki ej sy tuacji m o że d oprow ad z i ć do p oważn ych za burze ń d ziałani a programu. Wróćmy na chw ilę do klasy CMessage, której używali śmy przy okazji omawiania konstrukto rów kop iuj ący ch. P ami ętam y , że miała ona zmienną składową pme ssage, która był a wskaźni kiem do łańcu ch a . Rozważmy teraz , jaki skutek wywołałby w j ej przyp adku dom yślny operator kop iuj ący. Przypu ś ćmy , że mi eli śmy dwa egzemplarze tej kla sy m ott ol i motto2. M o glibyśmy s p ró bować u staw i ć składowe egze mplarza m otto2 na wartości składo wych egze mplarza mottol za p om o c ą domy ślneg o operatora przypisania, jak poniżej:
motto2 = mottol :
II
Użyc ie domyśln ego
operatora p rzyp isan ia.
ope ratora przypisania w tym przypadku b ędzi e taki sa m, ja k gdy kon struktora kopiującego . To będzie katastrofa! Jako ż e k ażdy z tych obiektó w posiada w ska źnik do tego samego łańcuch a , jego zmiana dla jednego obi ektu po woduje zmi anę dla obu. Dru gim problemem jest to, że je żeli jeden z tych obiektó w zos tan ie zniszczo ny, to destruktor w yczy ści pamięć u żywaną do przechowywani a ł ań cuch a , a więc drugi obiekt będzi e zaw i e rał ws k aźn i k do obszaru p amięci , który m o że być ju ż używany do cze goś ca łki e m inn ego.
Efekt
u życia do myś l nego
byśmy użyli dom yślnego
To, czego potrzebuj emy, to aby operator przypi sani a żącego do obiektu docelowego.
Rozwiązanie
s ko p iow ał
tekst do obszaru
p ami ęci
nale
problemu
Problem ten możemy rozwiązać za pomocą własnej funkcji operatora przyp isania. Za kładamy, że zos tała ona zdefiniowana wewnątrz definicji kla sy: II Przełado wany ope rator przypisania dla obiektu klasy CMessage.
CMessage&operat or=(const CMes sage& aMessl ( II Zwo lnienie pamięc i dla pi erwszej operacji.
delet e[] pmessage: pmessage = new cha r[ st rlen(aMess .pmessagel II Skopiowanie
łań cu cha
+
l] :
drugiego opera ndu do pierwszego.
st rcpy (this ->pmessage. aMess .pmessage) : II Zwrócenie referencj i do p ierwszego operandu.
ret urn *t hlS: Przypi sanie może wydawa ć s ię pro ste , ale jest kilka szczegółów, na które n ale ży zwrócić uwagę . Warto zauważyć, że funkcja operatora przypisan ia zw raca referencj ę . Na pielwszy rzut oka może nie być oczywiste , dlaczego tak s i ę dziej e - przec ież funk cj a doprowadza operację
460
Visual C++ 2005. Od podstaw przypisania do samego końca i obiekt z prawej strony zostaj e przekopiowany do tego, który jest z lewej. Na pierwszy rzut oka może się wydawać, że nie ma potrzeby zwracania czegokol wiek, ale musimy bliżej przyjrzeć się temu, w jaki sposób mógłby zostać użyty operator. Istnieje prawdopodobieństwo, że będziemy musieli użyć wyniku operacji przypisania po prawej stronie jakiegoś wyrażenia. Przyjrzyjmy się następującej instrukcji :
mot t ol
~
motto2 = mot t o3;
Jako że operator przypisania jest wykonywany od prawej strony do lewej , najpierw zostanie wykonana operacja przypisania obiektu motto3 do obiektu motto2. A więc powyższą instrukcję możemy przedstawić następująco:
mott al = (mott o2.operat or=(motto3) ) ; Rezultat wywołania funkcji operatora znajduje a więc ostatecznie instrukcja ma postać :
się
tutaj po prawej stronie znaku
równości,
mott ol. operat or=(motto2.operator=(motto3) ) ; A zatem , jeżeli to ma działać, to na pewno coś musi zostać zwrócone. Wywołanie funkcji ope r ator=() pomiędzy nawiasami musi zwrócić obiekt, który będzie mógł zostać użyty w innym wywołaniu tej funkcji. W tym przypadku wystarczyłoby zwrócenie typu CMessag e lub CM es sa ge&, a więc referencja nie jest tu obowiązkowa, ale musi zostać zwrócony przynajmniej obiekt klasy CMes sage. Z drugiej jednak strony
(mottal
~
mot to2)
~
rozważmy poniższy przykład:
mot t o3;
Jest to w pełni prawidłowy kod (nawiasy zostały użyte w celu upewnienia się, że przypisa nie po lewej stronie zostanie wykonane jako pierwsze). Kod ten można przekształcić do na stępującej postaci :
(motto l.operator =(motto2) ) = mott o3; Po wyrażeniu pozostałej operacji przypisania w postaci jawnego dowanej otrzymujemy:
wywołania funkcji przeła
(m ot t ol. op e r a t o r =(mo tto2)) . op e r a t o r ~(mot t o3);
Powstała nam teraz sytuacja, w której obiekt zwrócony przez funkcję oper at or =( ) zostaje użyty do wywołania funkcji operat or-O . Jeżeli typem zwracanym jest tylko CMes sag e, to kod ten jest nieprawidłowy, ponieważ w rzeczywistości zwracana jest tymczasowa kopia oryginal nego obiektu, a kompilator nie zezwala na wywołanie funkcji za pomocą obiektu tymczaso wego . Inaczej mówiąc, wartość zwracana, kiedy typem zwracanym jest Cme ssage, nie jest l val ue. Jedynym sposobem na sprawienie, aby takie coś chciało się skompilować i działać poprawnie, jest zwrócenie referencji, która jest typu l val ue. W związku z tym jedynym moż liwym typem zwracanym, jeżeli chcemy zapewnić pełną elastyczność użycia operatora przy pisania z naszą klasą, jest typ CMessag e&.
Rozdział 8.• Więcei na temat
klas
461
Zauważ, ż e język C++ nie nakłada żadnych ograniczeń co do akcept owanych typów zwra canych lub typów parametrów operatora przypisania. Rozsądnie jest jednak zadeklarować ten operator w sposób przed c h w i l ą przeze mnie opisany, jeżeli chcemy, aby nasze funkcje operatora przypi sania obsługiw ał y normalne użyci e operacji przypisani a w C++ .
o którym należy pamiętać, to fakt , że każd y obiekt ma z góry przydzieloną dla ł a ńcucha , a więc p ierwszą rzeczą, jaką musi zrobi ć funkcja operatora, je st wy czyszczenie pamięci przydzielonej dla pierw szego obiektu oraz ponowne przydzielen ie od powiedniej jej ilo ści dla łańcuch a tekstowego należącego do drugiego obiektu . Po wykon aniu tych czynno ś ci ła ńcuch z drugiego obiektu może zostać skopiowany do nowego obszaru pa mię ci, należącego teraz do pierwszego obiektu. Drugi
szc żegół ,
pamię ć
Nadal jednak je st jeden defekt w tej funkcji operatora. Co
si ę
stanie , gdy napiszemy
poniższą
instrukcję ?
~t o1 = mott ol: Oczywiście
nigdy nie napisalibyśmy cze goś tak głupiego , ale może Jak na przykład w poniższ ej instrukcji:
się
to zdarzyć w przypadku
u żywania wskaźników .
motto1
~
*pMess:
pMess wskazuje obiekt mott ol, to otrzymamy wyrażenie identyczne z tym W takiej sytuacji funkcja operatora w obecnej postaci wyczyściłaby pamięć przy dzi eloną dla obiektu motto l , przyd zieliła trochę wię cej na podstawie długo ści właśnie usu ni ętego łańcu cha , a następnie sp ró b owała s k op i ow ać starą pami ęć , która do tej pory m oże być już nieprawidł owa . Można ten problem rozwiązać , sprawdzając identyczność lewego i pra wego operandu na po czątku funkcji. W związku z tym nasza funkcja operat or=( ) wygląda na Jeżeli wskaźnik
powyżej.
stęp uj ąc o:
II Prz eładowany opera tor przypisania dla obiektów klasy CMessage .
CMessage&ope rato r=(const CMessage& aMess) if (t his ~~ &a Mess ) ret urn *this ;
II Sp rawdź adres. jeś li laki sam, II z wróć pierwszy ope rand.
II Zwolnienie pamię c i dla pierwszego opera ndu.
delet eC J pmessage : pmessage ~ new charCst rle n(aMess .pmessage) +lJ: II Skopiowanie
łań cuch a
drugiego opera ndu do pierws zego .
st rcpy(t his- >pmessage. aMess .pmessage); II Zwrócenie refe rencj i do pierwszego opera ndu.
return *t his ; Powyższy
kod został napisany przy defini cji klasy.
założeniu, że
defini cja funkcji znajduje
się
w
obrębie
462
Visual C++ 2005. Od podstaw
lmmjI Przeładowywanieoperalora przypisania Pozbierajmy wszystko, o czy m m ów i li ś m y do tej pory w jeden program. Dodamy do klasy funkcj ę Rese t() , która konwertuje komunikat na łańc uc h gwiazdek . II Cw8_0 5.cpp II Szlifowanie przeladowywanla_o-,-p_e_ra_t_o r_a_k_o-,--p_io_H_'a_n_ia_.
.
--J
#i nclude
#include
uSlng std: :cout ;
ustne st d: .endl :
cl ass CMessage (
pr i vat e: char* pmessage ;
II
Wska źnik
do
lańcu ch a
obiektu.
puol iC: II Funk cja wy swietlajqca komunikat.
vo i d Showlt () const
{
cout
end l
«
«
pmessage ;
}
IIFunkcj a
konwertują ca
komunikat na *.
vo id Reset( ) (
char* t emp = omessage:
v/hil e(*t emp)
*(t emp++ ) = '*'.
II Przela dowany ope ra tor p rzypisania dla obi ektów klasy CMessage .
CMes sage&
ope r a t o r ~( c o n s t
CMessage&aMess)
{
i f (t his == &a Mess) ret urn *t his :
II Sp ra wdzanie adresów. jeś li są takie sam e, II zwró ć pi erw szy operand.
II Zwo lnienie pamię ci dla pierwszego op erandu .
delet e[J pmessage:
pmessage ~ new cha r[ st rlen(aMess .pmessage) +lJ:
II Skop iowanie
lań cucha
drugiego ope ra ndu do pierwszego.
strcpy(thls- >pmessage. aMes s .pmessage ) : II Zwrócenie ref erencji do pierwszego operandu .
ret urn *th i s; II Definicja konstruktora.
CMessage(const char* t ext = "Komumkat
d omy ś lny" )
{
pmessage = new char[ st r l enr t ext) +1 J: st rcoy(pmessage. t ext ) : II Destruktor
zwalniający p ami ęć przydzieloną
II Przydzieleni e pamięci dla tekstu. II Skopio wanie tekstu do nowego obszaru pamięci.
przez operator new .
Rozdzial8. •
Więcej
na temal klas
463
-CMessage ( ) {
cout
«
"Dest ruk tor zost ał
«
endl :
wyw o ł a ny . "
ae1et e[] pmessage;
II Śledz i, co s ię dziej e. II Z wolnienie pamię ci p rzydzie lonej
wskaźn iko wi .
};
int :na in( ) {
CMessage mottoH "G ł u p iemu CMessage mott o2 ;
szc zęści
e sprzyja. ") :
cout « "mot t o2 zaWlera
motto2.Showlt ( );
cout « enol ;
motto2 = mott ol ;
II Użyj no wego ope ratora przypisania.
cout « "mott o2 zawiera
motto2.Showlt( );
cout « endl ;
mottol .Res et ( );
II Ustawianie mo/lo} na + nie
II ma lVP~V W1J na mottoI ,
cout « "mottol zawiera teraz
mott ol .Showlt ();
cout « endl ;
cout « "motto2 na dal zaWlera
motto2.Showlt ();
cout « end1;
ret urn O. Z danych na ekr an ie wynika, ż e wszystko dz i ała należy ci e , bez żadn y c h powiązań po m i ęd zy komuni katami obu obiektów, z wyj ątkiem sytuacji, w których jawnie ustawiamy je jako takie same;
motto2 zaWlera
Komuni ka t domy ś lny
mot t o2 zaWle ra G ł up l em u sz c zę śc i e sprzYJa .
mot t ol zawiera t eraz
***************** **************
mot to2 nadal zawiera
G ł up i emu s z c z ę ś c i e sprzYJ a.
Dest ruktor zosta ł wywoła ny .
Dest ruktor z os ta ł wywo ła ny .
W związku z powyższym możemy utworzyć jeszcze j edną złotą zasadę :
Zawsze implementuj operator.przypisania, gdy .dynamicznie przydzielasz pamięć zmien nym składowym klasy.
464
Visual C++ 2005. Od podstaw Mając zaimplementowan y operator przypi sani a zastan ówm y s i ę, co dzieje s i ę z operatorami typu +=. Nie dział aj ą, chyba że je także zaimpl ementujem y. Dla każdego operatora w postaci op=, którego chcemy u ży ć z naszą klas ą, musimy nap i s ać o d dzi e l n ą funkcję operatora .
Przeładowywanie Zaj miemy
si ę
operatora dodawania
teraz
p rz eład owywaniem
operatora dodawania dla klasy CSox. Jest to bardzo z tworzeniem i zwracaniem nowego obiektu. Obiekt ten będzi e to z naczyć w naszej defini cji) dwóch obiektów klasy CSox, które są
interesujące , gdyż związane jest sumą (cokolwiek jego operandami.
będzi e
Co więc mamy na myśli, m ówi ąc o sumie dwóch obiektó w? Istni eje co najmniej kilka od powiedzi na to pytan ie, ale dla naszych potrzeb wystarczy co ś prostego. Sumę dwóch obiek tów klasy CSox zdefiniujemy j ako obiekt klasy CSox wy starczaj ąc o duży, aby pomi eści ć dwa p o zo s tałe obiek ty ( p u de ł ka) um ieszczone jeden na drugim . Możemy tego dokonać poprzez dodanie do nowego obiektu składowej m_Length , której warto ś ć b ędzie równa wartości większej s kładowej m_Lengt h dwóch dodawan ych obiektów. W podobny spos ób utworzymy zmie nną s kładową m_Wi dt h. Zmienna s kładowa m_Hei ght będzie s umą zmiennych składowych m_Hei ght dodawanych obiektów. W ten sposób powstanie obiekt klasy CSox mogący pomie ści ć dwa inne obiekty tej samej klasy. Nie jest to może rozwiązanie najbardziej opty malne, ale na nasze potrzeby wystarczające . Zmien i aj ąc konstruktor, sprawimy także, że skła dowa m_Length obiektu klasy CSox zawsze będzie wi ęk sza lub równa s kł ad o wej m_W id th . Om awianą wersję powyższa
operatora dodawania najłatwiej przedstaw i ć w formie g raficznej . koncepcja został a przedstaw iona na rysunku 8.4.
Cała
Jako że potrzebujem y bezpo średn ieg o dostępu do s kład owych klasy, funkcję operator+( ) zde finiujem y jako funkcję s k ładową. Deklaracja tej funkcji w obrębi e defin icji klasy wygląda nast ępuj ąco:
~ Box
operator+(const CBox&aBox) const:
II Funk cj a
dodają ca
dwa obiekty klasy CBox.
Parametr z d e fin i ow al i ś my jako referencję w celu uniknięcia niep otrzebn ego kopiowania pra wego argum entu w momencie wywołania funk cji. Słowo kluczowe const zastosowane zostało ze w zględu na fakt, że funkcja w żade n sposób nie mod yfikuje przyjmowanych argumentów. Jeżeli nie zadeklarujemy param etru j ako s tałej referencji, to kompil ator nie pozwol i na prze kazan ie do funkcj i stałeg o obiektu, co z kolei uniemożliwiłoby zastosowanie jako prawego operandu operatora + stałego obiektu klasy CSox. Fu nkcj ę również zadeklarowaliśmy jako stałą, g dyż nie wpływa ona w ża den sposób na obiekt, dla którego jest wywoływana. Bez tego lewy operand o peratora + nie mógłby być stałym obiektem klasy CSox. Definicja funkcji ope rat or+()
wygląda n astępująco:
II Funkcj a dodająca dwa obiekty klasy CBox .
CBox CBox : :operat or+(const CBox&aBox) const { II Nowy obiekt ma dlugość i
szerokoś ć większego
obiektu oraz
s umę
ich wysokości.
return CBox( m_Length > aBox.m_Length ? m_Lengt h:aBox.m_Lengt h. mWidth > aBox.m Widt h ? mWldt h:aBox.mW i dt h.
Rozdzia. 8. • L. _
~L=30
T
l
r-
. ~ Maksymalna ~ - - dłuqo ś ć "
T :1
boxl
W =20
H
'"
15
~
L = 25
na łemał klas
465
~
box2
W = 25
~=
Więcej
H = 10
"------------"
- - - - · - -1- - - - - - - - -
I--
Maksymalna s ze rokość
, T l ', ---->- W =25
Suma wysoko ści
lL = 30
Y
---J
boxl+box2
,
~ H
, ,,
=15+10
=25 ~~
Rysunek 8.4
Lokalny obiekt klasy CBox konstruujemy z b ieżącego obiektu (*t hi s) i obiektu przekazanego jako argument - aBox. Należy pamiętać, że w procesie zwracania tworzona jest tymczasowa kopia obiektu lokalnego i to ona jest zwracana z powrotem do funkeji wywołuj ącej , a nie obiekt lokalny u sunięty podczas zwrotu funkcji .
RmnlIiI Ćwiczenie dodawania W poniż szym programie zobaczymy, jak działa nasz
p rzeładowany
operator dodawania:
II Cw8_06.cpp II Dodawanie obie któw klasy CBox.
#i nclude usi ng st d: :cout:
us mq st d: :endl :
II Dla strumienia
wejśc ia-wyjścia.
466
Visual C++ 2005. Od podstaw class CBox
II Definicja klasy o zas ięgu g loba lnym.
{
publ r e :
l0
II Defin icj a konstruktora.
CBox( double lv = 1.0. double wv
1.0. double hv
=
1.0): m_Height( hv)
(
mLengt h = lv > Wy? lv: Wy; m)idt h = wv < lv? wv l v:
I
II Fu nkcja
II Upewnienie s ię, że Il length > = width.
obliczająca p ojemność pudełka.
doubl e Vol ume( ) const
{
} II Funkcj a ope ratora większoś ci
II p orównująca pojemności obiektów klasy CBox.
int CBox : :operat or>( const CBox&aBox) const
{
ret urn thi s->Vol ume( ) > aBox. Volume():
}
II Funkcj a porówn ująca obiekt klasy CBox ze stalą .
i nt operat or>(const double&val ue) con st (
ret urn Volume() > value; II Funkcj a dodająca dwa obiekty klasy CBox.
CBox operat or+(eonst CBox& aBox) const ( II Nowy obiekt zloż ony z dłu ż szej dlugosci i sze rokości oraz sumy
wys okości.
ret urn CBox(m_Length > aBox.m_Lengt h7 m_Lengt h:aBox .m_Lengt h. m_Width > aBox .m_Widt h7 m_Widt h:aBox.m_Widt h. m_He ight + aBox.m_Height ): II Fu nkcj a po kazująca wymia ry pudelka.
II Długoś ć pudełka w centymetrach. II Szerokość pudelka w centymetrach. II Wys okość pudelka w centymetrach.
}:
int operat or>(const dauble&va l ue. const CBax&aBox); i nt ma in()
{
II Prototyp funkcji.
RozdziałB.
CBox CBox CBox CBox
•
Więcei na temat klas
467
sma llBox(4.0, 2.0, 1.0) ; med i umBox(10 .0, 4 O. 2.0) ;
aBox:
bBox:
aBox ~ smal l Box + med i umBox;
cout « "Wymi ary obiektu aBox :
aBox.ShowBox() ;
bBox = aBox + smal lBox + mediumBox:
ymiary obiekt u bBox:
cout « "W bBox.ShowBox() ,
ret urn O: II Funkcja porównująca s talą z obiektem klasy
Clłox.
int operato r>(const double&val ue, const CBox&aBox)
{
ret urn val ue > aBox .Vol ume();
}
Do klasy CBox będziem y jeszcze w tym rozdziale wracać kilka razy, a więc warto sobie ten fragment, gdyż będzie on jeszcze potrzebny.
zapamiętać
Jak lo działa Dla potrzeb tego programu zmieniłem nieco składowe klasy CBox. Jako że nie był nam tym razem potrzebny, usunęliśmy destruktor tej klasy oraz zmodyfikowaliśmy konstruktor w taki sposób, że składowa m_Length nie może być mniejsza niż m_Widt h. Dzięki informacji, że dłu gość pudełka obiektu nigdy nie jest mniejsza od jego szerokości , operacja dodawania jest nieco łatwiejsza. Dodałem również funkcję ShowBox() w celu wy świetlenia na ekranie wymiarów obiektu klasy CBox. Dzięki tej funkcji będziemy mogli s i ę zorientować , czy nasz przeładowany operator dodawania działa zgodnie z oczekiwaniami. Rezultat wykonania tego programu jest następujący:
W ymia ry obiektu aBox: 10 4 3 W ymia ry obiekt u bBox: 10 4 6 Dane wyj ś ciowe wskazują, że wszystko się zgadza i - jak widać - funkcja działa również z wielokrotnymi operacjami dodawania w wyrażeniu. W celu obliczenia wymiarów obiektu bBox przeładowany operator dodawania został wywołany dwukrotnie. Tę samą operację
staci funkcji
dodawan ia dla naszej klasy można było również Jej prototyp jest następujący :
zaimplementować
w po
zaprzyjaźnionej .
friend CBox operat or+(const CBox& aBox . const CBox& bBox ); Proces obliczania wyniku byłby dokładnie taki sam, poza koniecznością użycia operatora bez pośredniego dostępu do składowej w celu uzyskania składowych użytych jako oba argumenty funkcji . Podejście to miałoby identyczny skutek jak pierwsza wersja funkcji .
468
Visual C++ 2005. Od podstaw
Przeładowywanie
operatorów inkrementacii idekrementacii
Przed stawi ę
teraz krótk o mechanizm przeł ado wywan ia operatorów inkrementacj i i dekre mentacj i w klasie, poni eważ m aj ą one pewne specjalne wła ś ciwości o dróżn i aj ące j e od innych operatorów jednoargumentowych . Trzeba znaleźć jaki ś sposób na poradzenie sobie z tym, że operatory te mogą występować w dwóch formach (przyrostkowej i przedrostkowej ) i że efekt ich dział ani a w zależn o ś ci od użyt ej formy j est inny. W natywnym C++ implem entacja prze ładow anych operatorów inkrementacji i dekrementacji je st inna dla ka żdej z ich form . Poniżej znaj duje się przykładowy kod d efiniujący te operatory dla klasy o nazwie Length :
class Lengt h {
orivate: doub le len: publi c Lengt h&ooerat or++( ): const Leng t h operator++ (i nt):
II Przedrostkowy operator inkrementacj i. II Przyr ostkowy operator inkrementacj i.
Length&operator --( ) ; con st Lengt h ooerat or--( i nt) :
II Przedrostkowy operator inkrementacji. II Przyrostkowy operator dekrem entacj i.
II D l ugość klasy.
II Reszta kodu klasy...
P owyżs za pro sta kla sa za kł ad a, że długo ść przechowyw ana jest w zmiennej typu doub1e. W rzec zywi stoś ci klasę tę można by było trochę bardziej rozbudowa ć, ale celem p owyższej j est tylko prezentacja sposobu prz eład owyw an ia operatorów inkrementa cji i dekrementacji. Przedrostkow ą i przyrostkową form ę operatorów m ożemy rozróżni ć poprzez l istę parametrów. Operator w fonni e przedrostkowej nie ma żadnyc h parametrów, a w fOJ111ie przyrostkowej ope rator ma param etr typu i nt . Parametr w przyrostkowej fonn ie operatora został zdefiniowany w yłączni e w celu odróżni enia go od operatora w formie przedrostkow ej i nie jest w żade n inny sposób używ any w implementacji funkcj i.
Operatory inkrementacji i dekrement acji w fonni e przedrostkowej zwięks zają lub z mn iej szają operand przed użyciem go w wyrażeniu, a wię c zwracana jest tylko referencja do b ieżąc ego obiektu po jego zwiększeniu lub zmniejszeniu. Przy u życiu formy przyrostkowej operand jest zmniejszany lub zw iększa ny dopiero po użyciu go w wyrażeniu. Efekt ten uzyskuje s ię poprzez utworzenie nowego obiektu, który jest kopią bieżące go obiektu , przed zwiększeniem bie żącego oraz zwr ócenie kopii po zmodyfikowa niu tego ob iektu .
Szablony klas W roz dziale 6. definiow ali śmy szabl on y funk cji , które a.utomatycznie g enerowały funkcje różni ące s ię typem przyjmo wanych argumentów lub typem zwracanym . W C++ istnieje po dobny mechan izm dla klas. Szablon klasy nie jest sam w sobie klasą. Jest czy mś w rodzaju " przepisu" na klasę, w edłu g którego kompilator generuje kod klasy. Jak widać na rysunku 8.5,
Rozdział 8•• Więcej na
T jest parametrem, dla którego
podaje się wartość argumentu, która
jest nazwą typu. Każdy nowy typ
podany w postaci argumentu powoduje utworzenie nowej klasy
wart o ś ć
{
int m_Value;
pa ramet ru T ok reś lo n a ja ko in.
c1ass CExample Wart o ś ć
{
pa rame t ru T
IE-----okr e ślo n a jako double -
T m_Value;
szablon klasy
469
c1ass CExample
template c1ass CExample {
temat klas
Wa r tośl
-
---+I
egzemplarze klasy
double m_Value;
para metr u T okre śl o na jako cSox
'" c1ass CExample { CBox m_Value;
Klasa tworzona jest przy u ży ciu
podanej wa rto ś ci w st awionej w mi ejsce
.«
...---- II
pa ram e tru T w sza b lo nie
~ ~ I~
.. .-•
L-
-'
Rysunek 8.5 podobnie do szablonów funkcji - klasę, którą chcemy utworzyć, okre typ parametru (T w tym przypadku) znajdującego się pomiędzy nawiasami trójkątnymi w szablonie. W wyniku tych czynności powstaje nowa klasa, zwana egzemplarzem szablonu klasy. Proces tworzenia klasy z szablonu nazywa się tworzeniem egzemplarza. szablony klas
działają
ślamy , podając
Właściwa defin icja klasy generowana jest w momencie tworzenia obiektu szablonu klasy dla określonego typu. W zwi ązku z tym można utworzyć dowolną liczbę różnych klas z jed nego szablonu klasy. Najlepiej zrozumiesz to, patrząc na konkretny przykład .
Definiowanie szablonu klasy Definiowanie szablonu klasy przedstawię na bardzo prostym przykładzie. Nie będę kompli rzeczy, martwiąc się zbytnio o błędy, które mogą powstać w przypadku nieprawidło wego jego użycia. Przypu śćmy , że chcemy zdefiniować kilka klas do przechowywania pewnej liczby próbek danych jakiegoś rodzaju i że w każdej funkcji musi znajdowa ć się funkcja Max() określająca maksymalnąwartość przechowywanych danych. Funkcja ta podobna jest do tej, którą widzieliśmy w rozdziale 6., przy okazji omawiania szablonów funkcji . Poniżej znaj duje się definicja szablonu klasy generującego klasę CS arnpl es mogącą przechowywać dane dowolnego typu. kował
t emplate class CSamples (
publ i c:
II Defin icj a konstruktora przyjmującego
tablicę
CSamples( const T values[ J . int caunt)
{
prób ek danych.
470
Visual C++ 2005. Od podstaw m Free = count < 100? count:100: f6r(i nt i = O: i < mJ ree: i +ł ) m_Va l ues [ i] = val ues[i] :
II Nie przekr aczaj rozmiarów tablicy. II Przechowuje liczb ę próbek.
} II Konstruktor przyjmujący pojedynczą p róbkę.
CSamples(const T&va lue) {
m_Values[O] = value: mJree = 1:
II Prz echowuj e próbkę. II Nast ępna jest wolna.
II Defa ult construct or CSamples(){ m_Free ~ O
II Nic nie jest przecho wywane. II a więc pierwsza jest wolna .
II Funkcja dodająca próbkę.
bool Add(const T&va lue) {
bool OK = m_Free < 100: if (OK) m_Val ues[m_Free++] = value: return OK: II Funk cja
II Wskazuj e. że jest wolne miejsce. II Prawda,
w ięc
zap isz
wartość.
spra wdzająca n ajwiększą pró bkę.
T Max( ) const ( II Ustaw pierwszą pró bkę lub Oj ako maks imum.
T t heMax = m_ Free t or rmt i
?
m_Va lues[ OJ
= l: i < mFree: i++) i f (m_Val ues[i ] > t heMax) t heMax = m_Va lues[l] ; ret urn t heMax:
O: II Sprowdź wszystki e pr óbki . II Zapisz
dowolną większą prćbkę .
}
pri vat e:
T m_Values[l OO]: t nt mJ ree:
II Tablica prze ch owują ca dane.
/r Indeks wolnej lokaliza cji
II w tablicy m_Values.
}:
Celem zaznaczenia, że definiujemy szablon klasy, a nie zwykłą klasę, przed słowem kluczo wym cl ass oraz nazwą klasy CSampl es umieściliśmy słowo kluczowe t empl at e oraz parametr typu T w trójkątnych nawiasach. Składnia ta jest identyczna ze składnią, której używaliśmy do definiowania szablonów funkcji w rozdziale 6. Parametr Tjest zmienną typu, która zostaje zastąpiona właściwą nazwą typu podczas deklaracji obiektu klasy . Każde pojawienie się w definicji klasy parametru Tjest zastępowane typem podanym w jej deklaracji. W ten sposób tworzona jest definicja klasy odpowiadająca temu typowi. Można podać dowolny typ (pod stawowy lub klasowy), ale musi on oczywiście mieścić się w granicach rozsądku w kontekście szablonów klas. Każdy typ klasy używany do utworzenia egzemplarza klasy z szablonu musi m ieć zdefiniowane wszystkie operatory, które będą wykorzystywane przez funkcje składowe z takimi obiektami. Jeżeli na przykład dana klasa nie ma zaimplementowanej funkcji ope rator-- t ), to nie będzie działała z szablonem klasy CSamp1es . Do tego jeszcze wrócimy trochę później.
Rozdział8.
•
Więcei na
temat klas
471
Wracając
do przykładu, typ tablicy, w której przechowywane sąpróbki, został określony za symbolu T. Dzięki temu tablica ta będzie przechowywała dane takiego typu, jaki podamy dla symbolu T podczas deklaracji obiektu klasy CSamp l es. Jak widać , parametr typu T użyty został w dwóch konstruktorach klasy oraz w funkcjach Add( ) i Max( ). Każdy egzem plarz tego parametru zostaje zastąpiony podczas tworzenia obiektu klasy za pomocą szablonu . pomocą
obiekt, obiekt z pojedynczą próbką oraz obiekt zainicj alizowany Funkcja Add() pozwala na dodawanie po jednej jednostce do obiektu. Można by było tę funkcję przeładować w celu dodania tablicy jednostek. Szablon klasy zawiera pod stawowy środek zapobiegający przed przekroczeniem pojemności tablicy m values w funkcji Add ( ) oraz w konstruktorze przyjmującym tablicę próbek.
Konstruktory
tworzą pusty
tablicąjednostek.
Jak już wcześniej powiedziałem , teoretycznie można tworzyć obiekty klasy CSamples obsłu gujące wszystkie typy danych : typ i nt, doubl e lub jakikolwiek zdefiniowany przez programistę typ klasowy. W praktyce jednak nie zawsze da się wszystko skompilować i nie zawsze działa to tak, jak przewidywaliśmy. Wszystko zależy od tego, co robi definicja szablonu . Dany sza blon zazwyczaj działa prawidłowo tylko z określonym zestawem typów. Na przykład funkcja Max ( ) wymaga dostępności operatora >, bez względu na przetwarzany typ danych . Jeżeli go nie ma, to programu nie będzie można skompilować. Oczywiście, zazwyczaj będziesz defi niować szablon działający tylko z niektórym i typami, ale nie ma sposobu na wprowadzenie ograniczeń co do typu stosowanego do szablonu.
Funkcje składowe wszablonaełl klas Może się zdarzyć, że
zechcemy umieścić definicję funkcji składowej szablonu klasy poza szablonu. Sposób wykonania tego zadania nie jest wcale oczywisty, a więc przyj rzymy się temu bliżej. Deklarację funkcji w szablonie klasy umieszczamy w normalny sposób. Na przykład: definicją
template
class CSamples
{
II Reszta de lnic 'i sza blonu...
T Max( ) const:
II Funkcja
znajdująca największą próbkę.
II Reszta defini cji szablonu...
Powyższy
kod deklaruje funkcję Max( ) jako składową klasy, ale jej nie definiuje. Teraz musimy oddzielny szablon funkcji dla definicji funkcji składowej. Musimy użyć nazwy sza blonu klasy z parametrami w nawiasach trójkątnych w celu zidentyfikowania szablonu klasy, do której szablon funkcji należy:
utworzyć
template
T CSamples: :Max() const
{
T t heMax
=
m_Val ues[O ];
for( int i = 1: i < mJ ree; i++) if (m_Val ues[i ] > t heMax) t heMax = mVal ues[i] :
II Ustaw pierwsząpró bkęjako maksimum. II Sprawdź wszystkie próbki . II Zapisz
dowolną większą p rćbkę.
472
Visual C++ 2005. Od podstaw ret urn theMax: Składnię szablonu funkcji widzieliśmy już w rozdziale 6. Ze względu na fakt, że ten sza blon funkcji tworzony jest dla funkcji będącej składową szablonu klasy z parametrem T, defi nicja szablonu funkcji powinna m ieć tutaj takie same parametry jak definicja szablonu klasy. W tym przypadku jest tylko jeden parametr (T), ale może ich być więcej. Jeżeli szablon klasy m iałby dwa lub większą liczbę parametrów , to tyle samo miałby szablon definiujący funkcję składową,
N al eży zwró ci ć uwagę , że
nazwa parametru T razem z nazwą klasy zostały umieszczone przed operatorem zasięgu . Jest to konieczne - parametry są podstawą do identyfikacji klasy, do któ rej n al eży funkcja utworzona z szablonu. Podczas tworzenia egzemplarza szablonu klasy ty pem jest CSamp 1es-T> z nazwą typu przypisaną do symbolu T. Podany typ zostaje wstawiony do szablonu klasy w celu wygenerowania definicji klasy oraz do szablonu funkcji w celu wyge nerowania definicji funkcji Ma x() w tej klasie. Każda klasa utworzona z szablonu musi mi e ć własną definicję funkcj i M ax O. Defin iowanie konstruktora lub destruktora poza definicją szablonu klasy wygląda podobnie. Poniżej znajduje się przykładowa defmicja konstruktora przyjmującego t ablicę próbek:
t emplat e CSamples: :CSamplesC T val ues[ ] . i nt count) (
m_Free = count < IDO? count :lOO ;
II Nie przekraczaj rozmiarów tablicy.
tor rt nt i = O: i < mFree : i++ ) m_Values[ i ] = va lues[i ] :
II Zap isz
liczbę próbek.
Klasa, do której należy konstruktor, jest określona w szablonie w podobny sposób jak w przy padku zwykłej funkcji składowej. Warto zwrócić uwagę , że nazwa konstruktora nie wymaga określenia parametru (jest to po prostu CSamp 1es), ale potrzebny jest za to kwalifikator w po staci typu szabl onu klasy CSamp 1es- T>. Parametru z nazwą szablonu klasy używa się tylko przed operatorem zasięgu.
Tworzenie obiektów klasy szablonu funk cji zdefiniowanej za pomocą szablonu funkcji, kompilator potrafi tę z użytych typów argumentów. Parametr typu szablonu funkcji jest nie jawnie zdefiniowany przez określone użycie danej funkcji . Szablony klas są trochę inne. W celu utworzenia obiektu na podstawie szablonu klasy w deklaracji zawsze należy po d a ć parametr typu po nazwie klasy . Kiedy
używamy
funkcję wygenerować
N a przykład obiekt CSamp 1es<> sposób:
obsługujący
typ doub1e można
zadeklarować
w
następujący
CSamples<double> myDat aCI O.O ) : Powyższy
kod definiuje ob iekt typu CSample s<double>, który może przechowywać jednostki typu doubl e. Obiekt ten po utworzeniu będzie przechowywał jednąjednostkę o wartości 10. O.
Rozdział 8.• Więcei na temat klas
473
~ Tworzenie szablonu klasy Możem y ut w o rzy ć
obiek t z szablonu CSampl es-> przechowujący obiekty klasy CBox. To klasa CBox ma zaimp le mentowaną funkcję operato r>( ) p rzeładowującą ope rator większośc i. Nasz szab lon klas y p rz eć w i c zymy z fun k cją mai n() na po n iższym kodzie:
działa, po nieważ
II Cw8_ 07.cpp
II Używan ie szablonu klasy.
#include
using st d: :cout :
using std: :end l :
II Wstaw tutaj definicję klasy CBox z p rog ramu CwS_0 6.cpp .
II Definicja szab lonu klasy CSamp les.
t emplate class CSamples (
publi c. II Konstruktory
CSamp les( const T va l uesE ] . i nt count i :
CSamples(const T&va lue) :
CSamp les() { m_Free = O; }
bool Add (const T&va lue) : T Max() const : pri vate : T m_ValuesE IOO]: int mJ ree:
II Wstawianie jakiejś warto soi. II Oblic za nie maksimum .
II Tab lica przec h owująca próbki. II Indeks wolnej lokalizacj i w tabli cy m Values .
}: II Defini cja sz ablonu konstruktora przyjmującego
ta blicę próbek.
t emplate CSamples: :CSamples(const T val ues[]. i nt count ) (
m_Free = count < 100? count :l OO: for(i nt i = O: i < m Free: 1++)
m_Va l ues[i ] = va l ues[i ] :
II Nie przekraczaj rozmiaru tablicy .
II Przechowuje licz b ę próbek.
II Kons trukto r przyjmujący pojedynczą jednostkę .
t emp late CSamples: :CSamples(const T&val ue) {
m_Values[O] rnJree = 1;
=
va l ue ;
II Zapi suj e próbkę .
II Następn a j es t wolna.
II Funk cja dodaja ca jednostkę.
templat e bool CSamples : :Add(const T&valuel (
bool OK = m_Free < 100; i HOK) m_Va lues[m_Free++] = value: return OK:
II Wskaz uje, II Prawda.
ż e jest
wię c
woln e miejsce.
zap isz
wartość.
II Funk cj a znajdująca maksymalną jednos tkę .
temp lat e T CSamples: :Max( ) const {
T t heMa x = mFree? mVal ues[O] ; O:
II Usla w pierwszą próbkę lub O jako maksimum.
474
Visual C++ 2005. Od podstaw for (int i = 1; i < mFree: i++) lf(m_Val ueS[l] > theMax) t heMax = m_Val ues[ i] : return t heMax :
II Sprawdź wszystkie p róbki. II Przechowuje dowolną większą p ró bkę.
int ma in() (
CSox boxes[ ]
II Utwórz tablicę z obiektów p udelek.
CBox(8.O. 5.O. 2.O) . II Zainicja lizuj je... CBox(S .O. 4.0. 6.0). CBox( 4.0. 3.0. 3.0) }; II Tworzen ie obiektu CSamp les w celu przechowywania obiektow klasy CBox.
CSamples myBoxes(boxes. si zeof boxes / siz eof CBox); CBox maxBox = myBoxes.Max() : cout « endl « " N a jw ięk sze pud e ł k o ma « ma xBox.Volume()
«
II Pobierz naj większy obiekt II i wyś wie t l j ego pojemność.
p o j emn o ś ć "
endl :
ret urn O; W miejsce komentarza na początku programu należy wstawić definicję klasy CBox z programu Cw8_o6.cpp . Nie musimy przejmować się funkcją operator>( l po zwalającą porównywać obiekty z wartościami typu doubl e, ponieważ ten przykład jej nie wymaga. Wszystkie funkcje składowe szablonu, z wyjątkiem konstruktora domyślnego, zostały zdefiniowane za pomocą odrębny ch szablonów funkcj i w celu pokazania na pelnym przykładzie, jak się to robi. W funkcji mai nO utworzyliśmy tablicę trzech obiektów klasy CBox, którą nast ępnie wykorzy staliśmy do zainicjalizowania obiektu klasy CSa rnp l es mogącego przechowywać obiekty klasy CBox. Definicja obiektu klasy CSamp l es jest w zasadzie taka sama, jak gdyby była to zwykla klasa, ale z dodatkiem parametru typu w trójkątnych nawiasach umieszczonych po nazwie szablonu klasy . Program generuje
następujący
N aj w i ęk sze pud eł ko
ma
wynik:
pojem ność
120
Zauważ, ż e
tworzeniu egzemplarza szablonu klasy nie towarzyszy tworzenie egzemplarza sza blonów funkcji s kła d ow ych . Kompilator tworzy egzemplarze tylko tych szablonów funkcji składowych, które zostaną rzeczywi ście wywołane w programie. W rzeczywistości funkcje te mogą nawet zawierać błędy i dopóki ich nie wywołujemy , kompilator nie zgłosi komunikatu o błędzie . Możemy to sprawdzić na przykładzie. Wprowadź kilka błędów do szablonu funkcj i składowej Add(l . Program nadal się kompiluje i działa bez zarzutów, ponieważ nie jest w nim wywoływana funkcja Add ( l . Możesz spróbować dokonać działo
modyfikacji powyższego programu i sprawdzić, co s i ę przy tworzeniu egzemplarzy klasy przy użyciu szablonu z różnymi typami.
Możesz się zdziwić, widząc,
będzie
co się dzieje po dodaniu instrukcji wyjściowych do konstruk torów klasy. Konstruktor klasy CBox jest wywoływany aż J03 razy! Przyjrzyjmy się. co się
Rozdział 8.
•
Więcej
na temat klas
475
dzieje w funkcji main( J. Najpierw tworzona jes t tablica trzech obiektów klasy CBox, a wię c mamy trzy wywolania. Następnie tworzymy obiekt klasy CSamp 7es w celu ich przechowy wania, ale obiekt klasy CSamp 7es zawiera 100 zmiennych typu CBox, a wię c konstruktor domyślny zostaj e wywolany 100 razy - raz dla każdego elementu tablicy. Oczywiś cie, obiekt maxBox zostanie utworzony przez domyślny konstruktor kopiujący dostarczony przez kompilator.
Szablony klas zwieloma paramelrami Używanie
wielu parametrów typu w szablonie klasy je st prostym rozszerzeniem wcześniej w którym używaliśmy jednego parametru. Każdego z parametrów typu mo żemy używać w dowolnym miejscu definicj i szablonu. Spójrzmy na przykład definicji sza blonu klasy z dwoma parametrami typu : szego
przykładu,
t emplat e
class CExampleClass
{
II Zmienne sk ładowe klasy.
private :
Tl m_Va luel.
T2 m_Va lue2:
II Reszta definicji szabl onu...
}:
Typy obu pokazanych zmiennych składowych klasy są określone przez typy podane w miejsce parametrów w momencie tworzenia egzemplarza obiektu. Parametry w szablonach klas nie są ograniczone do typów. Można także używać parametrów, za które w definicji klasy mają być podstawione stałe lub wyrażenia stałe. W naszym szablonie CSampl es arbitralnie zdefiniowaliśmy tabli cę m_Values zawierającą 100 elementów. Możemy również pozwolić użytkownikowi, aby to on okre ślił rozmiar tablicy podczas tworzenia obiektu. Definicja takiego szablonu przedstawia się następująco:
template class CSamples rivat e : T mVal ues[SizeJ: int mJree: pub l ic:
II Tablica przechowująca pr óbki. II Indeks woln ej loka lizacj i w tablicy m_Valu es.
II Definicja konstruktora przyjmują cego tablicę próbek. CSamples(const T values[J. t nt count ) [
mFree = cou nt < Size? count: Size:
II Nie przekraczaj rozmiar ów tablicy.
for(int i ~ O: i < mJ ree: i ++ ) m_Va l ues [i J = val ues[i J:
II Przechowuj e
II Konstrukt or przyjmują cy pojedynczą próbkę .
CSamples(const T&va l ue)
{
m_Val ues[O J = value:
II Zapisz próbkę.
liczb ę
pr óbek.
476
Visual C++ 2005. Od podstaw m Free = 1: II Konstruktor
II Następnajes t wolna.
domyślny.
CSamples () {
mJ ree = O;
II Nic nie j est p rzecho wywane, a Il jest wolna.
więc p ierwsza
} II Funk cja
dodająca próbkę.
int Add (const T&val ue) int OK = m Free < Size ;
II Wskazuje, że jest wolne miej sce.
lf
m_Val ues[m_Free++] = va l ue; return OK :
II Prawda.
wi ęc
zap isz
wartość .
II Funk cj a znajdująca n aj większą próbkę.
T Max() const { II Ustaw pierwsz ą próbkę lub Oja ko maksimum .
T t heMax = m_Free ? m_Val ues[OJ : O:
rorrmt
i = l: i <
mFr ee :
II Sprawdź wszystkie próbki.
i ++ )
if (m_Val ues[ i ] > t heMax) t heMax = m_Val ueS [i] ; ret urn t heMax;
II Zap isz dowo lną
większą wartoś ć.
}: Wartość podana dla parametru Siz e podczas tworzeni a obiektu wstawiana je st w jego miejsce w definicji szablonu. Teraz obiekt klasy CSamp l es z poprzedniego przykładu możemy zade klarować następująco :
CSamp les M yBoxes(boxes. sizeof boxes/ sizeof CBox) ; Jako
że
w miejsce parametru Size
można wstaw ić
łyc h , powyższą deklarację mo żem y zapi sać
w
dowoln e wyrażenie sposób:
składając e si ę
ze sta
na stępujący
CSamples
M yBoxes(boxes , si zeof boxes /s izeof CBox):
Ten przykład zastosowania szablonów nie jest jednak godny polecenia - oryginalna wer sj a była o wiele bardziej użyte c zna. Konsekwencją zastosowania Si ze jako parametru sza blonu jest fakt, że egzempl arze szablonu przech owuj ące ob iekt y tego samego typu, ale mające różne wartości parametru Si ze są całkiem innymi klasam i i nie mo gą być mieszane. Na p rzykład obiektu typu CSamp l es-doubl e , 10> nie mo żna użyć w wyrażeniu z obiektem typu CSamples<double . 20>. Tworząc
egzemplarze sza blonów, trzeba zachować szc ze gó l ną ostrożność przy z u życiem operatorów porównywani a. Przyjrzyjmy s i ę poniżs zej instrukcji:
CSamples y ? 10
20 > M yType( );
II Źle'
wyrażeniach
Rozdział 8.• Więcej na
temat klas
477
Powyższego kodu nie będzie można poprawnie skompilować , gdyż znajdujący się przed y znak> jest traktowany jako zamykający trójkątny nawias. Instrukcja to powinna zostać zapi sana następująco:
CSamples y ? 10
20) > MyType();
II Dobrze...
Dzięki okrągłym nawiasom unikniemy pomieszania wyrażenia będącego drugim argumentem szablonu z nawiasem trójkątnym.
Używanie klas Omówiliśmy wszystkie podstawowe zagadnienia związane z tworzeniem klas w natywnym C++, dobrze by było, gdybyśmy teraz nauczyli się je stosować do rozwiązywania problemów programistycznych. Przykład, którym się posłużę, będzi e bardzo prosty, aby utrzymać rozsądną liczbę stron w tej książce . W związku z tym posłużę się rozszerzoną wersją klasy CBox.
Interfejs klasy Implementacja rozszerzonej wersji klasy CBox powinna zbiec się z wyjaśnieniem pojęcia inter fejsu klasy. Planujemy utworzyć zestaw narzędzi dla każdego, kto chce pracować na obiek tach klasy CBox, a więc musimy stworzyć zestaw funkcji, który będzie stanowił interfejs do świata "pudełek" . Jako że interfejs jest jedynym sposobem pracy z obiektami klasy CBox, musi on zostać zaprojektowany w taki sposób, aby jak najlepiej odpowiadał na potrzeby tego, kto chce z nimi pracować . Jego implementacja powinna zapewniać maksymalną o chronę przed nieprawidłowym użyciem klasy lub przypadkowymi błędami . Pierwszą rzeczą,
chcem y
nad którą należy się zastanowić, projektując klasę , jest natura problemu, który Na podstawie tych przemyśleń należy zdecydować , jaką funkcjonalnoś ć w interfejsie klasy.
rozwiązać .
zapewnić
Definiowanie problemu Główną funkcją pudełka
jest przechowywanie obiektów różnego rodzaju , a więc krótko pakowanie. Utworzymy klasę , która złagodzi problem y z pakowaniem, a następ nie zobaczymy, w jaki sposób można jej użyć . Zakład amy , że zawsze będziemy pakować obiekty klasy CBox do innych obiektów klasy CBox, ponieważ j eż e li chcemy zapakować do pudełka cukierki, to każdy cukierek można przedstawić jako idealny obiekt klasy CBox. Pod stawowe operacje, które nasza klasa powinna wykonywać, to:
mówiąc -
• Obliczanie pojemności pudełka. Jest to podstawowa właściwość obiektu klasy CBox i mamy już jej implementację . • Porównywanie pojemności dwóch obiektów klasy CBox w celu określenia, który jest większy. Dobrze by było zaimplementować pełną obsługę operatorów porównywania obiektów klasy CBox. Do tej pory mamy implementację operatora >.
478
Visual C++ 2005. Od podslaw • Porównywanie pojemności obiektu klasy CBox z określoną warto ścią i odwrotnie. Mamy już taką operację zaimplementowanądla operatora >, ale będziemy potrzebować także obsługi pozostałych operatorów porównywania. • Dodawanie dwóch obiektów klasy CBex, w wyniku czego powstanie nowy obiekt tej klasy zawierający oba obiekty składowe . W wyniku tej operacji powstanie obiekt o pojemności co najmniej równej sumie pojemno ści s kł ad o wy c h obiektów. Mamy już wersję , która przeładowuje operator +. • Mnożenie obiektu klasy CBex przez liczbę całkowitą i odwrotnie, w wyniku czego powstanie nowy obiekt zawierający określoną liczbę obiektów. To już jest projektowanie pudła. •
Okraś lenie , ile obiektów klasy CBex danego rozmiaru można upakować w innym obiekcie klasy CBex o podanym rozmiarze. Do wykonania tego trzeba po służyć się operatorem dzielenia , a więc będziemy musieli przeładować operator /.
•
Określanie pojemności
wolnego miejsca pozostałego po upakowaniu maksymalnej liczby obiektów klasy CBex o danym rozmiarze.
Lepiej już się zatrzymam! Bez wątpieniajest wiele innych bardzo pożytecznych funkcji, ale w dobrze pojętym interesie drzew poprzestaniemy na tych, które wymieniłem . Oczywiście będziemy jeszcze potrzebować różnych funkcji pomocniczych, takich jak uzyskiwanie dostępu do wymiarów pudełek.
Implementacja klasy wziąć pod uwagę, jaki poziom bezpieczeństwa chcemy zapewnić naszej klasie przed rodzaju błędami . Podstawowa klasa zdefiniowana w celu ilustracji różnych aspektów klasy może posłużyć jako punkt wyjścia, ale niektóre punkty musimy przemy śleć dokładniej . Konstruktor klasy ma taką słabość , że nie sprawdza, czy podane wymiary obiektu są prawidło we, a więc na początek dobrze by było dodać sprawdzanie, czy mamy prawidłowe obiekty. Podstawową klasę możemy ponownie zdefiniować w następujący sposób:
Musimy różnego
class CBox
II Definicj a klasy o z as ięgu globalnym.
(
publ ic: II Definicja konstruktora.
CBox(doub1e 1v ; 1.0. doub1e wv ; 1.0. double hv
~
1.0)
(
1v ; 1v wv ~ wv hv ~ hv
<; <; <;
O? 1.0: 1v: O? 1.0: wy: O? 1.0: hv :
m_Length ~ 1v > mWldt h ; wv m_Height ~ hv: II Funkcja
l v: wy: 1v? wy : 1v:
Wy ?
<
obliczająca pojemność pudelka .
doub1e Vo1 ume( ) eonst
{
ret urn m Lengt h*m Wi dt h*m Height :
II Sprawdzanie, czy II podane wymia ry obiektu mają II wartoś ci dodatnie. II Sprawdzanie, czy Il length >= wid/h .
Rozdział8 .• Więcei na
temat klas
479
II Funkcj a sprawdzająca dł ugos ć p udelk a.
double Get Length( ) const { ret urn m_Lengt h; II F unkcj a sprawdzają ca szerokoś ć pudelka .
double GetWidth ( ) const { ret urn m_W idt h; } II Funkcja
sprawdzająca wys okoś ć pudelka .
dou ble Get Height ( ) const { ret urn m_Height; private : double m_Length ; double mWidt h; doub le m_Hei ght ;
II Dlugos ć pudelk a w centy metrach. II Szerokoś ć pudelka w centymetrach. II Wysokoś ć pudelka w centymetrach.
};
Teraz konstruktor jest już bezpieczny, ponieważ każdy wymiar podany przez użytkownika będący liczbą ujemną lub zerem zostanie automatycznie zamieniony na jeden. Można także zastanowić się nad wyświetleniem komunikatu informującego o wystąpieniu błędu w przy padku podania wartości ujemnej lub zerowej. Oczywiście takie niejawne ustawienie wymia rów na l też może okazać się nie najlepszym rozwiązaniem. Domyślny
konstruktor kopiujący w przypadku naszej klasy całkowicie wystarcza, gdyż nie mamy w niej żad n yc h składowych z dynamicznie przydzielanąpamięcią. Domyślny operator przypisania również będzie działał jak należy i podobnie ma się sprawa z domyślnym destruk torem, którego także nie musimy definiować . Zastanówmy się teraz, co jest potrzebne do wyko nywania operacji porównywania obiektów naszej klasy.
Porównywanie obiektów klasy CHoJ( Powinniśmy dodać obsługę
operatorów >, >=, ==, < oraz =< W taki sposób, aby dział ały one przy z oboma operandami w postaci klasy CBox, jak również gdy jeden operand jest obiek tem, a drugi wartością typu doubl e. Możemy je zaimplementowaćjako zwykłe funk cje glo balne, ponieważ nie muszą one być funkcjami składowymi. Funkcje porównujące pojemność dwóch obiektów klasy CBox możemy napi sać , opierając s i ę na funkcji porównującej pojemność obiektu klasy CBox z wartością typu doubl e, a więc zaczniemy od tej drugiej . Na początku możemy powtórzyć funkcję operator>(), którą napisaliśmy wcześniej; użyciu
II Funk cja
spra wdzająca.
czy
stała jest
>
niż
obiek t klasy CBox.
i nt operat or>(const dauble&value. const CBox&aBox) (
ret urn va l ue > aBox,Val ume() ,
W podobny sposób II Funkcja
możemy teraz napisać fu nkcj ę
sprawdzająca,
czy
sta ła j est
<
niż
ope r at or<( ):
obiekt klasy CBox.
i nt operat or« const doubl e& val ue. canst CBox&aBax)
{
ret urn val ue }
<
aBox Vo l ume( ) ;
480
Visual C++ 2005. Od podstaw Na podstawie zdefiniowanych przed ch wilą funkcj i mo żemy teraz te same operatory, ale przy zmieni onej kolejno ści argumentów: II Funkcj a spra wdzająca, czy obiekt klasy Citox j est >
n iż
napi sać
kod
implementujący
sta/a.
i nt operato r>(const CBox&aBox. const doubl e&va l ue) { retur n va lue < aBox ; } II Funkcj a sp rawdzająca, czy obiekt klasy CBoxjest < n iż sta ła .
int operato r«const CBox& aBox. const doub le& value) { ret urn val ue > aBox; } W razie zmiany kolejności argumentów w przeładowaną na j ed n ą z powyższych .
wywołaniu
nowej funkcji zmieniamy tylko
funkcj ę
Funk cje impl em entujące operato ry >= i <= są takie same jak dwie pierwsze funkcje, tylko w miejsce < i > należy wstawić <= i >=. Nie ma potrzeby p rzepisywać ich tutaj ponownie . Funk cje oper at or==() również są bardz o podobne: II Funkcja sprawdzająca, czy
i nt
op e rato r ~ ~(const
s ta ła
równa j est poj emnosci obiektu klasy CBox.
double& va lue. const CBox&aBox)
(
retu rn val ue == aBox.Vo l ume (); } II Funkcja sprawdzająca, czy obiekt klasy CBox j est równy stalej.
i nt ope rator ==(const CBox& aBox . const double&value)
{
ret urn value == aBox;
}
W ten sposób mamy
pełny
zestaw operatorów porównywania dla obiektów klasy CBox. Należy tylko wtedy, gdy wyrażenia dają wyniki właściwego typu, ich używać także z innymi przeładowanymi operatorami.
pamiętać , że działają one praw idłowo dzięki
czemu
można
~ączenie obiektów klasy CBOK Zajmiemy się teraz kwestią operatorów +, * oraz %w podanej kolejności. Prototyp operacji dodawania, którą utworzyliśmy w program ie CwS_06.cpp, wygląda nastepująco :
CBox operator+(const CBox& aBox);
II Funkcja dodająca dwa obiekty klasy Cllox.
Mimo że oryginalna implementacja nie jest idealnym rozwiązaniem, to użyjemy jej tutaj, aby uniknąć nadmiernej komplikacji klasy . Lepsza wersja sprawdzałaby, czy operand y nie mają którejś ze ścian o takich samych wym iarach , oraz łączyłaby je właśnie tymi śc ia nami , ale kod takiej operacji byłby trochę skomplikowany. Oczywiście, gdyby miał to być program o praktycznym zastosowaniu, to obe cną wersję tej operacji można by było p óźni ej za stąpić jej ulepszoną wersją, a programy używające tej oryginalnej nadal działałyby, bez kon ieczn ości dokonywania w nich jakichkolwiek zmian . Oddzielenie interfejsu do klasy od jej implemen tacji jest wyznacznikiem dobrego stylu programowania w C++ . Zauważ, że
nie
miało
ze względu na wygodę pominąłem operator odejmowania. To roztropne przeocze miejsce, aby uniknąć komplikacji związanych z implementacją tego operatora. Je żeli
Rozdział 8.• Więcei na temat klas
481
n aprawdę
masz o c h o tę i wydaje Ci si ę to rozs ądnym pomy słem, mo żes z dać mu szansę - ale musisz podj ąć decyzj ę , co zro b ić w przypadku, gdy wynik będzie liczb ą uj emną. J eżeli pozwo lisz na wykon ywani e takic h dzi ałań , musisz sp raw dz ić, któr y wym iar lub które wym iary będ ą ujemne i co zro b ić z takim obiektem w dalszych operacjach.
Operacja mnożenia jest bardzo prosta. Pole ga na utwor zeniu obiektu zaw i e r ającego n obiek tów, gdz ie n jest mnożn iki em. Najpros tszym rozwi ązani em byłoby zapakowanie zmiennych m_Lengt h i m_Wi dt h obiektu oraz p omn ożeni e wys o ko śc i przez n w celu otrzymania now ego obiektu klasy CBox. Możemy być jednak sprytniejsi i sprawdzi ć, czy mnożnik jest li c zbą parzy s tą, a j eżeli je st, u stawić pudełka jedno obok drugiego, podwajając wartoś ć zmienn ej m_Wi dt h oraz mnożąc warto ś ć zmiennej m_Hei ght przez połowę n. Mechanizm ten został przedstawiony na rysunku 8.6. Oc zywi ście ,
nie musimy s p rawdzać , co w nowym obiekcie jest większe - długo ś ć czy szero konstruktor automatyc znie to sprawdzi. Fu nkcj ę operato r*( ) możemy zapi s ać jako fu nkcj ę s kł ad o w ą z lewym operandem w postaci obiektu klasy CBox: kość , p onieważ
II Operator m nożen ia klasy CBox this*n
CBox operator *(int n) const { i f ( n % 2)
return CBox(m_Lengt h. m~Wi d t h. n*m_Height) ; else return CBox(m_Lengt h. 2.0*m_Width. (n/2)*m_Height ):
II Mnożn ik n j est nieparzysty. II Mno ż nik n j est parzysty.
W powyższym kodzie do sprawdzania par zyst oś ci mn o żnika n u żyl i śm y operatora %. J eżeli n je st nieparzysty, to wynik d ziałan i a n % 2 wynosi l i instrukcja warunko wa if zwraca war to ść t r ue. Je ż eli n jest parzysty, to wynik dział ania n % w wynosi O i instrukcja zwraca war tość f a1se. Mo żemy
tera z wła śnie napisanej funkcji uży ć do implementacji wersji z lewym operan dem w postaci liczby całkowitej. Możemy ją zap is ać jako zwykłą funkcję (a niejako funk cję s kład ową):
II Operator mnożenia klasy CBox n *aBox.
CBox operator*(i nt n. const CBox& aBox) {
ret urn aBox*n; tylko kolejno ś ć operand ów, dzięki czemu mogli jej poprzedniej wersji. Na tym kończy się zestaw operatorów aryt metyczn ych dla klasy CBox, które zdefiniowali śmy . Na koni ec zajmiemy się jesz cze dw oma analityc znymi funkcjami operatorów - operat or I ( ) oraz oper at or%( ) .
Y Analizowanie obiektów klasy CBol Jak już powiedziałem, operacja dzielenia określa liczbę obiektów klasy CBox, identyc znych z obiektem podanym jako prawy operand, który może pomieścić obiekt klasy CBox podany jako lewy operand . Dla zachowania prostoty zakładamy, że wszystkie obiekty klasy CBox są usta wione w pozycji pionowej. Dodatkowo zakładamy, ż e wszystkie te obiekty ustawione są w taki sposób , że ich długości występują w jednej linii . Bez tych założeń kod byłby bardzo skomplikowany. Powstaje problem określen ia , ile obiektów znajdujących s i ę po prawej stronie operatora można umieścić na jednej warstw ie, a następnie określenia, ile warstw możemy uzyskać w obiekcie znajdującym się po lewej stronie operatora. Kod ten
możemy zapisać
w postaci funkcji
składowej :
Rozdział 8.
•
Więcei na temat klas
483
i nt operato r/ Ceonst CBox& aBox) {
i nt tel = O;
II Zmienna tymczasowa przech owująca liczbę w płaszczyźnie poziomej II w j eden sposób, II Zmie nna tymczaso wa przecho wująca liczbę w płaszczyźnie II w drugi sp osób ,
i nt t eZ = O;
t el = statie_east C (m_Lengt h / aBox,m_Length ))* st at le_east CCm_Widt h / aBox,m_Wi dt h)) ; t eZ = stat le_east C Cm_Length st at ie_east C Cm_Wi dth
II Dopasowanie II wjeden sposób
aBox,m_Widt hJ)* aBox.m_Length)) ;
II i w drug i sposób.
II Zwraca najlepsze dopa sowanie.
ret urn sta t ie_eastC Cm_Height/ aBox ,m_HeightJ*C te l>teZ ? t el
t eZ)) ;
Powyższa
funkcja sprawdza, ile obiektów klasy CBox podanych po prawej stronie operatora na warstwie , wyrównując je względem długości obiektu podanego jako lewy operand. Wynik zapisywany jest do zmienn ej t el. Następnie sprawdzane jest, ile obiektów zmie ści się na warstwie, na której obiekty z prawej strony operatora zo staną umieszczone wzdłuż boku stanowiące go szerokość obiektu podanego jako lewy operand. Na koniec war tość większej z dwóch zmiennych t el i t eZ mnożymy przez liczbę warstw, które możemy upakować , i zwracam y wynik . Proces ten został przedstawiony na rysunku 8.7. można umieścić
\
1\
aBox
W=6
<,
W=2~
H=lb::JJ
\
w= 2 "'~,.--------4\ 8/ 3 = 2
bBox 6/2
=3
bBox
8/2 =4
1\
er
cr
cr
ox
ox
ox
CD
\ bBox
cr
ox
bBox bBox
<, 2/ 1 = 2 "<,
W tej konfiguracji można przechowywać 12 pudełek
CD
er
cr
cr
ox
ox
ox
CD
CD
1\
cr
CD
ox
<,
\ \
2/1 = 2 ~
\
~
Wynik równy jest 16
Rysunek 8.7
\
CD
6/2 = 3 CD
bBox
CD
\
484
Visual C++ 2005. Od podstaw Sprawdzamy dwie możliwości : wstawianie obiektu bBox do obiektu aBox, ustawiając bok okreś laj ący j ego dłu go ś ć wzdłuż boku długo ści obiektu aBox, a następnie wzdłuż boku sze ro koś ci tego obiektu. Na rysunku 8.7 widać, że najlepszy wynik osiągni emy , przekręc aj ąc obiekt bBox w taki spo sób , że j ego bok szero kośc i przystaje do boku długo ści obiektu aBox. Drugi oper ator analityczny - oper atort O, służący do sprawdzania p ojemności wolnego obszaru poz ostałe go w obiekcie aBox, jest prostszy , ponieważ możemy u żyć operatora, który dopiero napi s aliśmy , w celu zaimplementowania j ego funkcj onalności . Funkcję tego operatora moż emy z apisać jako zwy kł ą funkcję globalną, ponieważ nie potrzebuje ona dostępu do składowy ch klasy znajduj ących się w sekcj i pr i vate . II Operator z wracający pojem ność wolnej
return aBox.Vol ume() - «a Box/ bBox)*bBox.Vol ume()): Przeprowadzenie obliczeń jest bardzo proste przy użyciu istni ejących funkcji klasowych . Wynik jest rezultatem odjęci a od poj emności dużego kartonu aBox zsumowanych pojemnośc i małych pudełek bBox do niego włożonych . Liczba obiektów bBox upakowanych do obiektu aBox jest obliczana za pomocą wyraż eni a aBox/bBox, w którym użyty j est w cześni ej przeładowany operator. Wartość tę mnożymy przez pojemność obiektów bBox w celu uzyskan ia wartośc i , którą należy odjąć od pojemno ści dużego kartonu aBox. W ten sposób zakończyl i śmy pracę nad interfejsem klasy. Oczywi ś cie jest wiele innych funkcji, które mogłyby być potrzebne do rozwiązania różnych innych problemów, ale to, co mamy do tej pory, w zu p ełnoś ci wystarczy jako model rozw iązania określonego problemu. Pójdziemy teraz o krok dalej i wypr óbuj emy to, co s two rzy l i ś m y na prawdziwym problemi e.
~ Oparty na wielu plikach proiekt zwykorzystaniem klasy CBo. mogli rozpocząć rzeczywiste pisanie kodu używającego klasy CBox wraz zjej operatorami, jej defini cj ę musimy złożyć w jedną zrozumiałą c ałoś ć . Podejśc i e do tego będzie się ró żniło od dotychczas stosowanego pod tym względem, że projekt będzie składał się z kilku plik ów. Zaczniemy także używać narzędzi dostępnych w Visual C++ 2005 do tworzenia i konserwowania kodu klas . Oznacza to, że mamy mniej do zrobienia, ale także to, że kod w niektórych miejscach będzie nieco inny.
Zanim
będziemy
przeładowanymi
Zacznijmy od utworzenia nowego projektu konsol owego WTN32 o nazwie Cw8_0 8. Podczas tworzenia projektu zaznaczamy opcję Empty proj ect. Klikając zakł adkę Class View , zobaczymy to, co przedstawiono na rysunku 8.8. W zakładce tej pokazane są wszy stki e klasy projektu, ale oczyw i śc i e w tej chwili jest ona j eszcze pusta . Mimo że nie ma jeszcze żadnych defini cji klas (lub czegokolwiek z nimi zw ią zanego) Visual C++ 2005 przygotował s ię już na ich pojawienie. Visual C++ 2005 możemy wykorzystać do utworzenia szkieletu naszej klasy CBox oraz plików, które są z nią powiązane . Kliknij prawym przyciskiem myszy Cw8_08 w panelu Class View i wybierz Add/Class z menu, które się pojawiło . Następnie wybieramy C++ z kategorii klas w lewym panelu, a potem C++ Class po prawej stronie i naciskam y klawisz Enter. W oknie Generic C+ + Class Wizard, które s ię pojawi, możem y podać nazwę tworzonej klasy - CBox, jak pokazano na rysunku 8.9.
Rozdział 8.• Więcej na temat klas
Rysunek 8.8
485
.... J}.X
c lass V telfl/
lO. - .. J.d] <5earch> I iti
· .0 '\
;:a ewB_OB
~ C lass V'ew ~ Resouice View
Rysunek 8.9
Generic CH Class Wizard -
rv
ewa_os --- ---------ffirg)
Welcome to the Generic C+ + ela ss Wizard
rY ~ss nam e :
"" ~h
II
C.nceł
l'
Plik, którego nazwa Box.cpp pojawiła się w oknie dialogowym, zawiera implementację klasy. Składają się na nią definicje funkcji składowych klasy. Jest to kod wykonywalny klasy. Nazwę tego pliku można zmienić, ale wydaje się, że zaproponowana przez program jest bardzo dobra. Definicja klasy będzie przechowywana w pliku o nazw ie Box.h . Jest to standardowy sposób przedstawienia struktury programu. Kod zawierający definicje klas przechowywany jest w plikach z rozszerzeniem .h, a kod definiujący funkcje w plikach z rozszerzeniem .cpp. Zazwyczaj każda defmicja klasy umieszczana jest w odrębnym pliku z rozszerzeniem .h, a implementacja każdej klasy w odrębnym pliku z rozszerzeniem .cpp . Po kliknięciu przycisku Finish
l
mają
miejs ce dwa zdarzenia:
Utworzony zostaje plik o nazwie Box.h , który zawiera szkielet klasy CBox. Zawiera on bezargumentowy konstruktor i bezargumentowy destruktor.
2. Utworzony zostaje plik Box.cpp zawierający szkie letową implementację funkcji klasy z definicjami konstruktora i destruktora -
ci ała
obu funkcji
są oczywiście
puste.
486
Visual C++ 2005. Od podstaw Okno edytora powinno wyglądać jak na rysunku 8.10. Jeżeli go nie widać , to nie kliknąć CBox w panelu Class View - wt edy powinno się pojawić.
Rysunek B.l0
należy
dwukrot... x
•/lI ox-h" I (GlobaI5cope) ( El #pragma once
clll
v
=
r
2] L 3~8
c res e CBox
-I:
(
5~ S!
7:
pub lic : CBox (voi d ] ; pub 1 1c :
e:
9:
,
- CBo:< (vo id ) ; ) ;
10
~
.11
<Je;,
~i
-
~ ), :-
Jak widać na rysunku, nad panelem zawierającym kod klasy znajdują s ię dwa menu rozwijane. Menu po lewej stronie pokazuje nazwę bieżącej klasy (tutaj CBox). Po rozw inięciu tego menu ukaże s i ę nam lista wszy stkich klas projektu. Za pomocą listy tego menu można przełączać się pomiędzy klasami, ale w naszym przypadku znajduje się na niej tylko jedna klasa . Prawe menu rozwijane zawiera s kład ow e zdefiniowane w pliku z roz szerzeniem .cpp b ieżącej klasy. Po rozwinięciu tego menu ukaże nam się lista składowych klasy. Wybranie składowej z tej listy spowoduje pojawienie się jej kodu w poniższym panelu. Tworzenie klasy CBox rozpoczniemy od tego, co Visual C++ 2005 matycznie.
może wykonać
za nas auto-
Deliniowanie klasy CHoI Kliknięcie znaku + znajdującego się po lewej stronie napisu CwS_OS w panelu Class View spowoduje rozwinięcie drzewa, w którym zobaczymy, że klasa CBox została już zdefiniowana dla projektu. Drzewo to przedstawia wszystkie klasy projektu. Kod źródłowy defin icji danej klasy można zobaczyć , dwukrotnie klikając jej nazwę w drzewie lub wybieraj ąc j ą z menu znajdując ego się nad oknem edytora, jak opisywałem wcześniej .
Wygenerowana definicja klasy CBox na samym
początku zawiera dyrektywę preprocesora:
#pragma ance Dyrektywa ta zapobiega wielokrotnemu otwarciu i dołączeniu pliku do kodu źródłowego przez kompilator w czasie jednej kompilacji. Zazwyczaj definicja klasy włączana jest do kilku plików projektu, ponieważ każdy plik , w którym znajduje się odniesienie do nazwy o kreś lonej klasy, musi mieć dostęp do jej definicji. Może się zdarzyć, że plik nagłówkowy zawiera dyrektywy #i ncl ude dołączające inne pliki nagłówkowe. W ten sposób zawartość jednego pliku nagłówkowego może znaleźć się w kodzie źródłowym w więcej niż jednym miejscu. Posiadanie
Rozdział 8.
•
Więcei na lemal klas
487
więcej niż jednej definicji danej klasy w kompilacji je st niedozwolone i zostanie zgłoszone jako błąd. Umieszczenie dyrektywy #pragma na początku każdego pliku nagłówkowego chroni nas przed taką sytuacją.
Należy pamiętać , że
dyrektywa #pragma once została wymyślona przez firmę Microsoft i może nie działać w innych ś rod ow i s kac h programistycznych. Przewidując, że może zajść potrzeba kompilacji pisanego przez nas kodu także w innych śro d ow is ka c h, możemy u żyć w pliku nagłówkowym następującej dyrektywy z takim samym skutkiem: . II Plik nagłó wkowy Box.h.
#ifndef BOX H #define BOX- H II Kod. który nie może zos tać dołączony II na przykład definicja klasy CBox.
więcej n iż jeden
raz.
#end if Istotne wiersz e zostały umieszczone na przyciemnionym tle i zawierają dyrektywy obsługi wane przez każdy kompilator C++ ISO/ANSI. Wiersze znajdujące się pomiędzy dyrektywami #i fndef i #endi f dołączane są do kompilacji, dopóki symbol BOX_Hnie zostanie zdefiniowany. Wiersz znajdujący s i ę po #i f ndef definiuje symbol BOX_H, zapewniając w ten sposób, że kod w tym pliku nagłówkowym nie jest dołączany drugi raz. A więc efekt jest taki sam jak w przypadku umieszczenia dyrektywy #pragma once na początku pliku nagłówkowego. Oczywiście, dyrektywa #pragma ance jest prostsza , a więc lepiej je st używać jej w przypadku, gdy nie planujemy kompilować kodu w innym kompilatorze niż Visual C++ 2005. Czasami można spotkać na stępujący zapis kombinacji dyrektyw #i fndef i #endi f :
#i f Idef ined BOXH #def ine BOXH II Kod, który nie moż e z osta ć do łączo ny II na przykład definicj a kłasy CBox .
więcej n iż jeden
raz,
#end1f Plik Box.cpp wygenerowany przez kreator klas zawiera
następujący
kod:
#include "Box.h" CBox: :CBox(void) { }
CBox: :-CBox(void) { }
Pierwszy wiersz zawiera dyrektywę #i ncl ude preprocesora, która dołącza zawartość pliku Box.h (definicj ę klasy) do tego pliku. Jest to konieczne ze względu na to, że kod w pliku Box.cpp odnosi się do nazwy klasy CBax, której definicja musi być dostępna, aby nazwa ta miała jakieś znaczenie .
488
Visual C++ 2005. Od podstaw
Dodawanie zmiennych składowych Dodamy teraz prywatne zmienne składowe m_Length, m_Widt h oraz m_Height . W tym celu klikamy prawym przyciskiem myszy w panelu Class View, a następnie wybieramy Add/Add Variable z menu kontekstowego. Następnie w oknie dialogowym Add Member Variable Wizard możemy podać nazwę , typ oraz sposób dostępu do pierwszej zmiennej składowej, którą chcemy dodać do klasy. Sposób określania nowej zmiennej składowej za pomocą tego okna dialogowego jest bardzo prosty. Jeżeli dla danej zmiennej składowej określimy dolny limit, to musimy podać także limit górny. Jeżeli podamy limity, to definicja konstruktora w pliku .cpp zostanie zmodyfikowana poprzez dodanie domyślnej wartości dla zmiennej składowej odpowiadającej limitowi dolnemu. W polu na samym dole możemy wpisać komentarz. Po kliknięciu przycisku OK nastąpi dodanie zmiennej do definicji klasy, włącznie z komentarzem, jeżeli został podany . Te same czynności powtarzamy dla pozostałych dwóch zmiennych składowych m_width i m_Hei ght. Definicja klasy w pliku Box.h wygląda teraz następująco:
#pragma once class CBox {
public: CBox( void) : publ ic : -CBox(void): pri vate: II Dlugość pudelka w centym etrach.
double m_Length: II Sz ero kość pudelka w centymetra ch.
double m_Width: II
Wys okość
pudelka w centymetrach.
doubl e mHeight: }: Oczywi ście deklaracje te, jeżeli chcemy, możemy wpisywać również ręcznie wprost do kodu. Zawsze mamy wybór, czy chcemy skorzystać z funkcjonalności IDE. Można także usunąć wszystko , co zostało wygenerowane automatycznie, ale trzeba pamiętać , że czasami zarówno pliki z rozszerzeniem .h, jak i z rozszerzeniem .cpp podlegają modyfikacjom.
Dobrym pomysłem jest zapisywanie wszystkich plików za każdym razem , gdy ręcznie dokonuje s ię jakichś zmian, ponieważ powoduje to aktualizację informacji widocznych w panelu Class View. Zaglądając
do pliku Box.cpp, widzimy, że kreator dodał także listę inicjalizacyjną do definicji konstruktora dla dodanych przez nas zmiennych składowych. Każda zmienna zostanie zainicjalizowana wartością O. Teraz zajmiemy się modyfikacją konstruktora, aby robił to, co chcemy.
Rozdział 8.
•
Więcej
na lemal klas
489
Definiowanie konstruktora Musimy zmi e nić dekl arację konstruktora bezargum entowego w definicji klasy, tak aby posiadał argumenty z domyślnymi warto ściami: CBox(doub le l v = 1.0. doub le wv = 1.0. double hv = 1.0): Teraz jesteśmy gotowi do jego implementacji . Otwieramy plik Box.cpp , j eżel i nie jest jeszcze otwart y, i modyfikujemy definicj ę konstruktora w n astępujący sposób: CBox ' 'CBox(double l v. doub le
Wy .
doubl e hv)
(
lv wv hv
~ ~ =
lv <= 0.0 <= 0.0 hv <= 0.0
WV
? ? ?
1.0 1. 0 1.0
lv, Wy :
hv:
m_Lengt h = l v>wv ? l v : wy: m_Widt h = wv
II Sp rawdzanie. czy II poda ne wymia ry obiektu II mają wartoś c i doda tnie. II Upewnienie s ię. że
Il length >= width.
) Pami ętaj , że warto ści inicjalizuj ące parametrów funkcji składowych powinny znaleźć się tylko w deklaracji składowej w definicj i klasy, a nie w definicji funkcji. Jeżeli umieścisz je w funkcji, kod nie zechce się skompilować . Kod ten już wcze śniej w idzieliśmy, a więc nie będę go omawiał ponownie . Dobrym p omysłem w tym momen cie byłoby zapisanie pliku poprzez klikni ę cie przycisku Save na pasku narzędzi. Wyrób sobie nawyk zapisywania edytowanego pliku przed przejściem do czegoś innego. Jeżeli potrzebujesz ponownie edytować konstruktor, możesz się do niego z łatwości ą dostać poprzez dwukrotne kliknięcie jego wpisu w dolnym panelu zakładki C/ass View lub wybranie go z menu rozwijanego znajdującego s i ę po prawej stronie nad oknem , w którym wy św ietlany jest kod.
Do definicji funkcji składowej można także dostać s i ę bezpośrednio w pliku z rozszerzeniem .cpp lub do jej deklaracji w pliku z rozszerzeniem .h, klikając prawym przyciskiem myszy jej nazwę w panelu C/ass View i wybierając odpowiedni element z menu kontekstowego , które się pojawi.
Dodawanie funkcii składowych Musimy teraz dodać do klasy CBox wszystkie funkcje, które widziel iśmy wcześn iej. Poprzednio zdefiniowaliśmy kilka funkcji składowych w obrębie definicji klasy, a więc funkcje te są automatycznie funkcjami inline. Ten sam rezultat można osiągnąć poprzez ręczne wprowadzenie kodu tych funkcji do definicji klasy. Możemy również skorzystać z kreatora Add Member Function . Może się wydawa ć , że każdą funkcj ę
inline można zdefiniować w pliku z rozszerzeniem .cpp oraz dodać słowo kluczowe i nl i ne do definicji tych funkcji. Problemem jest jednak to, że funkcje te okażą się funkcjami nieistniejącymi. Jako że kod każdej funkcji inline musi zostać wstawiony bezpo średnio w miejsce , w którym dana funkcja została wywołana, definicje tych funkcji muszą być dostępne w momencie kompilacji pliku zaw ierającego wywołania tych funkcji. Jeżeli ich nie będz ie , to progr am łączący zgłosi błąd i programu nie będzie
490
VisIlai C++ 2005. Od podstaw mo żna uruchomi ć . J e śli
chcesz, aby funkcje składowe były funkcjami inline, musisz włączyć ich definicje do pliku z rozszerzeniem .h klasy. Definicje te można umieści ć albo wewn ątrz definicji klasy, albo bezpo średn i o po niej w pliku z rozszerzeniem .h. Wszystkie globalne funkcje inline należy um ieśc ić w pliku z rozszerzeniem .h oraz dołączyć ten plik do wszystkich plików z rozszerzeniem .cpp, które ich używają, za pomocą dyrektywy #i nc1ude. Aby dodać funk cję GetHei ght ( ) jako inline, należy kliknąć prawym przyciskiem myszy CBox w panelu Class View, a następnie z menu kontekstowego wybrać Add/Add Function. Następni e w oknie dialogowym , które s ię pojawiło , wprowadzamy dane definiujące funkcj ę , ja k pokazano na rysunku 8.11 .
Rysunek 8.11
L'1J[8]
Add Member Function Wizard - ew8_08 Welcome to the Add Member Function Wi,ard
Fynctoo name:
Parametertype ;
]
[Illl a ccęss :
CommentUlnotationnot required);
Flnlsn
II
Concel
Typ zwracany można określić jako doub 1e, wybierając go z rozwijanej listy, ale równie dobrze można go po prostu wpisać . Oczywi śc ie w przypadku typu, którego nie ma na liście , musimy go wpisać ręcznie z klawiatury. Aby funkcja zosta ła utworzona jako inline, należy zaznaczyć opcję inline. Pozostałe opcje tworz enia funkcj i lo static, virtual oraz pure. Jak nam wiadomo, składowa funkcja statyczna istnieje niezależn ie od jakichkolwiek obiektów klasy. Do funkcji wirtualnych (vi rt ual ) i czystych funkcji wirtualnych (pure vi rt ua1) dojdziemy w rozdziale 9. Funkcja GetHeight () nie ma żadn y ch parametrów, a więc nie musimy dodawać nic więcej . Kliknięcie przycisku OK spowoduje dodanie definicji funkcji do definicji klasy w pliku Box.h. Po powtórzeniu tego procesu dla funkcji s k ład o wych GetWi dt h( ), GetLength () oraz Vol ume() definicja klasy CBox będzie wyglądała na stępuj ąco :
#pragma once class CBox {
publ ic: CBox(doubl e l v = l O. double wv = 1.0. doub le hv = 1.0) ; -CBox(void) : pri vat e: II Długoś ć pudełka w centymetrac h.
double m Lengt h;
II Szeroko'ić pudełka w centymetrach.
Rozdziala.•
Więcei na temat klas
491
dou ble mWidth; II
Wysokołć pudelka
w centymetrach.
double m_Hei ght: publ ic: dou ble GetHei ght(void) {
ret urn O: }
double GetWidth(void ) {
ret urn O; }
doubl e GetLength(void) {
return O; } II Obliczanie pojemności pudelka.
doubl e Vo lume(void) {
return O: } };
Do klasy została dodana dodatkowa sekcja publiczna zawierająca definicje funkcji inline. Musimy jeszcze zmodyfikować każdą z tych definicji, aby podać prawidłowy typ zwracany oraz zadeklarować je jako stałe (const). Na przykład kod funkcji GetHeight () powinien wyglą dać następująco:
double GetHeight (void) con st retu rn mHei ght:
Definicje funkcj i GetW i dth() i GetLength () możemy zmienić w podobny sposób. Definicja funkcji Volume() powinna wyglądać następująco: double Volume (void) const return m Length*m Wi dth*m Hei ght; Pozostałe funkcje składowe, które nie są typu inline, możemy dodać bezpośrednio w oknie edytora lub oczywiście za pomocą kreatora Add Member Function , a taka praktyka nam się przyda. Kliknij prawym przyciskiem myszy CBox w zakładce C/ass View i z menu kontekstowego, które się pojawi , wybierz jak poprzednio Add/Add Function . Następnie dodajemy szczegóły dotyczące pierwszej z naszych funkcji w oknie dialogowym (rysunek 8.12).
Powyżej zdefiniowaliśmy funkcję operator+()
CBox. Typ parametru oraz jego nazwa kliknięciem przycisku Finish należy
jako
kliknąć
funkcję publiczną z
typem zwracanym podane w odpowiednich polach. Przed przycisk Add w celu zarejestrowania nowego
zostały również
492
VisIlai C++ 2005. Od podstaw
Rysunek 8.12
f?lrg]
Add Member Function Wizard - ewB_OB Welcome to the Add Member Funct ion Wizard
.cpp [ile:
Ac c~SS :
C"'JY1lCf1l Ul not.tion not ' e(lUt e
· ~~ oper.tor do-:--:· dowa,," · .-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
--,
· FIJllctl'YJS1';]:15tIJ1r.:
- - - - - ---- - - -- - - - - - - -- - - - - 1 I
Fi11S!l
II
Cor cel
parametru na liście parametrów . Czynno ść ta powoduje takż e uaktualnienie sygnatury funkcji widocznej na dole okna dialogowego kreatora Add Member Function. Po dodaniu parametru do listy można podać następny parametr, jeżeli jest ich w ięcej, i ponownie kliknąć Add. Dodałem także komentarz w oknie dialogowym, który zostanie umieszczony przez kreator w plikach Box.h i Box.cpp. Kliknięci e przycisku Finish spowoduje dodanie deklaracji funkcji do definicji klasy w pliku Box.h oraz szkieletu definicji funkcji w pliku Box.cpp. Funkcja ta musi być stała , a więc musimy dodać słowo kluczow e const do deklaracji funkcji w obrębie definicji klasy oraz do jej definicji w pliku Box. cpp. Do c iała funkcji musimy jeszcze dodać następujący kod:
CBox CBox : :operator +(const CBox& aBox) const (
II Nowy obiekt jest dłuższy i sze rszy od tych dwóch Il oraz sumy ich wysokości .
ret urn CBox(m_Length > aBox .m_Length ? m_Length : aBox.m_Length. m_Width > aBox.m_Widt h ? mWidth aBox.m_Width. m_Hei ght + aBox .m_Height ); Ten sam proces musimy powtórzyć dla funkcji ope rat or*( ) i operator/ O, które już widzieliśmy wcześniej. Po dodaniu tych funkcji definicja klasy w pliku Box.h wygląda następująco ;
#pragma once class CBox (
publ i c: CBox (double lv = 1.0. double wv = 1.0. double hv = 1.0); -CBox(void): private : II Długość pudełka w centymetrach.
double m_Lengt h; II Sz erokoś ć pudelka w centymetrach.
Rozdział 8.
•
Więcej
na temat klas
493
doub le mWidt h;
II Wysokosć pudelka w centymetrach.
doub le m_Hei ght ; pub l te: double GetHeight (void) (
)
publ t e : dou ble Get Widt h(void) {
ret urn mJ .Ji dth ; } publ ic:
doub le Get Lengt h(void) {
ret urn m_Length; } publ ic :
doub le Vol ume(void) const (
)
publ i c: II Przeładowany operator dodawania.
CBox operat or+(const CBox&aBox) const; pub l lC ; II Mnożenie obiektu klasy CBox przez
liczbę calkowitą .
CBox operator*( int n) const ; publ i c: II Dzielenie obiektów klasy
Cłiox jeden
przez drugi .
int operator/ (const CBox&aBox) const ; }; Powyższy
kod
można edytować
trzymając się reguł poprawności
i przestawiać jego elementy w dowolny sposób (oczywiście kodu ). Dodałem dodatkowe puste linijki, aby ułatwić czytanie
tego kodu . Zawarto ś ć
pliku Box.cpp ostatecznie wygląda następująco:
#i nclude "Box .h" CBox; ;CBox( doubl e l v. doub le
Wy .
double hv)
{
1v = l v wv = WV hv = hv
<= <= <=
0.0 ? 1.0 0.0 ? 1.0 0.0 ? 1.0
m Lengt h = l v>wv
7
lv ;
l v; WY;
hv; Wy ;
II Sprawdzanie. czy II podane wymiary obiektu II mają wartości dodatni e. II Spra wdzanie, czy
494
Visual C++ 2005. Od podstaw mWi dth = wv
l v;
Il length > = width .
CBox : :- CBox( voi d) ( }
II Prze ładowany ope rator dodawan ia.
CBox CBox: :operat or+Ceonst CBox& aBox) eonst (
II No wy obiekt j est dluższy i szerszy od tych dw óch II i sumy ich wysokości.
ret urn CBox(m_Length > aBox.m_Lengt h ? m_Lengt h aBox.m_Lengt h. m_Widt h > aBox.m_Wldth ? m_Width aBox.m_Width, mHeight + aBox.mHei ght ) ; II Mnożen ie obiektu klas y CBox
CBox CBox: :operat or*(i nt n) eonst if (n%2) ret urn CBoxC m_Lengt h. m_Widt h. n*m_Height) ; else ret urn CBox (m Lengt h, 2.0*mWidth. Cn / 2)*mHeight ) ;
II Mnożn ik n jest nieparzysty.
/r Mno żn ik n jest pa rzysty.
II Dzielenie jedn ego obiektu przez drugi .
int CBox: :operator/ Ceonst CBox& aBox) eonst
r II Tymczasowa zmienna przechowujqca
i nt t el
=
II Ty mcz asowa zmienna przech owująca
int t e2 t el t e2
=
=
=
liczbę
w płaszczyźnie poz iomej w j eden sp osób.
liczb ę
w płaszczyźn ie w inny sposób.
O: O:
st atie_eastCCm_Length/ aBox.m_Lengt h))* st et tccast -nnt >: (m_Width/ aBox,m_Widt h)) ; st at ie_east ((m_Lengt h/ aBox.m_Widt h))* st at ie_east (Cm_Widt h/aBox.m_Lengt h)):
II Dopasowanie w j eden sposób
II i w drug i sposób.
II Zwra ca najlep sze dopasowanie.
ret urn stat ie east C( mHeight /aBox.mHei ght ))*(t el >te2 ? t el : t e2) ; Wiersze na przyciemnionym tle to wiersze, które powinny zos tać zmodyfikowane lub dodane rę cznie.
Defmicje bardzo krótkich funkcji, czyli tych, które zwracają tylko wartość zmiennej s kładowej, znajdują się wewnątrz definicji klasy, a więc są funk cjami inlin e. Jeżeli klikni emy zakładkę Class View, a nas tępni e znak + zn aj duj ący się obok nazwy kla sy CBox, to zobaczymy, że wszystkie składowe klasy są pokazane w dolnym panelu.
Rozdział B.• Więcej na temat klas
W tym momen cie funkcji globalnych
495
sko ń c zy liśmy prac ę
nad kl as ą CSox, ale pozost ały nam jeszcze definicje operatory p orównuj ące pojemność obiektu klasy CBox z
implementujących
warto ścią liczbow ą.
Dodawanie funkcji globalnych Dla funkcji glob alnych obsługujących operacje na obiektach klasy CSox musimy utworzyć nowy plik z rozszerzeniem .cpp . Plik ten równi eż musi być c zę śc ią projektu . Kliknij zakładkę Solution Explorer (w tej chwili jesteśmy w zakładce Class View), a następnie prawym przyciskiem myszy kliknij ikonę folderu Source Fi/es. Z menu kontekstowego, które si ę pojawi, wybieramy AddINew Item. Pojawia s i ę nowe okno dialogowe. Po lewej stronie w panelu Categories wybieramy Code, a po praw ej stronie w panelu Templates wybieramy szabl on pliku C++ - C++ File (.cpp) oraz wpisujemy nazwę pliku (Name) BoxOperators. Teraz w oknie edyt ora dodajemy
następujący kod:
II BoxOperators.cpp II Operacj e na obiektach klasy Cliox, które nie
wymagają dost ępu
do s kładowych prywatny ch.
#include "Box .h" II Funkcj a sprawdzająca. czy s ta ła jes t > n iż obiekt klasy
Clłox .
bool operator >Cconst double& va lue. const CBox& aBox) ( retu rn va lue > aBox .Volume() ; } II Funkcja
sprawdzająca.
czy s ta ła jes t <
n iż
obiekt klasy CBox.
bool ope rator
Clłox jest
> od s ta łej.
bool operator >(const CBox&aBox . const doub le& value) { return value < aBox ; } II Funkcj a sprawdzająca . czy obiekt klasy
Cłłox j est
< od s ta łej.
bool operator « const CBox& aBox. const doub le& va l ue) { ret urn va lue > aBox ; } II Funkcja
sprawdzająca.
czy s ta ła jes t > = od obiektu klasy CBox.
bool operator>=Ccons t doub le&va l ue. const CBox& aBox) ( return val ue >= aBox.Volume C) ; } II Funkcja sprawdzająca. czy s ta ła jest < = od obiektu klasy CBox.
bool opera tor <=Cconst doubl e&va lue. const CBox& aBox) ( ret urn va lue <= aBo x.Volume C) ; } II Funkcja sprawdzająca. czy obiekt klasy CBoxjest > = od stalej.
boo l operator>=C const CBox&aBox, const double& value) { ret urn va l ue <= aBox; } II Funkcja
sprawdzająca.
czy obiekt klasy CBox jest < = od s ta łej.
bool operato r<=C const CBox& aBox . const double&value) { ret urn va l ue >= aBox; } II Funkcja spra wdzają ca. czy s ta ła j est
==
obiektowi klasy CBox.
bool operator==(const double& va l ue, const CBox& aBox) ( ret urn val ue ~~ aBox.Vol ume C); }
496
Visual C++ 2005. Od podstaw
II Funkcja
sprawdzająca,
czy obiekt klasy CBox jest == stalej.
boo l operator==(const CBox& aBox. const double& va lue) { return value ~~ aBox : } II Operator
mnożenia
klasy CBox - n*aBox.
CBox operat or*(int n. const CBox& aBox ) { retur n aBox * n; } II Operator zwracający pojmeność wolnej przestrzeni zapakowanego obiektu klasy CBox.
double operator%( con st CBox& aBox . const CBox& bBox) ( ret urn aBox .Volume () - (aBox / bBox) * bBox.Vol ume(); } Na początku pliku znajduje się dyrektywa #i ncl ude dołączająca plik Box.h, ponieważ wszystkie zdefiniowane tutaj funkcje odnoszą się do klasy CBax. Zapisz plik. Teraz zajrzyj do zakladki Class View, w której pojawił się nowy folder Global Functions and Variabies zawierający wszystkie dodane funkcje. Definicje wszystkich tych funkcji już wcześniej widzieliśmy, a więc nie będę już tutaj po raz drugi omawiał ich implementacji. Jeżeli zechcesz którąś z tych funkcji umieścić w innym pliku z rozszerzeniem .cpp, musisz się upewnić, że wszystkie funkcje, których używasz, zostały przez Ciebie zadeklarowane i kompilator może je rozpoznać , Można to osiągnąć, wstawiając zestaw deklaracji do pliku nagłówkowego. Przejdź z powrotem do panelu Solution Explorer i prawym przyciskiem myszy kliknij nazwę folderu Header Fi/es . Z menu kontekstowego, które się pojawiło, wybierz Add/New ftem. Następnie w oknie dialogowym wybierz w lewym panelu Code, a w prawym Header File(.h). Wpisz nazwę pliku BoxOperators . Po kliknięciu przycisku Add do projektu zostanie dodany nowy pusty plik nagłówkowy. Wpisujemy do niego następujący kod w oknie edytora; II BoxtIperators.h - deklaracje globalnych operatorów klasy CBox.
#p ragma once bool operator>(const double& va lue. const CBox&aBox ) ; bool operator« const doubl e& va lue. const CBox&aBox ); bool operator>(const CBox& aBox. const double& value) ; bool operat or« const CBox& aBox. const doubl e& value); bool operat or>=(const double&val ue. const CBox& aBox ); bool o p e r a to r <~(co ns t double&value, const CBox& aBox ); bool operator>= (const CBox& aBox. const double&value); bool operator<=( const CBox& aBox . const double&value); bool operator==(const double&value. const CBox& aBox ); bool operator~~( const CBox& aBox. const double& value); CBox operator*(i nt n. const CBox aBox); doubl e operat or%(const CBox&aBox, const CBox&bBox); Dyrektywa #pragma ance zapewnia, że zawartość pliku nie zostanie dołączona więcej niż jeden raz do kompilacji. Teraz wystarczy dodać tylko dyrektywę i-i nc l ude dołączającą plik BoxOperators.h do każdego pliku, który używa którejś z tych funkcji . Jesteśmy już
gotowi do zastosowania tych funkcji razem z klasą CBax do rozwiązania określo nego problemu ze świata pudełek.
Rozdział 8.
•
Więcei na temat klas
497
Zastosowanie klasy CHoI Wyobraźmy sobie, że pakujem y cukierki. Cukierki te są naprawdę duże - prawdziwe potwory , które przechowuje się w opakowaniach o długości 4,5 centymetra oraz wysokości i szerokośc i po 3 centymetry . Mamy dostęp do standardowego pudełka na cukierki o wymiarach 13,5x21 x6 centymetrów. Chcesz dowiedzieć się , ile cukierków wejdzie do jednego pudełka, i na tej podstawie ustalić cenę. Posiadasz także standardowy karton o wymiarach 7,8 x54 centymetry i chcesz się dowiedzieć, ile pudełek cukierków można do niego włożyć oraz ile miejsca pozostanie niewykorzystanego po napełnieniu .
W przypadku gdy standardowe pudełko nie jest dobrym rozwiązaniem, chcesz się dowiedzieć, jakie pudełko robione na zamówienie byłoby najlepsze. Wiesz, że po dobrej cenie możesz kupić pudełka o długości od 9 do 21 centymetrów, szeroko ś c i od 9 do 15 centymetrów oraz wysokości od 3 do 7,5 centymetra. Przeskok jest co 1,5 centymetra. Wiesz także, że w każdym pudle musi sz umieścić co najmniej 30 cukierków, ponieważ jest to minimalna ilość "po c hła niana" przez Twojego największego kJienta za jednym razem . W pudełku nie może być także wolnego miejsca, gdyż klienci myślą, że ich oszukujesz. Dodatkowo dzięki całkowitemu zapełnieniu pudełka cukierki nie latają w śro dku . Nie możemy jednak być zbyt restrykcyjni, ponieważ pakowanie mogłoby okazać się bardzo trudne . Powiedzmy więc, że za brak zmarnowanego miejsca w pudełku uznajemy wo l ną przestrzeń mniejszą niż pojemność jednego pudełka z cukierkiem. Dzięki kJasie CBox problem staje się prawie dziecinnie prosty. Jego rozwiązanie zaprezentowane zostało w poniższej funkcji main ( l . Dodaj nowy plik C++ o nazwie CwS_OS. cpp do projektu za pomocą menu kontekstowego pojawiającego się po kliknięciu prawym przyciskiem myszy Source Fi/es w panelu Solution Explorer, jak robiliśmy poprzednio. Następnie wpisz poniższy kod:
II Cws _os .cpp II Przykładowy pro blem z pakowaniem.
#i nc lude #include "Box .h" #incl ude "BoxOperato rs .h" using st d: :cout : usi ng std : :end l : mt
II Definicj a cukierka. II Defini cja pudelka na cukierki. II Defini cja kartonu.
II Obliczanie liczby cukierków na pudełko.
int numCand ies
~
candyBox/candy;
II Obliczani e liczby pudelek z cukierkami na kar/on.
int numCboxes
=
cart on/candyBox:
II Obliczanie marnowanego miejsca w kartonie.
double space = carton%candyBox: cout « end l « "Wp u d e łk u zmi e s zc zą s i ę " « numCand ies « " cukierki. " « endl « "W ka rtonie zm ieś c i s i ę " « numCboxes « " st andardowych p u deł e k cuk ierków, " « endl
«
"przy zmarnowani u ..
498
Visual C++ 2005. Od podstaw « space « " centymet rów sz eśc i e nnyc h. "; cout « endl « endl « "ANALIZA PUDElKA NA CUKIERKI ROBIONEGO NA ZAMÓWIENIE (nic s i ę nie marnuje)": II Wypróbowanie
całego
zestawu pudełek robionych na zamówienie.
for (doubl e length = 3.0 ; length <= 7.5 ; lengt h += 0.5) for(double width = 3.0 ; wi dth <= 5.0 ; width += 0.5) for (double height = 1.0 ; height <= 2.5 ; height += O 5) { II W każdym cyklu utwórz nowe pudełko.
CBox t ryBox(length, width. height); if( carton%tryBox < t ryBox.Vol ume( ) && tryBox %candy == 0.0 &&try Box/candy >= 30) cout « end l « endl « "Pud ełko próbne L = " « tryBox.GetLength() « " W~ « t ryBox.GetWidt h() « " H ~ " « tryBox.GetHei ght( ) « end l « "Pudełko próbne pomieści" « tryBox / candy « " cuk ierków, " « a do kartonu wejdzie « carton / tryBox « " ta kich p u d e ł ek. " ; }
cout « endl : return O: Spójrzmy najpierw na strukturę programu. Podzieliliśmy go na kilka plików, co jest normalne podczas pisania programów w C++. Pliki te można zobaczyć w zakładce Solution Explorer, którą pokazano na rysunku 8.13 .
Rysunek 8.13
, ~"f! 1orer
~ ~;~
- 5olution 'ewa o... • JI,
[::l 5olutiOn 'ewa_os' 8 .] a ewa_oa 8
x
m
(l project)
~ Header Files
[!li Box.h
l!lI
BoxOperators.h
L:I Resource Flles 8
(C) sourre Files
~ Box.cpp
'8 BoxOperators ,cpp ~ CWS_OS .cpp
'§!Soiut!<,,:, E,'.c:J ijclass View I~ Resour ce .. .
Plik Cw8_08. cpp zawiera funkcję ma i n() oraz dyrektywę #i ncl ude dołączającą plik BoxOperators.h, ·który zawiera prototypy funkcji z pliku BoxOperators.cpp (które nie są składowymi klasy). Dodatkowo plik ten zawiera dyrektywę #i ncl ude dołączającą plik Box.h z definicją klasy CBox. Program konsolowy C++ zazwyczaj składa się z pewnej liczby plików podzielonych na trzy podstawowe kategorie:
l
Pliki z rozszerzeniem .h zawierające biblioteczne polecenia #i ncl ude, stałe i zmienne globalne, definicje klas oraz prototypy funkcji - inaczej mówiąc , wszystko, co nie
Rozdzial·8.•
Więcei na
temat klas
499
jest wykonywalnym kodem. W plikach tych znajdują si ę również definicje funkcji inline . Jeżeli program składa się z kilku klas, to ich definicj e cz ęsto umieszczane są w oddzielnych plikach. zawie rające wykonywalny kod programu oraz polecenia Hi ncl ude dla wszystkich definicj i wymaganych przez ten kod.
2. Pliki z rozszerz eniem .cpp
3. Jeszcze jeden plik z rozszerzeniem .cpp
zawierający funkcję
mai n( ).
Kod zawarty w naszej funkcji main() nie wymaga obszernych wyjaśnień - stanowi on prawie bezpośrednie wyrażenie definicji problemu w s łowa c h, p onieważ operatory w interfejsie klasy wyko nuj ą na obiektach klasy CBox czynnoś ci zorientowane na rozwi ązan ie problemu. Odpowiedź na pytanie dotyczące użycia standardowych pudełek znajduje się w instrukcjach deklaracji , wyliczających także odpowiedzi , których wymagamy jako wartości początkowych. Następn i e te wartości wysyłamy na wyj ście z dodatkiem komentarzy wyjaśniających. Drugą czę ś ć problemu rozwiązaliśmy za pomocą trzech zagni eż dżonyc h pętli przech odzących przez możliwe wartoś c i zmiennych m Lenqt h, m_Width oraz m_Hei ght , sp rawdzaj ąc w ten sposób wszystkie możliwe kombinacje. Moglibyśmy je wszystkie wyświetlić, ale ze względu na fakt, że byłoby ich aż 200, z których interesujących dla nas byłoby tylko kilka, dod aliśmy instrukcj ę warunkową i f identyfikującą opcje rzeczywiście nas interesujące. Warto ś ć tej instrukcji wynosi t r ue tylko w przypadku, gdy nie ma pozostawionego wolnego miej sca w kartonie i w bieżącym próbnym pudełku na cukierki również nie zostaje wolne miejsce, a także kiedy pudełko może pomie ścić co najmniej 30 cuk ierków. Poniżej
znajduje
się
wynik
działania
naszego programu:
Wp u d e ł k u zmieszczq się 42 cuk ie rki . Wkartonie z mi eśc i s i ę 144 st andardowych pu d e ł ek cukierków. przy zmarnowani u 648 centymet rów sze śc ie n nyc h . ANALIZA PUDE Ł KA NA CUKIERKI ROBI ONEGO NA ZAMÓWIEN IE (nlc Pud e ł k o
Pud eł ko P ud eł k o
Pu d eł k o Pud eł k o Pu d e ł k o Pu deł k o Pu dełko Pu dełko P ud e ł k o
si ę
nie marnuj e)
próbne L ~ 5 W= 4.5 H ~ 2 próbne pomi eśc i 30 cukierków. a do kart onu wejdzie 216 t akich
pud e łek .
próbne L ~ 5 W= 4.5 H = 2 próbne p om i e ś c i 30 cukie rków . a do kart onu wej dz ie 216 t akic h
p u deł e k .
próbne L ~ 6 W= 4.5 H = 2 próbne p om ieści 36 cukie rków . a do ka rtonu wejdzi e 180 takich
pu de łek.
próbne L = 6 W= 5 H ~ 2 próbne po m i e ś c i 40 cuk i erków . a do kartonu wej dzie 162 takic h
pu de łek .
próbne L = 7.5 W= 3 H = 2 próbne p om l e ś c i 30 cuk i erków. a do kartonu wej dz ie 21 6 takic h
p ude łe k .
Zduplikowane rozwiązania pojawiły się ze względu na fakt, że w zagnieżdżonej pętli sprawdzane były pudełka o długości 15 i szerokości 13,5 centymetra, a także o długości 13,5 i szerokości 15 centymetrów . Jako że konstruktor klasy CBox pilnuje, aby długość nie była mniejsza od szerokości , oba te rozwiązania są identyczne. Moglibyśmy dodać kod pozwalający uniknąć duplikowania rozw iązań , ale nie jest to warte zachodu. Możesz to potraktować jako proste ćwiczenie .
500
Visual C++ 2005. Od podstaw
Organizowanie kodu programu W stworzon ym przed chwilą programie po raz pierwszy rozdzieliliśmy kod na kilka plików. Jest to nie tylko czę s to stosowane podejście do programowania w C++, ale podejście wręcz ni e zbędne w programowaniu dla Windows. Kod nawet najprostszego programu musi zostać podzielony na nadaj ące się do przetwarzania fragm enty. Jak już mówiłem wcze śniej , istnieją
w program ie w C++ -
dwa podstawowe rodzaje pl ików z kodem źródłowym pliki z rozszerzeniem .h i .cpp . Zo stało to zilustrowane na rysunku 8.14.
Kod wykonywalny odpowiada definicjom funkcji składającym s ię na program . Dodatkowo są także różnego rodzaju definicje niezbędne do poprawnej komp ilacji kodu wykonywalnego. Są to stałe globalne i zmienne , typy danych, do który ch zalicza się klasy, struktury, unie oraz prototypy funkcji. Kod wykonywalny przechowuje si ę w plikach z rozszerzeniem .cpp, a defmicje w plik ach z roz szerz eniem .h. Czasami może s z zec h c i eć użyć kodu z istn iejący ch plików w nowym projekcie. W takim przypadku wystarczy tylko dodać odpowiedn ie pliki z rozszerzeniem .cpp do projektu, wybierając z menu Project op cj ę Add Existing Item lub klikając prawym przyciskiem myszy Souree Fi/es lub Header Fi/es w zakładce Solution Explorer oraz wybi erając z menu kontekstowego Add! Ex isting Item . Nie ma konieczności dodawania plików z rozszerzeniem .h do projektu , chyba że chcemy , aby były one natycłuniast widoczne w panelu Solution Explorer. Kod z plików z rozszerzeniem .h dodawany jest na początku plików z rozszerzeniem .cpp, które ich wymagają ze wzgl ędu na użyt e w nich dyrektywy #i ncl ude. Za pomocą dyr ektyw #i ncl ude dołącza się pliki nagłówkowe zawierające funkcje biblioteki standardowej oraz inne standardowe definicje, a także własne pliki nagłówkowe. Visual C++ 2005 automatycznie ś l edz i wszystkie te pliki i pozwala na ich przeglądanie w zakładce Solution Explorer. Jak przekonali śmy się w ostatnim przykładzie , definicje klas i stałe globalne oraz zmienne możemy obejrzeć w zakładce Class View. W programie Windows są j eszcze inne definicje okreś l aj ące takie rzeczy j ak menu czy przyciski paska narzędzi. Przechowywane są one w plikach z roz szerzeniami .re i .ico. Podobnie jak pliki z rozszerzeniem .h nie mu szą one być jawnie dodawane do projektu , poniew aż są one tworzone i śledzone automatycznie przez Visual C++ 2005 , kiedy ich potrzebuj em y.
Nazewnictwo plików programu Definicje klas, j ak j uż mówiłem , zazwyczaj przechowuje się bez względu na stopień ich skomplikowania w plikach z rozszerzeniem .h, których nazwa odpowiada nazwie klasy. Implementacj ę funkcji składowych tej klasy , które zost ały zdefiniowane poza nią, przechowuje s i ę natomiast w pliku z rozszerzeniem .epp o tej samej nazwie . W ten sposób nasza klasa CSox została umieszczona w pliku o nazwie Box.h, a implementacja tej klasy w pliku o nazwie Box.epp. Nie stosowaliśmy tej konwencji wcześniej w rozdziale, ponieważ przykłady były bardzo krótkie i łatwiej było odnosić się do przykładów kodu za pomocą nazw tworzonych przy użyciu numeru rozdziału oraz numeru listingu w tym rozdziale. Przy tworzeniu większych programów poprawna struktura kodu staje się bardzo ważn a , a więc dobrze by było wyrobić sobie od tej pory nawyk tworzenia plików z rozszerzeniami .h i .epp , by przechowywać w nich pisany kod.
Rozdział 8.
Definicje funkcji
Defini cje funkcj i
-
•
Więcej
na temat klas
501
Defin icja klasy
I
Definicje funkcj i
Defin icja klasy
'---
I
Stałe
Definicja funkcji mainO
globalne
I I
Stale globalne
-
~
~
Pliki źródłowe z rozszerzeniem .cpp
~
Pliki nagłówkowe z rozszerzeniem .h
RVSllnek 8.14 Podzielenie kodu C++ na pliki z rozszerzeniami .h i .cpp jest bardzo wygodnym podejściem, gdy ż ułatwi a ono odnalezienie definicji lub implementacj i dowolnej klasy, w szczegó l n ośc i podczas pracy w środowisku programistycznym nieposiad ającym tylu udogodnień co Visual C++ 2005. Je śli znamy nazwę klasy, to możemy natychmiast przej ś ć wprost do odpowiedniego pliku. Nie jest to jednak ś cisła reguła. Czasami dobrze je st um ieś ci ć zbiór definicji blisko spokrewnionych klas w jednym pliku oraz podobnie zebrać ich implementacje. Bez względu na to, j aką strukturę programu przyjmiemy, w pan elu C/ass View widoczne będ ą wszystkie klasy, jak również ich składowe, co przedstawiono na rysunku 8. 15. Dopa sowałem rozmiar panelu Class View, aby wszystkie elementy projektu b yły wido czne . W tym miejscu można podejrzeć s zczegóły dotyczące klas i elementów globalnych ostatniego programu . Jak już wcześniej mów iłem , dwukrotne kliknięcie elementu na liście spowoduj e przej ście do odpowiadającego mu kodu .
502
Visual C++ 2005. Od podstaw
Rysunek 8.15 <Sear ch >
B
GlB CWO_OO .. Glaba I Functions and Vambles . ~ Macros and Constants
''!s m . " "-CBaK('O ,d) ~ ... CBoK(dauble Iv = 1.OOOJOO, doub le wv '.. Ge1Height (void) Ge1Length (vald) " GeIWidth (void) '.. Volume (vo id)
L
~ operator + (void)
: .ił m_Height .fi
.#
_
m_Length m_Width
<
"
.
l,Je;t:
~ So~lt ian E... I~wJ~ReSOl! rC e
...
Programowanie wC++/CLI Mimo że dla klasy referencyjnej można zdefiniować destruktor w podobny sposób jak dla klas w natywnym C++, to zazwyczaj nie jest to konieczne. Niemniej jednak do tematu destruktorów dla klas referencyjnych wróc imy jeszcze w następnym rozdziale. Można również wywołać operator delete dla uchwytu do klasy referencyjnej, ale to także nie jest konieczne, gdyż mechanizm usuwania nieużytków automatycznie usuwa wszystkie niepotrzebne obiekty. Język C++/CLI obsługuje przeładowywanie operatorów, ale są tu pewne różnice w porównaniu z natywnym C++, o których należy wiedzieć . Na początku zastanówmy się , jakie są podstawowe różnice pomiędzy przeładowywaniem operatorów klas w C++/CLI i w natywnym CH. O kilku z nich już co nieco wiemy. Prawdopodobnie przypominasz sobie, że nie można przeładowywać operatora przypisania w klasach wartości, ponieważ proces przypisywania jednego obiektu klasy wartości do drugiego obiektu tej samej klasy jest już zdefiniowany jako kopiowanie pole po polu i nie możn a tego zmi enić . Nadmieniłem także , że w przeciwieństwie do klas natywnych, klasy referencyjne nie mają domy ślnego operatora przypis ania (jeżel i chcesz mieć operator przypisania działający z obiektami Twoich klas referencyjnych, musisz zaimplementowa ć odpowiednią funk cję). Następną różnicąjest to, że funkcje implementujące przeładowany operator w klasach C++/CLI mogą być zarówno funkcjami statycznymi, jak i składowymi egzemplarza. Oznacza to, że możemy implementować operatory binarne w klasach w C++/CLI za pomocą składowych funkcji statycznych z dwoma parametrami poza możliwościami dostępnymi w natywn ym C++, zatrzymując także możliwo ści znane nam z natywnego C++, w którym funkcje operatorów można implementować jako funkcje skła dowe z jednym parametrem lub funkcje niebędące składowymi z dwoma parametrami. Podobnie w C++/CLI mamy dodatkową możliwość implementacji przedrostkowego operatorajednoargumentowego jako statycznej funkcji składow ej bez parametrów. I na koniec , mimo że w natywnym C++ można przeładowywać operator new, w klasach w C++/CLI operatora gcnew przeładowywać nie można.
Przyjrzyjmy
się
tym zagadnieniom bardziej
szczegółowo, zaczynając
od klas
warto ści .
Rozdzial8. •
Przeładowywanie
Więcei na temat klas
503
operatorów wklasach wartości
Zdefiniujemy kl asę reprezentującą długość w metrach i centymetrach i użyjemy jej jako podstawy do zademonstrowania implementacji przeładowyw ania operatora dla klasy warto ści . Na początek w sam raz nadasię operator dodawania. Poniżej znajduje s ię definicja klasy wartości o nazwie Lengt h, włączni e z funkcją operatora dodawania:
val ue class Length {
pr ivate : int metry: int centymet ry:
II Metry. II Centymetry.
publ t e : st at ic i nit only int cmNaMet r
~
100:
II Konstruktor.
Lengt h(i nt m. i nt cm) : II Dlugość jako
metry un) .
centymet ry(cm){
łańcuch.
virt ual St ring ToStr ing() overr ide { ret urn met ry- L" met rów" + centymet ry + L" centymet rów"; A
II Operator dodawan ia..
Length operat or+( Lengt h len) {
int cmTot al = centymet ry + len.centymet ry + cmNaMet r * ( met ry + len.met ry ): ret urn Length(cmTotal /cmNaMet r . cmTot al %cmNaMet r) ; }: Stała cmNaMet r
jest statyczna, a więc mają do niej bezpośredni dostęp zarówno statyczne, jak i niestatyczne funkcje składowe klasy. Zastosowanie słowa kluczowego i nitonl y w deklaracji zmiennej cmNaMet r oznacza, że nie można jej modyfikować, a więc może ona należeć do sekcji publicznej klasy. Jest też definicja over r i de funkcji ToStri ng( l dla klasy, a więc obiekty klasy Lengt h można wysyłać do wiersza poleceń za pomocą funkcji Console : :Wr i t eLine( l. Implementacja funkcji operat or-t l jest bardzo prosta . Funkcja ta zwraca nowy obiekt klasy Length utworzony z połączenia składowych metry oraz cent ymetry bieżącego obiektu oraz parametru l en. Obliczenia wykonywane są poprzez połączenie tych dwóch długości w centymetrach, a następnie przekazanie tych argumentów do konstruktora klasy Length w celu utworzenia nowego obiektu z tych połączonych długości w centymetrach. Poniższy
fragment kodu sprawdza
działanie
Lengt h lenI = Lengt h(6. 9); Length len2 = Length(7. 8); Console: :WriteLine(L "{O} plus {l} równa
nowej funkcji operatora dodawania.
si ę
{2)'" . lenl , len2. len1+l en2) :
Ostatni argument przekazywany do funkcji Wr iteL-ineC l jest sumą dwóch obiektów klasy Length, co powoduje wywołanie funkcji oper ator-t l. W rezultacie powstaje nowy obiekt klasy Length , dla którego kompilator wywołuje funkcję ToSt ri ng ( l, w związku z czym ostatnia instrukcja w rzeczywistości wygląda następująco :
504
Visual C++ 2005. Od podstaw Conso1e: :WriteLine(L"{O} plus {l} równa Wykonanie tego fragmentu kodu da
6 metrów 9 centymetrów pl us 7 metrów 8 centymetró w równa
się
Oczywiście, funkcję operato r+ ( ) możemy także zdefiniować
13 met rów 17 centymet rów
jako
statyczną składową klasy
Length:
st at ic Lengt h operat or+( Length lenl . Length len2 ) {
int cmTot al = centymetry + len.centymetry + cmNaMet r * ( metry + l en.metry ); ret urn Length (cmTotal/cmNaMetr . cmTotal%cmNaMet r ) ; Parametrami są dwa obiekty klasy Length, które po dodaniu mają utworzyć nowy obiekt klasy Lengt h. Jako że funkcja operator+ () jest statyczną składową klasy, ma ona dostęp do składo wych prywatnych metry oraz cent ymet ry obu obiektów klasy Length przekazanych jako argumenty. W C++/CLI nie można używać funkcji zaprzyjaźnionych, a zewnętrzna funkcja nie miałaby dostępu do prywatnych składowych klasy. W związku z tym nie ma innej możliwości implementacji operatora dodawania. Jako że nie pracujemy z obszarami, mnożenie obiektów klasy Length ma sens tylko w przypadku mnożenia długości przez wartość liczbową. Funkcję tę można zaimplementować jako statyczną składową klasy, ale zdefiniujmy ją poza klasą. Klasa przedstawia się następująco:
value cl ass Lengt h {
pr i vat e: i nt met ry; int centymet ry;
II Metry. II Centymetry.
public: st at lc init only int cmNaMet r
=
100;
II Konstruktor.
Length (int m. int cm) : II Długoś ćjako
metry tm).
centymet ry(cm) {
łańcuch.
virtua l St r i ng ToString() override { ret urn met ry-L" met rów" + centymetry + L" centymetrów"; } A
II Operat or dodawania.
Length operator+(Length len) {
int emTotal = centymet ry + len.centymet ry + cmNaMetr * ( metry + len .metry ); retu rn Length(cmTotal I cmNaMetr. cmTotal %cmNaMetr) ; st at ic Length operato r*(double x. Length len); stat i c Length operator*(Length len. double x): };
II "Mnożenie przed " przez liczbę typu II double. II "Mnożenie po" przez liczbę typu double.
Rozdział 8.• Więcei na temat klas
505
Nowe deklaracje funkcji w klasie są deklaracjami funkcji operatora *, mnożących obiekt klasy Length przez liczbę typu doubl e znajdującą się w pozycji pierw szego i drugiego argumentu. Definicja klasy operat or*() na zewnątrz klasy dla mnożenia z li czbą jako pierwszym argumentem wygląda następująco:
Lengt h Lengt h: :operator*(double x. Lengt h len) (
int ins ; safe_cast (x * len.centymet ry + X * len.met ry * crnNaMetr): return Length(ins / cmNaMet r . i ns %cmNaMet r ); "Mnożenie
Oba wiersze wynikające z tego fragmentu kodu pow inny zawierać ten sam wynik mnożenia ( 19 metrów 2 centymetry). Wyrażenie argumentowe fact or*l en2 jest równoznaczne z poniższym :
Lengt h: :operat or*(fact or . len2) .ToSt ring() funkcji statycznej operator*() jest nowy obiekt klasy Length. Następnie jest funkcja ToSt ri ngO w celu utworzenia argumentu dla funkcji Wri t eL i net i . Wyrażenie l en2*factor jest podobne, ale wywołuje funkcję operat or*( ) z poprzestawianymi parametrami . Mimo że funkcje operato r* ( ) zostały napisane w celu obsługi mnożeni a liczb typu doubl e, działają one także z liczbami całkow itym i . Kompilator automatycznie konwertuje liczby całkowite do typu doubl e, kiedy zostaną użyte w wyrażen iu takim jak 12*(len1+l en2).
Wynikiem
wywołania
wywoływana
Rozszerzymy
naszą wiedzę
dając się działającemu
na temat programowi.
przeładowanych
operatorów w klasie Lengt h, przyglą
RI!mI!mI Klasa wartości zprzeładowanymi operatorami W poniższym kodzie zaimplementowano przeładowywanie operatorów dodawania, oraz dzielenia dla klasy Length:
value class Lengt h {
private: int metry;
II Metry .
mnożenia
506
Visual C++ 2005. Od podslaw int centymet ry:
II Centymetry.
pub1ic: st ati c initonly i nt cmNaMet r
~
100 .
II Konstruktor.
Length(lnt m. int cm)
met ry(m ). centymetry (cm) {
II Długoś ć j ako łań cu ch.
virtual St ringA ToSt ri ng() override { ret urn met ry-L" met rów" + centymet ry + L" centymet rów" ; }
II Operator dodawani a.
Lengt h operat or+(Lengt h len) {
i nt cmTot al ~ centymet ry + len.centymetry + cmNaMet r * ( metry + len.metry ): ret urn Length(cmTotal /cmNaMetr . cmTotal%cmNaMetr ) : II Operator dzielenia.
st atic Lengt h operat or/( Length len. double x) {
int ins = safe_cast (( len.met ry * cmNaMet r + len.centymet ry) I x): return Length(i ns I cmNaMet r. i ns %cmNaMet r) : stat ic Length operator*(doub le x. Lengt h len): st at ic Lengt h operat or*(Length len. doubl e x):
II" Mnożenie przed" przez liczbę typu Il double. II " Mnożenie po " przez liczb ę typu doubl e.
int ins ~ safe_cast (x * len.centymet ry + X * len.met ry * cmNaMet r) ; et r) : return Length (ins / cmNaMet r . ins %cmNaM Length Length: :operat or*(Length len. double x) ( ret urn operator*(x. len): } int main(array<Syst em: :St ri ng A> Aargs) (
Length lenI ~ Length(6. 9): Length len2 ~ Length (7. 8) ; doubl e factor ~ 2.5: Console: :Writ eLine(L"{O} pl us Conso le: :WriteLine(L"{O} razy Console: :WriteLine(L "{l} razy Console: :Wr iteL ine(L"Suma {O}
{l} równa si ę {2}". tactor . le n2. factor*len2): {O} równa si ę {2}". tact or . len2. len2*fact or ): i {l} podzie lona przez {2} równa s i ę {3}" . lenI. len2. fact or. (lenl+len2)/ factor ):
ret urn O: 6 met rów 9 centymetrów plus 7 met rów 8 centymet rów równa s ię 13 met rów 17 centymet rów 2.5 razy 7 met rów 8 centymetrów równa s ię 17 metrów 70 centymet rów 7 met rów 8 centymetrów razy 2.5 równa s ię 17 metrów 70 centymet rów Suma 6 metrów 9 centymetrów i 7 metrów 8 cent ymet rów podzielona przez 2.5 równa s ię 5 metrów 26 centymet rów
RozdzialB. •
Więcej
na temat klas
507
Jak to działa Nowa funkcja przeładowująca w klasie Length umożliwia dzielen ie obiektu klasy Length przez liczbę typu doubl e. Dzielenie liczby typu double przez obiekt klasy Length nie ma oczywistego znaczen ia, a wi ęc nie ma potrzeby implementacj i tej wersj i. Funkcja oper ator/ (l została zdefiniowana jako j eszcze je dna statyczna składowa kla sy, a jej defin icja została umi eszczona w ciele klasy w celu porównania, jak to wygląda w odróżnieniu od funkcji operat or*( l. Zwykle wszystkie te definicje funkcji umieścilibyśmy w definicji klasy. Oczywi ście , funkcję
operator / (l
można także zdefiniować
jako
niestatyczną składową
klasy:
Length operator/( double x) {
i nt ins = safe_cast «met ry * cmNaMet r + centymet ry) I x ): ret urn Length (i ns I cmNaMetr . i ns %cmNaMet r) ; Teraz funkcja ta ma tylko jeden argument , który stanow i prawy operand operatora l . Lewym operandem jest bieżący obiekt wskazywany przez wskaźnik thi s (w tym przypadku jawnie). Operatory testowane są w czterech instrukcjach wyjściowych. Tylko ostatnia z nich jest dla nas nowa. Łączy ona użycie przeładowanego operatora + dla obiektów klasy Lengt h z przeładowa nym operator em t . Ostatnim argumentem funkcji Console : .Wri teLi neCl w czwartej instrukcji wyjściowej jest wyrażenie (lenl +1 en2l/ factor, które jest równoznaczne z wyrażeniem :
Length: :operator/ (l enl .operator+(l en2) . fact or) .ToSt ring( ) Pierwszym argumentem statycznej funkcji operat or/ () jest obiekt klasy Length zwracany przez funkcję operator-t l, a drugim argumentem jest zmienna factor, która jest dzielnikiem . Funkcja ToStri ng() wywoływana dla obiektu klasy Length zwróconego przez funkcję operato r/ (l tworzy łańcuch, który jest następnie przekazywany do funkcji Console : :Wri t eL ine( l. Możliwe je st, że będziemy chcieli mieć możliwość podzielenia jednego obiektu klasy Length przez inny obiekt tej klasy i otrzymać wynik w postaci liczby całkowitej . Pozwoliłoby to na przykład na obliczenie, ile czterdziestocentymetrowych bali można otrzymać z pnia o długości 7 metrów. Implementacja takiej funkcjonalności jest bardzo prosta:
st at ic int operator /( Lengt h lenl. Length len2) {
ret urn (le nl .metry*cmNaMet r
+
lenl .centymet ry)/ ( len2.met ry*cmNaMet r + len2.centymetry):
Powyższy kod zwraca wynik dzielenia pierwszej długości w centymetrach przez w centymetrach.
drugą długość
Aby mieć już wszystko, możemy jeszcze dodać funkcję przeładowującą operator %, która pozwoli nam sprawdzić, ile centymetrów pnia zostało:
st at ic Lengt h operat or%( Lengt h lenl . Length len2) {
i nt i ns
=
(lenl .met ry*cmNaMetr
+
lenl. centymet ry)% (len2.met ry*cmNa Metr
+
len2 .centymet ry) :
508
VisIlai C++ 2005. Od podstaw return Length(ins / cmNa Metr . ins %cmNaMet r) ; Resztę w centymetrach obliczamy po podzieleniu l enl przez l en2 i zwróceniu wyniku jako nowego obiektu klasy Lengt h. Mając te wszystkie operatory, możemy używać obiektów klasy Length w tycznych. Możemy pisa ć instrukcje podobne do poni ższych :
zmiennej total wyniesie 278 metrów 43 centymetry . W ostatniej instrukcji użyty zooperator przypisania, który działa z każdą wartości ą klasy, j ak również z funkcjami klasy Lengt h operat or*(), operator+( ) oraz ope rat or/ () . Chyba każdy się ze mną zgodzi, że przeładowywanie operatorów jest nie tylko potężnym , ale i prostym w użyciu narzędziem . stał
Przeładowywanie operatorów inkrementacii idekrementacji Przeładowywanie ope ratorów inkrementacji i dekrementacji w C++/CLI jest prostsze ni ż w natywnym C++. Jeżeli funkcję jednego z tych operatorów zaimplementujemy jako statyczną składową klasy, to będz ie ona służyła jako funkcja operatora zarówno w formie przedrostkowej, jak i przyrostkow ej. Poni żej znajduje się przykładowa implementacja operatora inkrementacji dla klasy Lengt h:
clas s Lengt h {
publ i c: II Kod j ak pop rzednio... II Funkcja przeładowująca ope rator inkrementacji -
zwiększa
o j eden l centymetr.
stat lc Lengt h aperator++(Lengt h len) {
++len.cent ymetry; len.met ry += len.cent ymet ry/l en.cmNaMet r : len.centymet ry %= len.cmNaMetr : ret urn len:
Funkcja oper at or++ () powyższej implementacji szy kod sprawdza działanie tej funkcji: Lengt h len = Lengt h(l. 97); Console : :Wri te Ll ne(l en++); Console : :Wri t eLi ne(++len) : Wykonanie powyższego kodu da l met r 97 centymetró w l met r 99 centymetrów
zwiększa długość
o jeden centymetr.
11 1 metr 97 centymetrów.
następujący
rezultat:
Pon iż
Rozdzial8. •
Więcej
na temat klas
509
A więc widać , że operator inkrementacji działa prawidłowo .zarówno w formie przedrostkowej, jak i przyrostkowej przy użyciu funkcji jednooperatorowej w klasie Lengt h. Jest to moż liwe, ponieważ kompilator mógł okre ślić, czy warto ść operandu w otaczającym wyrażeniu powinn a być użyta przed , czy po jego zwiększeniu , i odpowiednio skompilować kod.
Przeładowywanie opera1orów
wklasach referencyjnych
Prz eł adowywanie
operatorów w klasach referencyjnych w gruncie rzeczy polega na tym samym co przeładowywanie operatorów w klasach wartości . Najważniejszą różnicą jest to, że parametry i wartości zwracane z reguły s ą uchwytami. Przyjrzyjmy s i ę referencyj nej implementacji klasy Lengt h. Wtedy będziemy mogli porównać obie wersje .
~ Przeładowywanie operatorów wklasie referencyinei Poniższy kod definiuje kl asę Lengt h jako klasę referencyjną z takim samym zestawem dowanych operatorów jak w wersji klasy warto ści:
przeła
#i nc l ude "st dafx.h" using namespace System: ref class Length (
pr ivat e: int met ry: int centymet ry; publ ic: sta t ic init only lnt cmNaMet r
=
100:
II Konstruk tor.
Lengt hCi nt m. i nt cm) : met ryCm), centymet ry(cm){ II Długoś ć jako
łańcuc h.
vi rtua l St ring ToSt ri ngC) override ( retur n metry-L " met rów " + centymet ry + L" centymet rów": A
II Przeładowany ope rator dodawania.
Length operator+(Length len) A
A
(
i nt cmTota l = centymet ry + len->centymet ry + cmNaMet r * Cmet ry + len->metry ): ret urn gcnew Length(cmTotal / crnNaMet r , cmTot al %cmNaMet r l ; II Przełado wany operator dzielenia - prawy operand typ u double.
sta t ic Length A operator/ CLengthA len. double x) {
int ins = safe_cast CClen->metry * cmNaMet r + len->centymet ry)/x) ; ret urn gcnew Length Ci ns / cmNaMetr , t ns % cmNaMetr):
510
lisual C++ 2005. Od polislaw II Przeładowany operator dzielenia - oba operandy typu Length.
st at ic int operato r/ (Lengt h lenI , Length len2) A
A
{
ret urn (lenl ->met ry * cmNaMetr + lenl ->centymet ry)/ (len2->met ry * cmNaMet r + len2->cent ymet ry) ; II Przeładowany operator dzielenia modulo .
stat ic Lengt h operat or%( Length lenI, Length A len2) A
A
{
i nt ins = (lenl ->metry * cmNaMet r + lenl ->centymet ry)% (len2->met ry * cmNaMetr + len2->centymet ry): ret urn gcnew Lengt h(i ns / cmNaMet r , i ns %cmNaMet r) ; st at 1C l.enqth" operat or*(double x. Lenqt h" len): st at i C l.enqth" operator*(Lengt h len. double x):
II Mnożen ie - prawy operand double II Mnożenie -lewy operand double .
A
II Przedrostk owy i przyr ostkowy operat or inkrementa cji.
stat ic Lengt h operato r++( Length len) A
A
{
++ len->centymet ry; len->met ry += len->centymet ry / len->cmNaMet r; l en->centymet ry %= l en->cmNaMet r ; ret urn len; } }: II Impłementacja
ope ratora
mnoże nia
- p rawy operand dou ble.
Lengt hA Length : :operat or*(double x. Lengt h len) A
{
i nt ins = safe_cast (x * len->centymet ry + X * len->met ry*cmNaMetr ) : ret urn gcnew Lengt h(l ns / cmNaMetr . i ns %cmNaMet r) : II Impl ementacja ope ratora
Length tot al = l2*(lenl+len2+len3) Console: :Wr iteL i nert ot.a l r: A
II
Użycie
+
(len3/gcnew Lengt h(1.7))*len2:
ope ratora dzielenia modulo.
Console: :Wri te Li ne( L"{O} mo ż n a dł ug o ś c i {3} . ".
poc iąć
na {l}
ka wałk ów
o
d ł u go ś ci
{2}. Zost anie
k awa łe k
len3. len3/l enI. lenl . len3% lenl) ; Lengt h len4 = gcnew Lengt h(l, 11) ; A
II
Uży cie
II I metr 11 centymetrów.
operat ora inkrementa cj i w form ie przedrostkow ej i przyrostkowej. II Użycie ope ratora w form ie przyrostkowej.
Console: .Wri t eL i ne(l en4++ ):
Rozdział
Console: :Wri t eLi ne(++len4) : ret urn
8. •
Więcei
na temat klas
511
II Użycie operatora II w f ormie przedrostkowej.
o:
Rezultat działania tego programu jest następujący: 293 met rów 90 centymet rów 14 met rów 60 centymetrów mo żn a po c ią ć na 5 k awa ł k ów o d ł ug ośc i 2 met rów 60 centymetrów. Zost anie kawał ek d ł ugo ś c i 1 met rów 60 centymetrów. 1 met rów 12 cent ymetró w 1 metrów 13 centymet rów
Jak lo działa Główne różnice dot yczą
typów parametrów i typów zwracanych przez funkcje operatorów z którymi użyty został operator - >. Obiekty typu l.enqt h tworzone są teraz na stercie CLR za pomocą operatora genew. Poza wym ienionymi zmianami kod je st w zasadzie taki sam, a operatory działają tak samo dobrze jak w poprzednim programie. przeładowanych, w zw iązku
Podsumowanie W rozdziale tym nauczyliśmy się podstaw defin iowania klas oraz tworzenia i używania ich obiektów. Nauczyl i śmy s ię także przeładowywać operatory w klasach w taki sposób, aby można je było zastosować do obiektów klasowych. Poniżej
znajduje
się
lista
najważn iejszych zagadnień
poruszanych w tym rozdziale:
•
Obiekt y tworzone są za pomocą funkcj i zwanych konstruktorami. Głównym przeznaczeniem konstruktora jest ustawianie wartości składowych (pól) obiektów klasy.
•
W C++/CLI można także statyczne pola klasy .
•
Obiekty niszczone są za pomocą funkcji zwanych destruktorami. W natywnym C++ należy pamiętać o zdefiniowaniu destruktora klasy w celu niszczenia obiektów zawierających składowe alokowane na stercie, gdyż destruktor domyślny ich nie usuwa .
•
Jeżel i dla klasy w natywnym C++ nie zostanie zdefiniowany konstruktor kopiujący, to kompilator dostarczy domyślny konstruktor kopiujący. Nie działa on prawidłowo z obiektami, których składowym została przydzielona pamięć w obszarze wolnym .
•
D efiniując własny
•
Konstruktora kopiującego nie można definiować w klasie wartości. Kopie obiektów klasy warto ści są zawsze tworzone poprzez kopiowanie pól.
stworzyć
konstruktor statyczny klasy
in icjaLizujący
konstruktor kopiujący w klasie w natywnym C++, parametru w postaci referen cji.
należy użyć
512
Visual C++ 2005. Od podstaw •
Klasa referencyjn a nie posiada domyślnego konstruktora kopiuj ącego , ale w razi e potrzeby można zdefiniować własny.
•
J eżel i dla klasy w natywnym C++ nie zostanie zdefiniowany konstruktor przypisania, to kompil ato r dostarczy jego domyślną wersję . Podobnie jak domyślny konstruktor kopiujący, domyślny ope rator prz ypis ania nie działa prawidłowo z ob iektami, których s kład owym została przydzielona pamięć w obszarze wolnym.
•
Operatora prz ypi sania nie można definiować dla klasy wartości. Przypisywanie obiektów klasy warto ś ci zaw sze odbywa się poprzez kopiowanie pól.
•
W klasach referencyjnych nie ma domy ślnego operatora przyp isania, ale w razie potrzeby można zdefiniować własną funkcj ę operatora przypisania.
•
Dla klas w natywnym C++, których składowe zostały alokowane za pomocą operatora new, należy zawsze dostarczać destruktor, konstruktor kopiujący oraz operator przypisania.
•
Unia to mechanizm pozwalający dwóm lub ten sam obszar pamięci.
•
KJasy C++/CLI mogą zawierać pola literalowe, które definiuj ą stałe wewnątrz klasy. W klasach tych można także definiować pola initonly, których nie można zmi eniać od momentu, kiedy zo stały zainicjalizowane.
•
Najbardziej podstawowe operatory można przeciążać w celu nadania im funkcjonalno ści potrzebnej do pracy z obiektami dan ej klasy. Funkcje operatorów należy definiować w sposób zgodny z no rmalną interpret acją operatorów podstawowych.
•
Szablon klasy to wzorzec, na podstawie którego tworzone strukturze, ale obsługujące różne typy danych.
•
Można zdefiniować wartości stałe
•
większej
liczbie zmi ennych
szablon klasy z wieloma parametrami, zamiast typów.
są klasy
włącznie
zajmować
o takiej samej
z reprezentującymi
Definicje programu powinno umieszczać się w plikach z rozszerzeniem .h, a kod wykonywalny (definicje funkcji) programu w plikach z rozszerzeniem .cpp. Następnie zawarto ść plików z rozszerzeniem .h można dołączyć do plików z rozszerzeniem .cpp za pomocą dyrektyw #i ncl ude.
Ćwiczenia Kod źródłowy wszystkich listingów w tej książce oraz rozwiązania do ze strony httpt/Zhelion.pl/ksiazki/vcppo.htm ,
ćwiczeń można pobrać
1. Zdefiniuj klasę w natywnym C++ reprezentującąjakąś liczbę całkowitą w przybliżeniu , np . około 40 . Liczba ta może być traktowana jako dokładna wartość albo jako wartość przybliżona, a więc klasa musi zawierać składową reprezentującą wartość oraz znacznik "p rzybl iżenia" . Stan znacznika przybliżenia ma wpływ na operacje arytmetyczn e. A zatem 2 razy około 40 równa się około 80. Stan zmiennych powinien dać się prz ełączać pomiędzy "przybliżony" a "do kładny".
ROlllZial8. •
Więcej
na temat klas
513
Utwórz co najmniej jeden konstruktor takiej klasy. Przeładuj operator +, aby liczb tych można było używać w wyrażeniach arytm etycznych. Chcesz, aby funkcja operatora + b yła funkcją globalną czy składową? Czy potrzebujesz operatora przypi sania? Napisz funkcję składową Pr i nt ( ) wysyłającą liczby na ekran z literą "P" z przodu oznaczającą, że dana liczba jest przybliżona. Napisz program sprawdzaj ący działanie Twojej klasy, szczególną uwagę zwracając na poprawność działan ia znacznika przybliżenia.
2. Zaimplementuj prostą klasę typu łańcuchowego w natywnym C++ zawierającą składowe: wskaźnik do znaku char* oraz liczbę całkowitą l engt h. Utwórz konstruktor przyjmujący argument typu const char* oraz dodaj implementację funkcji konstruktora kopiującego, operatora przypisania oraz destruktora. Sprawdź, czy klasa działa. Najłatwiej będzie użyć funkcji łańcuchowych z pliku nagłówkowego
.
3. Jakie inne konstruktory Zrób
listę
mogłyby być
potrzebne dla Twojej klasy
łańcuchowej?
i napisz ich kod.
l. Dla zaawansowanych: czy Twoja klasa radzi sobie prawidłowo w takich przypadkach jak poniższy? st ri ng s 1:
sI = sI Jeśli
nie, to co trzeba
zmienić?
I. Dla zaawansowanych:
przeładuj
operatory + i += W swojej klasie, aby
umożliwić
konkatenację łańcuchów .
.. Zmodyfikuj przykład ze stosem z ćwiczenia 7. z poprzedniego rozdziału w taki sposób, aby rozmiar stosu był określany w konstruktorze i alokowany dynamicznie. Co jeszcze trzeba dodać ? Sprawd ź działanie swojej nowej klasy.
7. Zdefiniuj
klasę referencyjną o nazwie Sox pos iadającą taką samą funkcjonalność co klasa CSox z programu Cw8_08.cpp, a następnie zaimplementuj go ponownie jako program CLR.
514
Visual C++ 2005. Oli podstaw
9
Dziedziczenie ifunkcje wirtualne
Rozdział ten został poświęcony tematowi, który stanowi samo serce programowania zorien towanego obiektowo - dziedziczeniu. Mówiąc krótko, dziedziczenie polega na definiowaniu nowej klasy na podstawie klasy już istniejącej. Technika ta jest podstawą programowania w C++, a więc bardzo ważne jest, aby ją dobrze opanować.
W rozdziale tym dowiesz
się:
• Jak dziedziczenie wpasowuje obiektowo. • Jak
definiowa ć
się
w koncepcję programowania zorientowanego
nowe klasy na podstawie klas już
• Jak używać słowa kluczowego protected do klasy w nowy sposób. • Jak klasa
może być zaprzyjaźniona
z
są
funkcje wirtualne i jak ich
używać.
• Czym
istniejących.
określania dostępu
do
składowych
inną klasą.
• Czym są funkcje czysto wirtualne. • Czym są klasy abstrakcyjne. • Czym
są destruktory
wirtualne i kiedy
się
ich
używa .
Podstawy programowania zorientowanego obiektowo (OOP) Jak już wiemy, klasa stanowi typ danych , który definiuje się w celu rozwiązania określonego problemu w programie. W programowaniu zorientowanym obiektowo klasy definiują również obiekty, do których odnosi się program. Kod rozwiązujący dany problem pisze się, tworząc odpowiednie do jego rodzaju obiekty przy użyciu działań operujących bezpośrednio na nich.
516
Visual C++ 2005. Od podstaw Klasa może zostać zdefiniowana do reprezentacji tworów abstrakcyjnych, takich jak na przy kład liczba zespolona, czyli koncept matematyczny, lub ciężarówka, która jest zdecydowanie obiektem fizycznym (zwłaszcza gdy się na jakąś wpadnie na drodze) . A zatem klasa może być zarówno typem danych, jak i definicją zbioru rzeczywistych obiektów określonego typu przynajmniej w stopniu niezbędnym do rozwiązania problemu. Klasę można traktować jako zbiór cech i właściwości pewnej grupy elementów, które okre śla wspólny zestaw parametrów i na których może być wykonywany wspólny zestaw działań. Dzi ałania , które można wykonywać na obiektach danej klasy, zdefiniowane są w interfejsie . klasy, czyli w funkcjach znajdujących się w publicznej sekcji definicji klasy. Dobrym przy kładem klasy jest znana nam z poprzedniego rozdziału klasa CBox - definiowała ona pudełko na podstawie jego wymiarów, a zestaw funkcji publicznych tej klasy pozwalał na wykony wanie różnych działań na jej obiektach (pudełkach). Oczywiście,
w rzeczywistym świe cie istnieje wiele różnego rodzaju pojemników, które mają podobne cechy do pudła . Mogą to być kartony , trumny , pudełka na cukierki, pudełka z ryżem i wiele, wiele innych. Pojemniki te można rozróżniać na podstawie ich zawartości , materiału , z którego są wykonane, a także na wiele jeszcze'innych sposobów, ale nawet wtedy wszystkie pudełka mająjedną wspólną cechę - są pudełkami . A zatem możemy sobie wyobrazić , że wszystkie pudełka są w pewien sposób powiązane ze sobą, mimo że mają wiele cech je różnią cych. Można by było zdefiniować określony rodzaj pudełka posiadającego wszystkie cechy ro dzajowe pudełek, czyli zapewne długość, szerokość i wysokość. Następnie - w celu odróżnienia danego pudełka od pozostałych - można podać dodatkowe jego cechy. Może się także okazać, że z nowym pudełkiem można zrobić rzeczy, których nie można zrobić z innymi pudełkami. Możliwe jest także utworzenie nowego rodzaju pudełka poprzez połączenie jednego jego rodzaju z innym obiektem, np. pudełkiem z cukierkiem lub kratką piwa. W tym celu można by było zdefiniować jeden rodzaj pudełka z podstawowymi właściwościami, a następnie utworzyć inny rodzaj pudełka, pochodzący od tego podstawowego, ale przeznaczony już do konkretnego celu. Rysunek 9.1 przedstawia przykładowe powiązania pomiędzy pudełkami różnego rodzaju.
Bardziej ogólne
Klasa CBox m_Length m_Width m_Height
/
Klasa CCarton m_MaxWeight
Klasa CCandyBox m_Contents
~ I
Klasa CCrate
I m_nBottles
\
Klasa CBeerCrate m _Beer Bardziej wyspecjaliz owane
Rysunek 9.1
Rozdział 9.
• Dziedziczenie i flmkcie wirtualne
517
Pudełka
o najwęższym zastosowaniu znajduj ą s i ę na samym dole diagramu , a strzałki wska od którego dane pudełk o pochodzi. Na rysunku 9.1 zdefiniowano trzy różn e rodzaje pudełek na podstawie typu rodzajowego CBox. Kratki na piwo zostały zdefiniowane jako bardzi ej wyspe cjalizowany rodz aj kratek na butelki. zuj ą pudełko,
A zatem świat rzeczywisty w C++ można całkiem dobrze odwzorować za pomocą wzajemnie powi ązanych definicji klas . Pudełko na cukierki może być pudełkiem p osiadaj ącym wszy stkie cechy rodzajowe plus kilka dodatkowych wł asnych. Sytuacja taka bardzo dobrze ilustruje związki pomiędzy klasami w C++, gdzie jedna klasa defin iowana jest na podstawie innej. Bardziej wyspecjalizowana klasa ma wszystkie cechy klasy rodzajowej oraz kilka własnych cech , które odróżniająją od innych . Zobaczmy, jak to działa , na konkretnym przykładzie.
Dziedziczenie wklasach Klasa utworzona na podstawie innej klasy nazywa się klasą pochodną. Zawiera ona auto mat yczn ie wsz ystkie zmienne składowe klasy, której użyliśmy do jej zdefiniowania, oraz wszystkie jej funkcj e składowe, ale z pewnymi ograniczeniami. Proces przejmowania skła dowych klasy bazowej przez klasę p ochodną nazywa s ię dziedziczeniem. Jedyne składowe klasy bazowej , które nie są dziedziczone przez klasę pochodną, to destruk tor, konstmktory oraz funkcje składowe przeładowujące operator przypisania. Wszystkie pozo stałe funkcje składowe, a także zmienne składowe klasy bazowej są dziedziczone przez klasę pochodną. Oczywiście powodem, dla którego niektóre składowe klasy bazowej nie są dziedzi czone , jest to, że klasa pochodna zaw sze ma własne konstruktory i destruktor. Jeżeli klasa bazowa ma operator przyp isania, to klasa pochodna dostarcz a swoją własną wersję . Mówiąc , że funkcje te nie są dziedziczone, mam na myśli , że nie występują one jako składowe obiektu klasy pochodnej . S ą one jednak nadal dostępne dla obiektów klasy bazowej , o czym się nie bawem przekonamy.
Czym jest klasa bazowa Klasa bazowa to każda klasa , na podstawie której tworzymy nową klasę. Je żeli na przykład zdefiniujemy klasę B bezpośrednio na podstawie klasy A, to Ajest bezpośrednią klasą bazową klasy B. Na rysunku 9.1 klasa CCrat e jest bezpośredniąklasą b azową klasy CBee rCrat e. Kiedy tworzymy klasę CBeerCrate, definiującją na podstawie klasy CC ra t e, to klasa CBeer Crate jest klasą pochodną klasy CCra t e. Jako że klasa CC ra t e została zdefiniowana na podstawie klasy CBox, to klasa CBox jest pośrednią klasą bazową klasy CBeerCrat e. Jak to wygląda w definicji klasy, zobaczymy za chwilę. Na rysunku 9.2 pokazano sposób dziedziczenia składowych klasy bazowej przez klasę pochodną. Fakt, że funkcje składowe są dziedziczone, nie oznacza, że nigdy nie ma potrzeby zamiany ich w klasie pochodnej na ich zmodyfikowane wersje. Oczywiście w razie potrzeby można to zrobić.
518
Visual C++ 2005. Od podslaw
Rysunek 9.2
Składowe
Klasa bazowa
dziedziczone
Zmienne składowe Funkcje składowe Konstruktory Destruktor P rzeładowany
Inne
operator=
przeładowane
operatory
Klasa pochodna Zmienne składowe Funkcje składowe
Nie Nie Nie Inne
przeładowane
operatory
Własne zmienne składowe Własne
funkcje
składowe
Własne konstruktory Własny destruktor
Tworzenie klas pochodnych Powr óćm y
na
na chwilę do oryginalnej klasy CBox z publicznymi poprzedniego rozdziału:
s kład o wymi, którą widzieliśmy
początku
II Plik
nagłówkowy
Box.h pr oj ektu Cw9_0l.
#pragma ance class CBax {
publ te : dauble m_Lengt h: dau ble m_W idth: dauble m_Height : CBox(double l v = 1 O. double wv ~ 1.0. dou ble hv = 1.0) : m_Length(l v). m_Wi dth(wv). m_Height( hv){} ):
Utwórz pusty projekt WIN32 o nazwie ew9_0J i zapisz powyższy kod w nowym pliku na główkowym o nazwie Box.h. Dyrektywa #pragma once sprawia, że definicja klasy CBox zosta nie w kompilacji użyta tylko jeden raz. Klasa posiada konstruktor, a więc podczas tworzenia obiektów można go inicjalizować . Przypuśćmy teraz , że potrzebujemy jeszcze jednej klasy obiektów CCandyBox, które mają wszystko to, co mają obiekty klasy Cbox, plus dodatkową zmien ną składową w postaci wskaźnika do łańcucha tekstowego identyfikującego zawartość pudełka. Klas ę
CCandyBox możemy
II Plik
nagłówkowy
zdefiniować
jako pochodną klasy CBox w
następujący
CandyBox.h pr ojektu Cw9_0l.
#pragma once #include "Box .h" clas s CCandyBox: CBox {
publ ic: cha r* m_Content s : CCandyBox(char* st r
=
"Cukierek ")
II Konstruktor.
sposób:
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
519
m_Cont ent s = new charC st rl en(st r ) + 1 ] : st rcpy_s(m_Conte nts. st r lent st r ) + 1. str) : -CCandyBox() { delet eC ] mContent s : }:
II Destruktor.
}:
Dodaj ten plik nagłówkowy do projektu Cw9_0l . Dyrektywy #i ncl ude dołączającej plik Box.h potrzebujemy, ponieważ w kodzie odnosimy się do klasy CBox. Gdybyśmy pominęli tę dyrek tywę, to kompilator nie wiedziałby, jak wygląda klasa Cbox, i kodu nie można by skompilować . Nazwa klasy bazowej CBox znajduje się za nazwą klasy pochodnej (CcandyBox) i jest od niej oddzielona średnikiem. Pod każdym innym względem wygląda to jak zwykła definicja klasy. Dodaliśmy nową zmienną składową m_Cont ent s i ze względu na fakt, że jest ona wskaźnikiem do łańcucha , do jej inicjalizacji potrzebujemy konstruktora oraz destruktora, aby zwolnić pa mięć zajmowaną przez łańcuch. Łańcuchowi stanowiącemu zawartość obiektu CCandyBox nadaliśmy także wartość początkową w konstruktorze . Obiekty klasy CCandyBox zawierają wszystkie składowe klasy bazowej CBox plus dodatkową zmienną składową m_Contents. Warto zwrócić uwagę na sposób użycia funkcji st rcpy_s
~ klasy pochodnei Zobaczymy teraz na przykładzie , jak działa klasa pochodna. Dodaj kod z poniższego listingu do projektu Cw9_0l jako nowy plik źródłowy Cw9_ Ol .cpp : II Cw9_01.cpp II Używanie klasy po chodnej.
#incl ude #i nclude #incl ude "CandyBox. h" using std: :cout ; using std : :endl :
II Dla strumienia wejścia-wyjścia . II Dlafunkcji strlent) i str cpy{). II Dla klas CBox i CCandyBox.
i nt main() ( II Tworzenie obiektu klasy CBox.
CBox myBox(4.0. 3.0. 2.0): CCandyBox myCandyBox:
CCandyBox myMint Bo x ("Mięt ówki cienkie j ak l i st ek"); II Tworzeni e obiektu klasy
II CCandyBox.
cout « endl « "Obiekt m yBox zajmuj e" « size of myBox II Pokazuje , ile pamię ci « " ba j ty" « endl II wymagają te obiekty. « "Obi ekt m yCandyBox zajmuj e " « si zeof myCandyBox
520
Visual C++ 2005. Od podstaw « « «
" bajty" « end l "Obiekt myMintBox zajmuje" " bajt y":
«
si zeof myMintBox
cout « end l
«
"D ł ugość
obiekt u myBox wynosi "
«
myBox.m_Length:
myBox .m_Lengt h = 10 .0. II myCandyBox.m_Length
=
10.0;
II Usunię cie komentarza z tego wiersza spowoduje
błąd.
cout « endl : return O:
Jak to dziali w
kodzie umieściliśmy dyrektywę #i ncl ude dołączającą nagłówek CandyBox.h. ten zawiera z kolei dyrektywę #i ncl ude dołączającą nagłówek Box.h, a więc nie ma potrzeby dodawania go jeszcze tutaj . Za pomocą dyrektywy #i ncl ude moglibyśmy dołączyć nagłówek Box.h w tym pliku , ale dyrektywa #pr agma once i tak nie pozwoliłaby na jego dołą czenie więcej niż jeden raz. Jest to ważne, gdyż każda klasa może być zdefiniowana tylko jeden raz. Dwie definicje tej samej klasy w kodzie spowodowałyby błąd. powyższym
Nagłówek
Po zadeklarowaniu obiektu klasy CBox i dwóch obiektów klasy CCandyBox wysyłamy na wyj ście liczbę bajtów zajmowaną przez każdy z nich. Spójrzmy na rezultat działania naszego programu:
Obiekt myBox zajmuje 24 bajty
Obiekt myCandyBox zajmuje 32 bajty
Obiekt myMint Box zajmuje 32 bajt y
D łu g o ść obiektu myBox wynosi 4
W pierwszym wierszu znajduje się to, czego moglibyśmy się spodziewać z rozważań w po przednim rozdziale. Obiekt klasy CBox ma trzy zmienne składowe typu doubl e, z których każda zajmuje osiem bajtów pamięci, co w sumie daje 24 bajty. Oba obiekty klasy CCa ndyBox mają takie same rozmiary - 32 bajty. Długość łańcucha nie ma wpływu na rozmiar obiektu, ponie waż pamięć dla niego przydzielona znajduje się w obszarze wolnym. Na te 32 bajty składają się 24 bajty zajmowane przez trzy zmienne składowe typu doubl e, odziedziczone po klasie bazowej CBox, plus cztery bajty zajmowane przez wskaźnik m_Cont ents, co w sumie daje 28 bajtów. A gdzie jeszcze cztery bajty? Są one sp rawką kompilatora, który wyrównuje składowe w adresach będących wielokrotnościami wartości ośmiobajtowych. Można to zademonstro wać, dodając dodatkową składową typu i nt do klasy CCandyBox. Okaże się , że rozmiar obiektu nadal wynosi 32 bajty. Wyświetlamy także wartość
zmiennej składowej obiektu myBox klasy CBox - m_Length. Mimo nie ma problemu z uzyskaniem dostępu do tej zmiennej składowej obiektu klasy CBox, usunięcie komentarza z poniższej instrukcji w funkcji mai n() : że
II myCandyBox.m Length
=
10.0;
II Usun ięcie komentarza z lego wiersza spowoduj e błąd.
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
521
spowoduje, że programu nie będzie można skomp ilować . Kompilator wygeneruj e następujący komunikat o błędzie:
err or C2247 : 'CBox: :m_Lengt h' not accessible because 'CCandyBox' uses 'privat e' t e i nherit frem 'CBex' Z powyżs zego komun ikatu jasno wynika , że składowa m_Length z klasy bazowej jest niedo stępna, ponieważ w klasie pochodnej stała s ię ona prywatną zmienną składową. Dzieje się tak, gdyż istnieje domyślny specyfikator dostępu pr i vat e dla klasy bazowej , kiedy definiuje się klasę pochodną - to tak jakby pierwszy wiersz klasy defini cji klasy pochodnej wyglądał następująco:
class CCandyBex : private CBox Zawsze musi być specyfikator dostępu dla klasy bazow ej, który określa status odziedziczo nych składowy ch w klasie pochodnej. Jeżeli nie określimy dostępu do klasy bazowej, to kom pilator domyślnie zastosuje pr i vate . Zmiana definicji klasy CCa ndyBox w pliku nagłówkowym CandyBox.h do poniższej postaci:
class CCandyBex: publi c CBex publi c:
cha r* m_Centents :
CCandyBex(char* st r = "Candy")
II Kons truktor.
(
m_Contents ~ new charC str len(str) + l J:
st rcpy_s(m_Cont ents , st rlen(st r ) + l. str) ;
-CCa ndyBox() { delet eCJ m_Cont ent s: }:
II Destruktor.
}:
spowoduje, że składowa m_Length zostanie odziedziczona przez klasę pochodnąjako publiczna i będzie do stępna w funkcji main (). Dzięki zastosowaniu specyfikatora publi c dla klasy bazo wej wszystkie odziedziczone składowe początkowo określone jako publiczne w klasie bazowej mają taki sam poziom dostępu w klasie pochodnej .
Kontrola dostępu do dziedziczonych składowych Kwestii
dostępu
do dziedziczonych składowych w klasie pochodnej musimy przyjrzeć się prywatnych składowych klasy bazowej w klasie pochodnej.
dokładniej. Rozważmy status
Miałem bardzo dobry powód, aby w powyższym przykładzie wybrać w ersję klasy CBox z publicznymi zmiennymi składowymi zamiast jej bezpieczniejszej wersji z prywatnymi zmien nymi składowymi. Mimo że prywatne zmienne s kł adowe klasy bazowej są również składo wymi klasy pochodnej, to pozostają one prywatnymi składowymi klasy bazowej, a więc funk cje składow e klasy pochodnej nie mają do nich dostępu . Dostęp do nich w klasie pochodnej można uzyskać jedynie poprzez funkcje składowe klasy bazowej , które nie znajdują się
522
Visual C++ 2005. Od podstaw w prywatnej sekcji klasy bazowej. Możemy to bardzo łatwo sprawdzić, zamieniając wszyst kie zmienne składowe klasy CBox na prywatne i umieszczając funkcję Vo l ume()w klasie po chodnej CCandyBox: II Wersje klas. które nie dadzą się skompilować.
class CBox (
publ ic: CBox Cdouble lv ~ 1.0. dou ble wv = 1.0. doubl e hv = 1.0) : m_Lengt hCl v) . m_Width Cwv). m_HeightChv){l pr i vate: double m~Lengt h : double m_W idth: double m_Height :
l: class CCandyBox: public CBox (
publ ic: char* m_Cont ents: II Funkcja obliczająca pojemność obiektu klasy CCandyBox. doub le Vol umeC) const II Bląd --skladowe nie są dostępne.
( ret urn m_Length*m_Width*m_Height: CCandyBo xCc har* st r = "Cu kierek")
II Konstruktor.
(
m_Contents ~ new char[ st rlenCstr ) + l ]: st rcpy_sCm_Cont ent s. str lenCst r) + l. st r ): }
-CCandyBax() ( delete[] m_Cant ent s :
II Destruktor.
l. Programu wykorzystującego te klasy nie będzie można skompilować . Funkcja Vo l ume() w klasie CCandyBox próbuje uzyskać dostęp do prywatnych składowych klasy bazowej, co jest niedozwolone.
~ Uzyskiwanie dostępu do prywatnych składowych klasy bazowei Funkcji Vol ume () można natomiast używać w klasie bazowej. Jeżeli zatem przeniesiemy defi nicję funkcji Vo l ume() do sekcji publicznej klasy bazowej klasy CBox, to nie tylko będzie można skompilować program, ale również obliczyć pojemność obiektu klasy CCandyBox. Utwórz nowy projekt WIN32 o nazwie Cw9_02 z plikiem nagłówkowym Box.h o poniższej zawartości: II Box.h w projekcie Cw9 02.
Plik nagłówkowy CandyBox.h projektu zawiera następujący kod : II Plik naglówkowy Candy Box.h pr ojektu Cw9 02.
#pragma once
#include "Box.h"
class CCa ndyBox : public CBox
publ t e :
char* m_Content s :
CCandyBox(char* st r
~
"Cukierek")
II Konstruktor.
(
m_Cont ents = new char[ st rl en(st r) + l J;
strcpy_s(m_Contents . str len(m_Contents) . str):
-CCandyBox() { delete[] m_Contents: }:
II Destrukt or.
}:
Plik źródłowy projektu Cw9_02.cpp zawiera: II Cw9_02. cpp II Używanie funkcji odziedziczon ej z klasy po dstawowej.
#inc lude II Dla strumienia wejś c ia-wyjścia.
#include <est ri ng> II Dla funkcji strlent) i strcpy i).
#incl ude "CandyBox.h" II Dla klas CBox i CCandyBox.
using st d: :cout :
usi ng std : :endl :
int main( ) (
CBox myBox(4.0,3.0 .2.0) ; II Tworzenie CCandyBox myCandyBox:
CCandyBox myM i ntBo x ( "M i ętówki cienkie j ak l istek" ):
obiektu klasy CBox .
II Tworzenie ob iektu klasy
IICCandy Box.
cout « endl « "Obiekt myBox zajmuje " « S i zeof myBox II Pokazuje, ile pamię ci « " ba j ty" « endl II wymagają te obiekty. « "Obiekt m yCandyBox zajmuje " « sizeof myCandyBox « " ba jt y" « end l « "Obiekt myMintBox zajmuje" « si zeof myMintBox « " ba j ty";
523
524
Visual C++ 2005. Od podstaw cout
« «
end l "P o j emno ś ć
obiekt u myM intBox wynos i
«
myM int Box.Vol uneO : II Sprawdzanie II poj emnoś c i obiektu klasy CCandyBox.
cout « end1: retu rn O: Rezultat
działania powyższego
programu jest następujący:
Obiekt myBox zajmuje 24 bajt y Obiekt myCandyBox zajmuje 32 baj ty Obiekt myMint Box zaj muje 32 baj t y POJemno ś ć obiektu m yM i ntBox wynosi
Jak to działa Interesujące
dodatkowe dane na wyjściu znajdują się w ostatnim wierszu. Pokazuje on war przez funkcję Vo l ume() znajdującą s i ę teraz w sekcji publicznej klasy bazowej. Wewnątrz funkcji pochodnej operuj e ona na składowych tej klasy , które zostały odziedzi czone z klasy bazowej. Jest ona pełną składową klasy pochodnej , a więc można jej swobodnie używać z obiektami tej klasy . tość obliczoną
Pojemność
obiektu klasy pochodnej wynosi 1, ponieważ podczas tworzenia obiektu klasy CCandyBox najpierw został wywołany konstruktor domyślny CBox() (w celu utworzenia częś ci obiektu złożonej z klasy bazowej), który ustawia domyślnie wymiary obiektów klasy CBox na 1.
Działanie konstruktora
wklasie pochodnej
tak jak powiedziałem, konstruktory klasy bazowej nie są dziedziczone przez klasę to są one nadal obecne w klasie bazow ej i za ich pomocą tworzona jest ta częś ć obiektu klasy pochodnej, która zawiera składowe klasy bazowej . Dzieje się tak , poni ewa ż utworzenie bazowej c zęści obiektu klasy pochodnej należy do konstruktora klasy bazowej, a nie do konstruktora klasy pochodnej. Dodatkowo widzieliśmy już, że prywatne składowe klasy bazowej są niedostępne w obiekcie klasy pochodnej, mimo że są dziedziczone, a więc odpowiedzialność za nie spada na konstruktory klasy bazowej. Mimo
że ,
pochodną,
Domyślny
konstruktor klasy bazowej został w ostatnim przykładzie wywołany automatycznie w celu utworzenia bazowej części obiektu klasy pochodnej, ale nie musi się to odbywać w ten sposób. Możemy spowodować wywołanie określonego konstruktora klasy bazowej z konstruk tora klasy pochodnej. Dzięki temu możem y zainicjalizować zmienne składowe klasy bazo wej za pomocą konstruktora innego niż domyślny lub wybra ć konkretny konstruktor klasy, w zależności od danych przekazanych do konstruktora klasy pochodnej .
~ WYWOłYW811ie konslruklorów Jak to działa, prześledzimy na zmodyfikowanej wersji poprzedniego przykładu. Aby klasa po chodna była użyteczna, musimy dostarczyć dla niej konstruktor pozwalający na określenie wym iarów obiektu. W tym celu możemy dodać do klasy pochodnej dodatkowy konstruktor
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
i jawnie w ywoł ać konstruktor klasy bazowej w celu ustawienia wych dziedziczonych z klasy bazowej. Plik nagłówkowy Box.h w projekc ie Cw9_03 ma II Plik
nagłó wkowy
wartości
zmiennych
następującą zawarto ść :
Box.h w proj ekcie Cw9 03
#pragma once
#include
using std : :cout :
using std : :endl :
class CBox (
pub l ic : II Konstruktor klasy bazo wej.
CBoxC double l v ~ 1.0. doubl e wv = 1.0. double hv = 1.0):
mLengt hCl v) , mWidt h(wv ), mHeightChv )
( cout« endl « "Konst rukt or klasy CBox z o st a ł-wy wo ł any . " ;
II Funkcja
ob liczająca pojemność
obiektu klasy CBox.
double Vol umeC ) const
{ ret urn m_Lengt h*m_Wldt h*m_Height :
pri vat e:
doubl e m_Length :
double m_Width:
double m_Height:
}: Zawartość
pliku
nagłówkowego
CandyBox.h proj ektu Cw9_03 j est
następująca :
II Plik n agłówkowy CandyB ox.h proj ektu Cw9 03.
#pragma once
#incl ude
#incl ude "Box.h"
usi ng st d: :cout :
usi ng st d:: endl : class CCandyBox: public CBox (
publ ic:
char* m_Conte nts :
II Konstrukt or usta wiający wymiary i zawart ość
II za pom ocąjawnego wywołania konstrukt ora klasy CBox.
CCandyBox(double lv , double
WV .
double hv. cha r* st r = "Cukierek" )
:CBox(lv. WV , hv)
cout « endl « "Konst rukt or constru ct or2 klasy CCandyBox m_Contents = new char[ str len(st r ) + l J:
strcpy_s(m_Cont ents. str len(m_Cont ents), st r) ;
II Konstruktor ustaw iaj acy zawa rtość
II automatycznie wywo łuje domyślny konstrukt or klasy CBox.
CCandyBox(char* st r = "Cukie rek" )
zo s ta ł wywołany. " :
525 składo
526
Visual C++ 2005. Od podstaw
cout « endl « "Konst rukt or const ructorl klasy CCa ndyBox m_Cont ent s ~ new char[ str len(st r) + l ];
st rcpy_s(m_Conte nts . strlen(m_Contents), st r) ;
,
z o s t ał wywo ł a n y .
}
-CCandyBox( ) { delete[] m_Cont ents ;
II Destrukt or.
};
Dyrektywa #incl ude dołączająca plik nagłówkowy oraz dwie deklaracje using nie są tutaj niezbędne, ponieważ plik nagłówkowy Box.h zawiera ten sam kod. Umieszczenie tych instrukcji w tym miejscu oznacza, że gdyb yśmy usunęli je z pliku nagłówkowego Box.h , ponieważ nie byłyby tam już potrzebne , to nadal nie mielibyśmy żad nych kłopotów z kompi l acj ąpliku CandyBox .h. Zawartość
pliku
źródłowego
Cw9_03.cpp je st następuj ąca :
II Cw9_ 03,cpp II Wywoływanie konstruktora kłasy bazowej za pom ocą konstruktora klasy po chodnej.
#1ncl ude II Dla operacji wejśc ia-wyjśc ia.
#i nc l ude II Dla fu nkcj i strlent) i strcpy() .
#include "Candy Box .h" II Dla klas CBox i CCandyBox,
using st d; :cout ;
using st d: :endl ;
int mai n( ) {
CBox myBox(4.0, 3.0. 2,0);
CCandyBox myCandyBox;
CCandyBox myMi ntBox(1.0. 2,0. 3.0.
"M i ętówk i
cienkie j ak l i st ek,");
cout « « « « « « « cout « «
endl "Obiekt myBox zajmuje " « sizeof myBox II Pokazuje, ile pamięci .. ba jt y" « endl II zajmują te obiekty. "Obiekt myCandyBox zajmuje .. « S izeof myCandyBox .. ba jty" « endl "Obiekt myMint Box zajmuje " « sizeof myMint Box .. bajty"; endl "Po j em n o ś ć obiekt u m yMint Box wynosi " II Sp rawdzanie pojemnoś ci « myMintBox,Volume(); II obiektu klasy CCandyBox. cout « endl ;
ret urn O;
Jak to lJziała Poza dodatkowym konstruktorem w klasie pochodnej, do każdego konstruktora dodali śmy instrukcję wyjściow ą, dzięki czemu wiemy, kiedy są one wywoływane. Jawne wywołan ie konstruktora klasy CBox znajduj e s ię po średniku w nagłówku funkcji konstruktora klasy pochodnej. Jak możn a zauw ażyć , notacja ta jest identyczna z notacją używaną do inicjali zacj i składowych w konstrukt orze:
Rozdział 9.•
II
Wywoływan ie
Dziedziczenie i funkcie wirtualne
527
konstruktora klasy bazow ej.
CCandyBox(double lv. double wv . doub le hv, cha r* st r= "Candy" ): CBox(lv . wv . hv )
Jest to w pełni zgodne z tym, co robimy tutaj, ponieważ w zasadzie inicjalizujemy podobiekt klasy pochodnej klasy CBox. W pierwszym przypadku jawnie wywołujemy konstruktor do myślny dla składowych typu doubl e znajdujących się na liście inicjalizacyjnej : n1_Length, m_Width oraz m_Hei ght. W drugim przypadku wywołujemy konstruktor klasy CBox. Powoduje to wywołanie wybranego konstruktora klasy CBox przed wywołaniem operatora klasy CCandyBox. Rezultat wykonania tego programu jest następujący:
Konstrukto r klasy CBox zo s t a ł wywoła ny .
Konst ruktor klasy CBox zo s t a ł wywo ła ny .
Konst ruktor const ructor l klasy CCandyBox Konst ruktor klasy CBox zo s ta ł wywo ł a ny .
Konstruk tor con st ruct or2 klasy CCandyBox Obie kt myBox zajmuj e 24 bajty
Obiekt myCa ndyBox zajmuj e 32 bajty
Obiekt myMint Box zajmu je 32 bajt y
P Ojemn o ś ć obi ekt u myM i ntBox wynosl 6
Wywołania
konstruktorów
objaśniono
w
zos t ał wywo ła ny.
zo s t a ł wywo ła ny .
poniższej
Dane na ekranie
tabeli:
Tworzony obiekt
Konstruktor klasy CBox został
wyw ołany .
myBox
Konstruktor klasy CBox został
wywołany.
myCandyBox
Konstruktor const ructorl klasy CCandyBox został Konstruktor klasy CBox zo stał
wywołan y.
myM i nt Box
wyw ołan y .
Konstruktor const ruct or2 klasyCCandyBox został
myCandyBox
wyw ołan y.
myMlntBox
Pierwszy wiersz danych wyjściowych otrzymaliśmy dzięki wywołaniu konstruktora klasy pochodzącego od deklaracji obiektu klasy CBox o nazwie myBox. Drugi wiersz danych wyjś ciowych powstał w wyniku automatycznego wywołania konstruktora klasy bazowej, spo wodowanego deklaracją obiektu klasy CCandyBox o nazwie myCandyBox.
CBox,
Zauważ, że
konstruktor klasy bazowej j est wywoo/wany zawsze przed konstruktorem klasy pochodnej.
Następny
powstał dz ięki naszej wersji domyślnego konstruktora klasy pochodnej obiektu myCa ndyBox. Konstruktor ten został wywołany , gdyż obiekt nie jest zainicjalizowany. Czwarty wiersz danych wyjściowy ch powstał w wyniku jawnego wywo łan ia konstruktora klasy CBox w naszym nowym konstruktorze dla obiektów klasy CCandyBox. Wartości argumentów określających wym iary obiektu klasy CCandyBox zostały przekazane do konstruktora klasy bazowej. Dalej mamy wiersz wysłany na wyjście przez sam konstruktor nowej klasy pochodnej, a więc ponownie wywoływane są konstruktory klasy bazowej, po czym następuje wywołani e konstruktora klasy pochodnej.
wiersz
wywołanego dla
528
Visual C++ 2005. Od podstaw Z tego, co widzieli śmy do tej pory, powinno być jasne, że każdemu wywołaniu konstruktora klasy pochodnej towarzyszy wywołanie konstruktora klasy bazowej w celu utworzenia bazo wej częśc i obiektu klasy pochodnej . J eżeli nie określimy konstruktora klasy bazowej, który ma zostać użyty, kompil ator wywoła konstruktor domyślny klasy bazowej . Ostatni wiersz tabeli pokazuje, że inicjalizacja bazowej częś ci obiektu myMi nt Box działa pra widłowo - prywatne składowe zostały zain icjalizowane przez konstruktor klasy CBox. Posiadanie prywatnych składowych klasy bazowej dostępnych tylko dla funkcji składowych klasy bazowej nie zawsze jest wygodne . Może si ę zdarzyć, że będziemy chcieli mieć prywatne składowe klasy bazowej , do który ch dostęp można uzyskać z wnętrza klasy pochodnej. Jak nietrudno się domyślić, w C++ istnieje taka możliwość .
Deklarowanie chronionych składowych klasy dostępu do składowych klasy pub l ic i pri vat e istnieje jeszc ze jeden prot ected. Wewnątrz klasy słowo kluczowe prot ect ed ma taki sam efekt jak słowo kluczowe pri vate : do chronionych (protec ted) składowych klasy dostęp można uzyskać wyłącznie za
Poza specyfikatorami
pomocą
funkcji s kł ad o wy c h tej samej klasy (a także funkcji składowych klasy zaprzyjaź o klasach zaprzyjaźnionych będzie mowa w dalszej części tego rozdzi ału) . Przy użyciu słowa kluczowego protect ed definicję klasy CBox moglibyśmy ponownie zdefiniować w następujący sposób :
nionej -
II Plik
nagłówkowy
Box.h w proj ekcie Cw9 04,
#pragma once
#i ncl ude
using std : :cout:
using st d: :endl :
class CBox
{
puhl t c :
II Konstruktor klasy bazowej.
CBox(doub le l v = 1.0. double wv = 1,0. double hv = l O): m_Length(l v) . m_Width(wv). m_He ight (hv ) { cout« endl « "Konstru ktor klasy CBox zos ta ł wywo ł a ny , ": II Destruktor klasy CBox - do sledzenia
wywołań,
{ cout « "Destruktor klasy CBox
zo s ta ł wywo ła ny"
-csc« )
« endl: }
protected:
double m Lengt h:
double m)idth;
doub le m_Height :
}:
Od tej pory zmienne składowe są nadal prywatne , ponieważ nie mają do nich dostępu funkcje globalne, ale s ą one dostępne dla funkcji skł ado wych klasy pochodnej .
zwykłe
Rozdzial9. • Dziedziczenie i funkcie wirtualne
529
Rm!I!mI Zastosowanie chronionych składowych Zastosowaniu chro nionych zmiennych składowyc h przyj rzymy si ę dokładniej , używając tej wersji klasy CBox do utworzenia nowej wersji klasy CCandyBox, która bę dzi e uzys kiwała dostęp do składowych klasy bazowej poprzez włas ną fu nkcję składową Vol uret ): II Plik
nagl ćwkowy
CandyBox. h w projekci e Cw9 04.
#pragma ance
#include "Box.h"
#include
us ing st d: :cout ;
using st d: :endl ;
class CCandyBox : publ ic CBox
{
publ i c:
cha r* m_Cont ent s ;
II Funkcja klasy pochodnej
obliczająca pojemność.
double Volume() const
{ retu rn mLengt h*mWldth*mHeight ; }
II Kons trukto r ustawiają cy wym iary i zawartość
II za pomocąjawnego wywołania konstruktora klasy CBox .
CCa ndyBox(doubl e l v. doub le wv. doub le hv. cha r* st r ~ "Ca ndy ")
:CBox(l v. wv . hv) II Konstruktor. cout « endl « "Konstrukt or construct or2 klasy CCandyBox zosta l m_Cont ent s = new cha r[ str len(str) + l ] ;
st rcpy_s(m_Contents . strl en(m_Cont ent s ). str);
wywo ła ny . ";
II Konstruktor ustawiający zawartość
II aut omatycznie wywoluje domyślny konstrukt or klasy CBox.
CCandyBox(char* str ~ "Cukierek") II Konstruktor.
{
cout « endl « "Konstrukt or const ruct orl klasy CCa ndyBox m_Content s = new char[ st rlen(st r) + l ] ;
cout « "Destruktor klasy CCa ndyBox zast al delet e[] m_Cont ents .
wywoła n y . "
« endl ;
};
Kod funkcji mai n() projektu ew9_04 wyg ląda jak p on i żej : II Cw9_04.cpp
II Używanie specyfikatora
#incl ude #incl ude #incl ude "CandyBox h"
dostępu protected.
II Dla strum ienia wejścia -wyjś cia .
II Dla funkcj i strlen() i strcpy ().
II Dla klas CBox i CCandyBox.
530
Visual C++ 2005. Od podstaw usi ng st d: :cout :
usi ng st d: :end l :
int ma in() {
CCandyBox myCandyBox :
CCa ndyBox myToff eeBox(2. 3. 4.
" Ciągną c e s i ę
toff i "):
cout « endl « "Poj emn o ś ć obiektu myCandyBox wynosi " « myCa ndyBox.Vo l ume() « endl « "P o j emn o ś ć obiektu rnyToffeeBox wynosi" « rnyToff eeBox.Vol ume() : II coul < < endl < < my ToffeeBox.m~englh; II Usunięci e komentarza z lego wiersza spowoduje
bląd.
cout « endl :
ret urn O:
Jak to działa Powyższy
program oblicza pojemność dwóch obiektów klasy CCandyBox poprzez wywołanie funkcj i Vol ume( ), która jest składową klasy pochodnej . Funkcja ta wykonuje obliczenia dzięki uzyskaniu dostępu do odziedziczonych zmiennych składowych m_Length, m_Wi dt h, m_Height. Składowe w klasie bazowej zostały zadeklarowane jako chronione i takie też są w klasie po chodnej. Rezultat działania tego programu widoczny jest poniżej :
Konst ruktor klasy CBox zos t a ł wywo ł any .
Konst ruk tor const ructorl klasy CCandyBox zos ta ł Konstruk tor klasy CBox z os t a ł wywo ł any .
Konst ruktor const ructor2 klasy CCandyBox z os t a ł PO j emn o ś ć obiekt u rnyCandyBox w ynosi 1
P o j emno ś ć obiekt u rnyToffeeBox w ynosi 24
Destrukt or klasy CCandyBox z o s ta ł wywoł a ny .
Dest ruktor klasy CBox z o st ał wyw o ł a n y.
Destrukt or klasy CCandyBox z o s t a ł wywo ła ny.
Destruktor klasy CBox zo s t a ł wywo ł any .
wywo ł a ny .
wywoł a ny .
Z danych na ekranie wynika, że pojemność obu obiektów klasy CCandyBox została obliczona prawidłowo. Pierwszy obiekt ma domyślne wymiary ustawione przez domyślny konstruktor klasy CBox, a więc jego pojemność wynosi I. Drugi obiekt ma wymiary zdefiniowane jako wartości początkowe w jego deklaracji. Na
wyjściu
pokazana została także kolejność wywołań konstruktora i destruktora, dzięki czemu jak każdy obiekt klasy pochodnej jest niszczony w dwóch etapach.
możn a prześledzić,
Destroktory obiektu klasy pochodnej >tyWorywane są w odwrotnej kolejności w stosunku do konstruktorów tego obiektu. Ta ogólna zasada ma zastosowanie zawsze. Najp ierw wyworywany jest konstrukt or klasy bazowej, a nast ępn ie konstruktor klasy po chodnej. Przy niszczeniu obiektu natomiast pierwszy >tyWo~vany j est destruktor klasy pochodnej, a dopiero po nim destroktor kimy bazowej.
Rozdział 9.
• Dziedziczenie ilunkcie wirtualne
531
Aby si ę przekonać, że chronione składowe klasy bazowej pozostają chronione w klasie pochod nej, można usunąć komentarz z instrukcji poprzedzającej in strukcję return w funkcji ma int ). Jeśli to zrobimy, otrzymamy od kompilatora następujący komunikat o błędzie : err or C2248: ' CBox : :m_Length ' : cannot access pr ot ected member decl ar ed i n cl ass ' CBox'
Komunikat ten informuje,
że
zmienna
składowa
m_Length jest
niedostępna.
Poziom dostępu do dziedziczonych składowych klasy Jak wiemy, jeżeli nie podamy specyfikatora dostępu dla klasy bazowej w definicji klasy po chodnej, to domyślnie zostanie zastosowany specyfikator pr i va te. W efekcie tego odziedzi czone z klasy bazowej publiczne i chronione składowe w klasie pochodnej stają się składowymi prywatnymi. Składowe prywatne klasy bazowej pozostają w niej prywatne i są niedostępne dla funkcji składowych klasy pochodnej . W rzeczywisto ś ci pozostają one prywatne w klasie bazowej bez względu na to, jak klasa bazowa jest określona w definicji klasy pochodnej. Użyliśmy także słowa
kluczowego publ i C jako specyfikatora klasy bazowej. Dzięki temu klasy bazowej zachowują swój poziom dostępności w klasie pochodnej, a więc skła dowe publiczne nadal są publiczne, a chronione są nadal chronione. składowe
Ostatnią możliwościąjest
zadeklarowanie klasy bazowej jako chronionej. W takim przypadku odziedziczone publiczne składowe klasy bazowej w klasie pochodnej stają się chronione. Składowe chronione i prywatne klasy bazowej zachowują swój poziom dostępności w klasie pochodnej. Zasady te zostały podsumowane na rysunku 9.3. Powyższe reguły mogą wydawać się
szych
stwierdzeń
nieco zawiłe, ale można je sprowadzić do trzech na temat dziedziczonych składowych klasy pochodnej:
poniż
Składowe klasy bazowej zadeklarowane jako prywatne nigdy nie w klasie pochodnej.
są dostępne
• Zdefiniowanie klasy bazowej jako publicznej nie zmienia poziomu jej składowych w klasie pochodnej .
dostępności
•
• Zdefiniowanie klasy bazowej jako chronionej powoduje, stają się chronione w klasie pochodnej.
że jej
publiczne
składowe
Możliwość zmieniania poziomu dostępności odziedziczonych składowych w klasie pochod nej daje pewien stopień elastyczności, ale należy pamiętać , że nie można zwiększyć poziomu dostępności określonego w klasie bazowej. Można go tylko zmniejszyć . Oznacza to, że klasa bazowa musi zawierać publiczne składowe, jeżeli chcemy mieć możliwość zmiany poziomu dostępności w klasie pochodnej . Może się wydawać, że zaprzeczamy tym samym koncepcji hermetyzacji danych w klasie, która zakłada ochronę ich przed nieautoryzowanym do stępem, ale jak się przekonamy, często się zdarza, że klasy bazowe definiowane są w taki sposób wy łącznie po to, aby służyły jako baza do tworzenia innych klas. Nie planuje się tworzenia obiek tów bezpośrednio z tych klas.
532
Visnal C++ 2005. Od podstaw
RysUnek 9.3
c1ass CABox:p u blic CBox [
r---dziedzi czona jako _ _ public: ~
dz iedziczona ja ko _ _ protected:
c1assCBox {
public:
======~=!=,...,
l
protected:
I
~~vat e:
c1ass CBBox:protected CBox (
dziedziczona jako_ protected:
======~-l dziedziczona jako - . protected:
Brak dostępu
c1ass CCBox:private CBox [
-
dziedziczona jako _ _ private:
'------- dziedziczona ja ko _
private:
Konstruktor kopiująCY wklasie pochodnej Należy pam ięta ć, że
konstruktor kopiujący wywoływany jest auto matycznie w momencie deklaracji obiektu inicjalizowanego innym obiektem tej samej klasy . Spójrzmy na poni ższe instrukcje:
CBox myBox(2.0. 3.0. 4.0): CBox copyBox(myBox) :
II Wywołuj e konstruktor.
II Wyw oluje konstruktor kopiujący.
Pierwsza z powyższych instrukcji wywołuje konstruktor przyjmujący trzy argumenty typu double, a druga wywołuje konstruktor kopiujący. Jeżeli nie dostarczymy własnego konstruktora kop iującego , to kompilator wywoła domyślny konstruktor kopiujący składowa po składowej obiekt inicjalizujący do odpowiadających im składowych nowego obiektu. Aby móc śledzi ć, co s ię dzieje podczas wykonywania operacji, w klasie CBox można umieścić własną wersję konstruktora kopiującego. Następnie klasa ta może posłużyć jako baza do zdefiniowania klasy CCandyBox. II Plik nagłówko wy Box.h w proj ekcie Cw9 05.
#pragma once
#include
using std: :cout :
using st d: :endl :
cla ss CBox {
II Defini cj a klasy bazowej.
Rozdział 9.
• Dziedziczenie i funkcie wirtualne
533
publ ic: II Konstrukt or klasy bazowej.
CBox(doub le l v = 1.0. double wv = 1.0. doubl e hv = 1.0):
.m_Lengt h(l v) . m_Widt hCwv) . m_Hei ght Chv)
l cout « end l « "Konst rukt or klasy CBox z os tał wyw ołan y ." :
II Konstrukt or kopiujący.
CBoxCconst CBox&i nltB) (
cout « endl « "Konst rukt or m_Length ~ init B. m_Length ;
m_Width = initB.m_Widt h;
m_Height = init B.m_Height ;
kopi ujący
klasy CBox
II Destrokt or klasy CBox - do ś ledzen ia
wywołań .
-CBoxC)
l cout « "Dest ruktor klasy CBox
z os ta ł wywo ła ny . "
z o st ał wywoła ny ." ;
« endl ; }
protect ed:
double m_Length ;
doub le m_Width;
double m_He ight ;
}; Należy również
w tym miejscu przypomnieć sobie, że parametr konstruktora kopiującego musi być referencją, aby uniknąć nieskończonej liczby jego samowywołań . Proces ten zostałby spowodowany koniecznością skopiowania argumentu przenoszonego przez wartość. Konstruk tor kopiujący w naszym przykładzie w momencie wywołania wysyła na ekran komunikat, dzięki czemu wiemy, kiedy został on wywołany. Poniżej
II Plik
znajduje
się
nagłówkowy
nowa wersja klasy CCa ndyBox:
CandyBox.h w proj ekcie Cw9 05.
#pragma once
#i ncl ude "Box. h"
#include
usi ng st d: 'cout ;
usi ng std : :endl ;
class CCa ndyBox: publ ic CBox
l
publ l C:
char* m_Content s ;
II Funkcj a klasy pochodnej
obliczająca pojemność.
double Volume C) const
( retur n m_Length*m_Width*m_Hei ght ; }
II Konstrukt or ustawiają cy wym iary i zawarto ść
II za pomocąjawnego wywo łan ia konstrukt ora klasy CBox.
CCandyBoxCdouble l v. double wv . double hv. cha r* str = "Candy")
:CBox Cl v . wv. hv) II Konstrukt or.
cout « endl « "Konst ruktor const ruct or2 klasy CCandyBox mConte nts = new cha r[ st rlenCs tr ) + l J ;
z o s t ał wywoł a n y . " ;
534
VislJal C++ 2005. Od podstaw st rcpy_s(m_Cont ent s . st rlen(m_Cont ent s ). st r) ; II Konstru ktor ustawiający zawartość
II automatycznie wywo luje domyślny konstruktor klasy CBox .
CCandyBox(cha r* st r = "Cukt erek") II Konstrukto r.
(
cout « endl « "Konstrukt or const ructor l kl asy CCandyBox m_Cont ent s = new char[ st rlen(st r) + l ] : strcpy_s(m_Contents . str len(m_Contents) . st r ) : -CCandyBox( )
z o s ta ł wywo ł any .
II Destrukt or.
{
cout « "Dest rukt or klasy CCandyBox delet e[] m_Content s:
zo s ta ł wywo ł a ny . "
end l;
«
}:
W powyższym kodz ie brakuj e jeszcze konstruktora jego wer sji dostarczonej przez kompil ator.
kop iującego ,
a
w i ęc będziem y pol egać
na
~ Konstruktor kOpiujący wklasie pochodnei Prz ed chwil ą zdefin iowany konstruktor kop iuj ący gram ie: II Cw9_05.cpp
II Używa n ie konstrukt ora
kopi ującego
#i nclude #include #incl ude "CandyBox.h" using st d: :cout ;
II Dek laracj a i inicj alizacja . II Użyc ie konstruktora kopiującego.
cout « end l « « « «
"Po jem no ś ć
obiektu chocBox wynosi "
«
chocBox.Vol ume()
endl " Poj emno ś ć
obiekt u choco lateBox wynosi "
«
choco late Box.Vol ume()
endl;
ret urn O:
Jak to działa [lub czemu nie działa) Kiedy uruchomimy wersję te stową tego programu , to poza spodziewanymi danymi wymi zobaczymy okno dial ogowe widocz ne na rysunku 9.4 .
wyjści o
Rozdzial9. • Dziedziczenie i funkcje wirtualne
535
RV51mek9.4
Program: ,..
FlIe: dbgdel,cpp lIne : 52
For h fiJrmatlon on row 'fO'J p-ogram can cause sn sssertco fai""e, ss e the Visua l C++ documenta tiÓrlon asssrts . (l'ress Retry to debug !he application) ,
I
Przerw ij
I Porów p-óbę I
19"oruj
Kliknię cie przycisku Przerwij spowoduje zniknię cie okna dialogowego i ukaże s i ę nam spo dziewane okno konsoli z wynikiem działani a programu. Z danych w tym oknie wynika, że kon struktor kop iujący wygenerowany przez kompilator dla klasy pochodnej automatycznie wy wołał konstruktor kopiujący klasy bazowej .
Jak jednak widać, nie wszystko jest w porządku . W tym przypadku konstruktor kopiujący wygenerowany prze z kompilator spraw ia probl emy, poni eważ obszar pamięci wskazywany przez skład ową klasy pochodnej m_Content s w drugim zadeklarowanym obiek cie wskazuje ten sam obszar pamięc i co w pierwszym obiekcie. Kiedy jeden z tych obiektów zostaje znisz czony (kiedy wychodzi poza zasięg na końcu funkcji main ()), to zostaje zwolniona pamięć zajmowana przez tekst. Podczas niszczenia drugiego obiektu destruktor próbuje zwolnić pa mięć, która została już zwolniona prze z destruktor niszczący poprzedni obiekt - i to jest powod em pojawienia się komunikatu o błęd zie w oknie dialo gowym .
~ Naprawianie blędlJ zoperatorem kopiuiącym Z probl emem tym możemy sobie poradzi ć , dodając poni żs zy kod konstruktora do sekcj i publicznej pochodnej klasy CCandyBox w projekcie ew9_05:
kopiującego
II Konstrukt or kop iujący klasy pochodn ej.
CCandyBox(const CCandyBox&i nitCB) {
cout
«
endl
«
"Konst rukt or
II Zarezerwuj
nową pamięć .
m_Content s
=
II Kop iowanie
k o p i u ją cy
klasy CCandyBox
new char[ st rlen(lnit CB .m_Cont ents )
l
J:
łańcucha.
st rcpy_s(m_Conte nt s . st rlen(i nit CB.m_Contents) Aby
+
zo stał wywo ł a ny.
+
l . initCB.m_Conte ntsJ:
przekonać się ,
chomi ć
jak teraz działa nowy konstruktor kopiujący, należy powyżs zy kod uru z tą samą funk cją mai n( ) co w ostatnim przykładzie .
Jak lo działa Teraz program zach owuje
się
znacznie lepiej i daje
następujący
rezultat:
536
VisualC++ 2005. Od podstaw Kons tru ktor klasy CBox z o s t a ł wywo ł a ny . Konstruktor constructor2 klasy CCandyBox zost ał wywo ł a n y. Konstru ktor klasy CBox zost a ł wywo ł a ny. Konst ruktor k op i u jący klasy CBox z os t a ł wyw ołan y . Po j emn o ś ć obiekt u choc Box w ynosi 24 Po j emn o ś ć ob iekt u chocol ateBox wynosi l Dest rukt or klasy CCandyBox z os t ał wywo ł a ny . Dest rukt or klasy CBox zost ał wywo ł a ny . Dest rukt or klasy CCandyBox z o s t a ł wywo ł a ny . Dest rukt or klasy CBox z o s t a ł wywo ł a ny. A jednak cały czas coś jest nie tak. W trzecim wierszu danych wyjściowych pojawia się infor macja, że dla części obiektu choco1ateBox tworzonej na podstawie klasy CBox wywołany zost ał konstruktor domyślny zamiast konstruktora kopiującego. W konsekwencji obiekt ten ma domyśln e wymiary zamiast wymiarów obiektu in icjalizującego, a więc pojemność jest nie prawidłowa. Powodem zaistniałej sytuacji jest fakt, że pisząc konstruktor dla obiektu klasy pochodnej, j esteśmy odpowiedzialni za prawidłową inicjal izację składowych klasy pochodnej. Do nich zali czają się także składowe odziedziczone. Rozwiązaniem
klasy w
liści e
jący wygląda
tego problemu jest wywołanie konstruktora kopiującego dla bazowej częśc i inicj alizacyjnej konstruktora kopiującego klasy CCandyBox. Konstruktor kopiu wtedy następująco :
Teraz konstruktor kopiujący klasy CBox wywoływany jest z obiektem klasy i ni t CB. Przeka zana do niego zostaje tylko bazowa część obiektu, a więc wszystko działa jak powinno. Jeżeli do ostatniego listingu dodamy wywołanie konstruktora kopiującego klasy bazowej , rezultat działania programu będzie następujący :
Konstruktor klasy CSox zost ał wywoła ny . Konstrukto r constructo r2 klasy CCandyBox z ost ał wy wo ł a ny . Konstrukto r kop iują cy kl asy CBox zosta ł wyw o ła ny . Konstru ktor kO P1UjąCY klasy CCandyBox zost a ł wywoł any . Poj emn o ś ć obiektu chocBox wynosi 24 Po j emn oś ć obiekt u chocolat eBox w ynosi 24 Dest rukt or klasy CCandyBox z o st ał wywoła ny. Dest rukt or klasy CBox zos t a ł wywo łan y . Dest ruktor klasy CCandyBox z o s t a ł wywo ła ny . Dest rukt or klasy CBox zost ał wywo ła ny . Z danych na ekranie wynika, że wszystkie konstruktory i destruktory zostały wywołane w pra widłowej kolejnośc i , a konstruktor kopiujący części bazowej obiektu choco1ateBox wywołany został przed konstruktorem kopiującym klasy CCandyBox. Pojemnoś ć obiektu chocol at eBox klasy pochodnej jest teraz taka sama jak poj emność obiektu i n icj a l i zuj ące go, czyli prawidłowa.
Rozdzial9. • Dziedziczenie i1unkcie wirtualne W związku z powyższym
537
możemy sformułować następną złotą zas adę:
Pisząc
dowolnego rodzaju konstruktor klasy pochodnej, jesteśmy odpowiedzialni za ini wszystkich sk ładowych obiektu tej klasy, włączn ie ze wszys tkimi składowym i • odziedziczonymi. cja liz ację
Składowe
klasy jako przyjaciele
funkcje zaprzyjaźnione klas . Dzi ęki temu za ich wolny dostęp do wszystkich składowych klasy. O czywiście nie ma powodu, dla którego funkcja zaprzyj aźniona nie mogłaby być składową innej klasy.
W rozdziale 7.
nauczyli śmy się deklarow a ć
pomocą mogliśmy uzyskać żadnego
Przypuśćm y , że
mamy
zdefiniowaną klasę
CBot t l e reprezentuj ącą butelkę:
cl ass CBottle (
pub1ic: CBot t le (double hei ght . double dlamete r) (
m_He ight = height: m_O iamete r = diameter : )
private : doub le m_He ight ; do uble m_Oia met er ;
II Wys okoś ć butelki. II Śre dn ica butelki .
};
Teraz potrzebujemy klasy reprezentującej opakowanie dla dwunastu butelek, którego wymiary automatycznie są dopasowywane do butelek określonego rodzaju. Klasę tę możemy zdefinio wać następująco :
II Wysok ość butelki. II Cztery rz ędy... 11 ...p o trzy butelki.
II Długość karto nu. II Szerokość kartonu. II Wysok ość karton u.
l: Konstruktor w powyższej klasie ustawia wysokość na taką samą wartość jak wysokość bute lek, które będą przechowywane w tym kartonie. Długość i szerokość ustawiane są w oparciu o ś red n i cę butelki w taki sposób, aby zmieściło się ich pięć w pudełku.
538
lisual C++ 2005. Od podstaw Jak już wiemy, takie coś nam nic zadziała . Zmienne składowe klasy CBott l e są prywatne, a zatem nicdostępne dla konstruktora klasy CCa rt on. Wiemy również , że deklaracja z użyciem słowa kluczowego f ri end w klasie CBottl e naprawi ten błąd:
cla ss CSott le (
publ lC:
CBott le(doub le height . double di ameter )
(
m_Height = helght:
m_Diamete r = diameter :
pr ivat e: doub le m_Helght : double mDi ameter:
II Wysokość butelki. II Średnica butelki.
II Wpuszczamy konstruktor kartonu .
fr iend CCarton ' :CCart on(const CBot t le&aBot t le) : }:
Jedyna różnica pomiędzy deklaracją fri end w powyższym kodzie i w rozdziale 7. jest taka, że tutaj trzeba podać nazwę klasy oraz operator zasięgu z nazwą funkcji zaprzyjaźnionej w celu jej identyfikacji. Aby kod ten można było skompilować , kompilator musi mieć in formacje na temat konstruktora klasy CCart on. W związku z tym przed definicją klasy CBot t l e należałoby umieścić dyrektywę #i ncl ude dołączającą plik nagłówkowy zawierający defi nicję klasy CCarton.
Klasy zaprzyjaźnione Możemy także pozwolić
wszystkim funkcjom składowym j ednej klasy uzyskać dostęp do innej klasy, deklarując jąjako klasę zaprzyjaźnioną. Aby CCa rt on jako klasę zaprzyjaźnionąklasy CBott l e, należy dodać do definicji
wszystkich zmiennych zdefiniować klasę
tej klasy
deklarację
składowych
fri end:
f riend CCart on: Dzięki umieszczeniu powyższej deklaracji w klasie CBott le wszystkie funkcje CCarton mają dostęp do wszystkich zmiennych składowych klasy CBott l e.
składowe
klasy
Ograniczenia klas zaprzyjaźnionYCh Przyjaźń pomiędzy klasami nie jest odwzajemniana. To, że klasa CCarton jest zaprzyjaźniona z klasą CBot 11e, nie oznacza, że klasa CBott l e jest zaprzyjaźniona z klasą CCa rt on. Jeżeli chce my, aby tak było , to musimy dodać deklarację fri end dla klasy CBott le do klasy CCarton.
Przyjaźń pomiędzy
klasami nie jest dziedziczona. Jeżeli zdefiniujemy inną klasę na bazie klasy klasy CCar t on nie będą miały dostępu do jej zmiennych składowych, nawet do zmiennych odziedziczonych z klasy CBot t l e.
CBott l e, to
składowe
Rozdzial9. • Dziedziczenie i funkcje wirtualne
539
Funkcje wirtualne
Przyjrzymy się bliżej zachowan iu odziedziczonych funkcji składowy c h oraz ich związkom z funkcjami składowym i klasy pochodnej. Do klasy CBox możemy d odać funkcję wysyłającą na ekran pojemność obiektu tej klasy. W uproszczeniu klasa ta wygląda n as t ępując o: II Plik
nagłówko wy
Box.h w proj ekcie Cw9_06.
#pragma once #i ncl ude using std : :cout : usi ng std : :endl: class CBox
II Klasa bazowa.
{
publ i c:
II Funkcj a pokazująca pojemność obiektu .
void ShowVolume() const
{
cout
« «
II Funkcja
endl
"Pojenno ś ć użytkowa
ob liczająca pojemność
obiekt u CBox wynoSl " « Vol umeO ;
obiektu klasy CBox.
double Volume( ) const { ret urn m_Length*m_Wi dt h*m_Height : } II Konstruktor.
CBox(doub le l v = 1.0. double wv = 1.0. doub l e hv = 1.0)
:m_Length( l v). m_Widt h(wv). m_Hel ght(hv ) {}
prot ect ed:
doubl e m_Length:
doubl e m_Widt h;
double m_Hel ght :
l: pojemność dowolnego obiektu klasy CBox możemy wysłać na ekran za pomocą wywo dla niego funkcji ShowVol ume t ). Konstruktor ustawia wartości zmiennych składowych w liście inicjalizacyjnej, dzięki czemu niepotrzebne są żadne instrukcje w ciele funkcji. Zmienne składowe są takie same jak wcze śniej i zostały zdefiniowane jako chronione, a więc są dostępne dla funkcji składowych wszystkich klas pochodnych.
Teraz łania
Przypu śćmy, że
chcemy utworzyć klasę pochodną reprezentującą pudełko do przechowywania o nazwie CG l assBox. Zawartość tego pud ełka jest delikatna i w związku z tym, że doda wany jest do niego ochronny materiał pakujący, pojemność jest mniejsza niż pojemność bazo wego obiektu klasy CBox. Potrzebujemy zatem innej funkcji Vol ume O , która będzie uwzględ niała tę nową sytuację . Dodamy ją do klasy pochodnej : szkła
II Plik
nagłówkowy
GlassB ox.h w p rojekcie Cw9_06 .
#pragma once #include "Box.h"
540
Visual C++ 2005. Od podstaw el ass CG lassBox. publ ic CBox
II Klasa p ochodna .
(
publ iC : II Funkcja obliczająca p ojemność' obiektu klasy CGlassBox II zostawiająca 15% na materiał wypełniający.
do ub le Vol umeC) eonst { ret urn O.85*m_Lengt h*m_W idt h*m_He ight: } II Konstruktor.
CG lassBoxC double l v. doubl e wv, double hv) . CBox( lv . wv. hv ){}
}:
w tej klasi e pochodnej mogłoby być więcej dodatkowych składowych, ale nie zbytnio komplikować kodu i skoncentrujemy się na razie na spo sobie działania funkcji składowych. Konstruktor klasy pochodnej wywołuje tylko konstruktor klasy bazowej w swojej liści e inicjalizacyjnej w celu ustawienia wartości zmiennych składowych . W jego ciele nie są potrzebne żadne instrukcje. Bazową wersję funkcj i Vol ume ( ) przesłoniliśmy jej nową wersją, aby móc podczas wywoływania odziedziczonej funkcji ShowVo l ume( ) dla obiektu klasy CGl assBox wywołać wersję funkcji Vol ume( ) zdefiniowaną w klasie pochodnej .
Z
pewnością
będziemy
~ Zastosowanie Ollziellziczonei fllllkcii Widzimy teraz, jak nasza klasa pochodna działa w praktyce . Możemy ją łatwo wypróbować, obiekt klasy bazowej ora z obiekt klasy pochodnej o tak ich samych wymiarach, a następnie sprawdzając , czy obliczane są prawidłowe pojemności. Wykonująca to funkcja ma i n( ) wygl ąda następuj ąco : tworząc
II Cw9-,-06.cpp
II Zachowanie odziedziczonych funkcj i w klasie pochodnej.
#i nel ude
#i nel ude "GlassBox.h" usi ng st d: :eout :
usinq std : .endl :
II Dla kłas CBox i CGlassBox.
i nt main( ) {
CBox myBox(2.O. 3 O. 4. O) : CG lassBox myG l assBox(2.O. 3. O. 4.O):
II Deklaracja obiektu klasy bazo wej. II Dekl aracja obiektu klasy pocho dnej - takie same II wymiary.
myBox.ShowVol ume() : myGlassBox.ShowVolume () :
II Pokaż pojem ność obiektu kłasy bazow ej . II Pokaż p ojemność' obiektu klasy pochodnej.
eout « endl :
return O:
Jak lo działa Uruchomienie powyższego programu da P Ojemno ś ć u ży t k owa P oj e m n o ś ć użyt k owa
następuj ący
obiekt u CBox wynosi 24 obiekt u CBox wynosi 24
rezultat:
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
541
Rezultat ten nie tylko j est nudny i powtarzający si ę , jest także kata strofą! Program dzi ała cał kowicie inaczej, niż oczekiwali śmy , i j edynym interesuj ącym w tym wszystkim problemem jest pow ód takiego stanu rzeczy. Najwyraźniej fakt, że drugi e wywołani e doty czy obiektu klasy pochodnej CG l ass Box, nie jest w ogóle brany pod uwagę. Dowodem jest n ieprawidłowa po jemn o ś ć na wyj ś ciu. Pojemn o ś ć obiektu klasy CG l as sBox powinna być zdecydowanie mniej sza od poj emnośc i obiektu kla sy bazowej CBox o tak ich samych wymiarach . Powodem, dla którego o t rzy ma li ś my nieprawidłowy wynik, jest to, że wywołanie funkcji Vol ume( l w funkcji ShowVol ume( lwykonywane j est przez kompilator jeden raz dla wszystkich i że wywoływana jest wersja tej funkcji zdefiniowana w klasie bazow ej . Funkcj a ShowVo l ume ( l jest funkcj ą klasy bazowej i podczas kompilacji klasy CBox wywołanie funk cji Vol ume() je st traktowane jako wywołanie funkcji Vol ume ( l należącej do klasy bazowej . Kompilator nie wie nic na temat żadnej innej funkcji Vo l ume( l. Nazywa się to wiązaniem statycznym (ang. static resolution) wywołania funkcji, ponieważ wywołan i e to jest ustalone przed wykonaniem pro gramu . Czasami nazywa si ę to także wczesnym wiązaniem (ang. early binding), ponieważ wybrana funkcja Vo l ume ( l jest zw iąza n a z wywoła nie m funkcji ShowVo l ume ( l podczas kom pilacji programu. programie mieliśmy nadzieję, że kwestia dotycząca wyb oru konkretnego wy funkcji Vol ume ( l w danym przypadku zostanie rozstrzygnięta w czasie wykonywania programu. Operacja ta nazywa się wiązaniem dynamicznym (ang. dynamie binding) lub wią zaniem późnym (ang. lale binding). Chcemy, aby właściwa wersja funkcji Vo l ume ( ) wywo ływana przez funkcję ShowVo l ume ( ) była określana przez rodzaj przetwarzanego obiektu, a nie określ an a z góry przez kompilator przed uruchomieniem programu. W
powyższym
wołania
Bez wątpienia nie zdziwi Ci ę fakt, że w C++ można takiego czego ś dokonać - w przeciwnym przypadku cała ta dysku sja byłaby na próżno. W tym celu musimy użyć czegoś , co nazywa s ię funkcją wirtualną (ang . virtualfunctioni.
Czym jest lunkeja wirtualna Funkcja wirtualna to funk cj a składowa klasy zadeklarowana przy użyciu s łow a kluczowego vi rtua l . Jeżeli w klasie bazowej zadeklarujemy funkcję wirtualną, a w klasie pochodnej znaj duje s i ę jej inna defini cja, to jest to znak dla kompil atora, że nie chcemy, aby funkcja ta była wi ązana statycznie. To, czego chcemy, to, aby wyb ór wersji funkcji wywoływanej w progra mie był dokonywany na pod stawie rodzaju obiektu, dla którego jest ona wywoływana .
~ Naprawa klasy eOlassBol Aby program działał tak, j ak się spod ziewal i śmy, należy tylko dodać sł owo kluczowe vi rt ual do definicji funkcji VOl ume ( l w obu klasach. Wypróbujemy to w nowym projekcie Cw9_07. P oni żej widoczna jest definicja klasy CBox: II Plik nagłówkowy Box.h w projekcie Cw9 07.
#pragma ance
#include
usi ng st d:: cout :
uswg st d: :endl :
542
lisual C++ 2005. Od podstaw class CBox
II Klasa bazo wa.
(
publ i c:
II Funkcja pokazująca pojemność obiektu.
void ShowVolume() const (
cout « end l «
" Pojemno ś ć u ży t k owa
klasy CBox wynosl "
II Funkcia oblicza '
Volume():
«
CBox .
virt ua l double Vo l ume() const II Kons trukto r.
CBox(double l v = 1.0, double wv = 1.0, do uble hv = 1.0) :m_Lengt h(l v), m_Widt h(wv) , m_Helght (hv ) {} protect ed:
double m_Lengt h:
doub le m_Wi dth:
doub le m_Height :
}:
Plik GlassBox.h zawiera następujący kod : II Plik
nagło wko wy Glass łłox. h
w projekcie Cw9_07.
#pragma once
#i nc l ude "Box .h"
cl ass CGlass Box : pub lic CBox
II Klasa pochodna.
(
publ ic. II Funkcja ob liczająca poje mność obiektu klasy CGlass Box.
II zostawia ' c 15% na material w e łniai c .
virtual double Volume() const
II Konstruktor.
CGla ssBox(double l v, double wv. doub le hv): CBox(l v.
II Dek laracja obiektu klasy bazowej. II Deklaracj a obiekw klasy pochodnej - lakie same II wymiary .
Rozdzial9.• Dziedziczenie i funkcie wirtualne myBox.ShowVol ume( ); myGlassBox ShowVo l ume( ) ;
543
II Pokaż pojemn oś ć obiektu klasy bazowej. II Poka ż pojemność obiektu klasy pochodnej.
(out « end1: ret urn O;
Jak to dziala Uruchomienie tego programu z dodatkiem tylko jednego do definicji funkcji Vol ume ( ) da następujący rezultat: Po j emno ś ć użytkowa Po jem no ś ć u żyt k owa
małego słowa
kluczowego vi rt ual
klasy CBox wynosi 24 klasy CBox wynosi 20.4
Teraz program najwyraźniej działa zgodnie z naszymi oczekiwaniami. Pierwsze wywołanie funkcji ShowVol ume ( ) w obiekcie klasy CBox o nazwie myBox powoduje wywołanie funkcji Vol u rre () w wersji dla klasy CBox. Drugie wywołanie obiektu klasy CG l ass Box o nazwie myGl ass Box wywołuje wersję tej funkcji zdefiniowaną w klasie pochodnej . Warto zauważyć , że mimo iż wstawiliśmy słowo kluczowe vi rt ual do definicji funkcji Vo l ume( )w klasie pochodnej, to nie było to konieczne. Definicja wersji klasy bazowej funkcji jako wirtualnej byłaby wystarczająca . Gorąco jednak zachęcam do stosowania słowa kluczo wego vi r tu al także w definicjach funkcji w klasach pochodnych, gdyż dzi ęki temu osoba czytająca nasz kod może się łatwo zorientować , że dana funkcja jest wirtualna, a co za tym idzie, że jest ona wybierana dynamicznie. Aby funkcja zachowywała się jak funkcja wirtualna, musi mieć taką samą nazwę, listę para metrów oraz typ zwracany we wszy stkich klasach pochodnych co w klasie bazowej . Dodat kowo, jeżeli funkcja klasy bazowej jest zadeklarowana jako const, to funkcja klasy pochodnej również musi być const . Jeżeli funkcje będą miały inne parametry lub typy zwracane albo jedna z nich zostanie zadeklarowana jako const , a druga nie, to mechanizm funkcji wirtualnych nie zadziała . W takim przypadku funkcja operuje z wiązaniem statycznym ustalonym w cza sie kompilacji . Mechanizm funkcji wirtualnych daje niezwykle duże możliwości. Czasami można usłyszeć termin polimorfizm w kontekście programowania zorientowanego obiektowo - odno si się on do możliwośc i funkcji wirtualnych. Coś, co jest polimorficzne, może występować pod wieloma postaciami, jak na przykład wilkołak, dr Jekyll czy polityk przed wyborami i po wyborach. Wywołanie funkcji wirtualnej daje różne efekty w zależności od rodzaju obiektu , dla którego została wywołana.
Warto zauważyć,
że funkcja
Vo lume( ) w klasie pochodn ej CGl assBox w rz eczywistości klasy pochodnej wersję tej funkcji w klasie bazowej. Aby odwołać się do funk cji Vo lume ( ) w klasie bazowej z fu nkcj i z klasy pochodnej, należy posłużyć się operatorem zas ięgu w następujący sposób : CBox : : Vo lume( ).
przesłania funkcjom
544
Visual C++ 2005. Od podstaw
Używanie wskaźników
do obieklówklas
Używanie w skaźników
z obiektami klasy bazowej i klasy pochodnej jest bardzo ważną tech do obiektu klasy bazowej można przypisa ć adres obiektu klasy pochodnej lub klasy bazowej. W ten sposób można użyć wskaźnika typu wskaźnik do obiektu klasy bazowej, aby uzyskać różne zachowania z funkcjami wirtualnymi w zależności od rodzaju obiektu wskazywanego przez wskaźnik. Stanie się to jaśniejsze, kiedy przyjrzymy się temu na przykładzie. niką. Wskaźnikowi
~ Wskaźniki do klas bazowych ipochodnych W programie tym będziemy używali tych samych klas co w poprzednim, ale dokonamy pew nych modyfikacji funkcji mai nO , aby używała wskaźnika do obiektu klasy bazowej. Utwórz projekt o nazwie Cw9_08 i utwórz w nim pliki Box.h i GlassBox.h o takiej samej zawartości jak w poprzednim projekcie. Pliki te można skopiować z folderu projektu Cw9_07 . Doda wanie istniejącego pliku do projektu j est proste. Należy kliknąć prawym przyciskiem Cw9_08 w zakładce Solution Explorer, a następnie wybrać z menu Add/Existing Item, Potem wybie ramy plik nagłówkowy, który chcemy dodać do projektu. Po dodaniu plików nagłówkowych zajmujemy się modyfikacjąpliku Cw9_OS.cpp : II Cw9_OS.cpp
II Ustawianie wskaźnika na adres obiektu klasy bazow ej. II Wyświetlanie pojemności obiektu klasy bazowej. II Ustawianie wskaźnika na obiekt klasy po chodnej. II Wyświetlanie pojemności obiektu klasy poch odnej.
do obiektów klasy bazowej.
eout « endl :
ret urn O:
Jak to działa Klasy są takie same jak w projekcie Cw9 _07, ale funkcja main( ) została zmodyfikowana w taki sposób, że do wywołania funkcji ShowVol ume ( ) używa wskaźn ika . Jako że używamy wskaźnika, to przy wywołaniu funkcji posługujemy się operatorem pośredniego dostępu do składowej. Funkcja ShowVol ume ( ) wywoływana jest dwa razy i oba te wywołania wykonywane
Rozdzial9. • DziedzIczenie I lunkcje wirtualne są
545
tego samego wskaźnika pBox do obiektu klasy bazowej. Za pierwszym razem zawiera adres obiektu klasy bazowej myBox, a za drugim razem adres obiektu klasy pochodnej myGl assBox. przy
użyciu
wskaźnik
Rezultat
działania
programu jest
P o jemnoś ć u żyt k owa Poj emn ość u ży tkowa
następujący:
klasy CBox wynosi 24 klasy CBox wynos i 20 .4
Rezultat ten j est identyczny z rezultatem poprzedniego programu, w którym w funkcji zasto sowaliśmy rzeczywiste obiekty.
wywołaniu
Z tego przykładu można wyciągnąć wniosek, że mechanizm funkcji wirtualnych działa równ ie dobrze poprzez wskaźnik do obiektu klasy bazowej, gdzie określona funkcja wybierana jest na podstawie typu wskazywanego obiektu. Pokazano to na rysunku 9.5. pBox->Sh owVolumeO
I
Ws ka ź n i k
this jest
ustawiony na pBox
!
c1assCBox
{.,.l /
virtual double Volume
void ShowVolumeO co nst {
(o ut « end l
« · PoJ e m n o ść użytkowa
« Vo lum e {);
Oconst
/
obiektu klasy CBox wynosi " Ws kaźni k pBoxw skazują cy
_ _ o biekt klasy CBox
---
===============
pBox w skazują cy o bie kt klasy CGlassBox
Ws kaź nik
c1assC
ssBox
virtual double Volume
Oconst
(...l
R.sunek 9.5 Oznacza to, że nawet gdy nie znamy dokładnie typu obiektu wskazywanego w programie przez wskaźn ik klasy bazowej (na przykład kiedy do funkcji jako argument przekazywany jest wskaźnik), to mechanizm funkcji wirtualnych przypilnuje, aby wywołana została właściwa funkcja , Daje to ogromne możliwości , a więc musisz to dobrze zrozumieć . Polimorfizm sta nowi fundamentalny mechanizm w języku C++, którego będziesz często używać.
Używanie Jeżeli
relerencii zlunkciami wirtualnymi
zdefiniujemy funkcję z parametrem będącym referencją do klasy bazowej , to będziemy mogli do niej przekazywać jako argumenty obiekty klasy pochodnej. Podczas wykonywania tej funkcji odpowiednia funkcja wirtualna dla przekazanego obiektu wybierana jest automa tycznie . Możemy to zaobserwować, modyfikując funkcję mai n() w ostatnim przykładz ie, aby wywoływała funkcję z parametrem w postaci referencji .
546
Wisnal C++ 2005. Od podstaw
~ Używanie relerencii zlunkejami wirtualnymi Przenieśm y wywoł anie funkcję
funkc ji ShowVo l ume( l do oddzielnej funkcji i wywołajmy z funkcj i ma i n( l :
II Cw9_09.cpp
II Używanie referencji do
wywo ływania funkcji
tę oddzi e ln ą
wirtualnych.
#include #incl ude "GlassBox.h·· usi ng st d: :cout : usi ng st d: :end l ;