216 6 40MB
Polish Pages 1205 Year 2007
Podręcznik
dla początkujących programistów . języka Visual C++ 2005 ..... Jak
pisać
wydajne programy uruchamiane systemie Windows?
bezpośrednio w
.....
Jak błyskawicznietworzyć aplikacje na platformę .NET? Jakie techniki
zastosować,
.....
by wygodnie zarządzać bazami danych?
PROGRAMMER TO ."
PROGRAMMER™
Wilii
Podręcznik
dla początkujących programistów języka Visual C++ 2005
OD PODSTAW
Ivor Horton
~ wrox
Im
Helion http:/ /heUon.pl
Tytuł oryginału :
Beginning Visual CH 2005
Tłumaczenie : Łukasz
Marcin
Piwko Rogóż
(wstęp ,
rozdz. l - lO), (rozdz. II - 22 , dod . A, B)
ISBN : 978-83-246-0652-8
Copyright © 2006 by Ivor Horton
Ali Rights Reserved. This translation published under license.
Translation copyright © 2007 by Wydawnictwo Helion .
Polish language edition published by Wydawnictwo Helion.
Copyright © 2007
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
n a rzę d zi
882 88 2 884 885 885 88 6 88 7 88 8 889 88 9 88 9 89 2 89 5 89 6 896 897 898 900 90 2 90 2 906 90 7 907 910 911 9 12 9 14 9 15 9 16 9 17 9 19 920
921 922 92 2 922 924 92 6 92 8 929 929 929 931 932 935 937 939 939 942 943 944
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
15
977
,
977 980 980 982 985 985 986 987 987 989 992 1002 1004 1004 1005 1006 1009 1013 1016 1022 1022 1023 1023 1024 1026 1028 1029 1031 1032 1032
16
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.
Zdarzenia:
Dane z klawiatury
Wciśnięcie
Wciśnięcie
lewego przycisku myszy
prawego przycisku myszy
---------
WINDOWS
Przetwarzanie donych z klawiatury
Przetwarzanie zdarzenia naciśnięcia lewego przycisku myszy
Przetwarzanie zdarzenia naciśnięcia prawego przycisku myszy
------ -
-;
Dane programu
Naszprogram
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
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( )
{
l ong numl = O. num2
~
O. num3 = O. num4 = O:
num4 = (numl = la . num2 = 20. num3 ~ 30) ; cout « endl
« " w a r t o ś c 'i ą szeregu wy r aże ń"
« "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++:
Rozdział 2.•
Dane. zmienne i działania arytmetyczne
105
lączność
·Operatorv
lewa () [ ] - >
lewa
I - + (jednoargumentowy) - (jedno argumentowy) - - (jednoargumentowy) * (jednoargumentowy)
(typecas t ) st at ic_cast const_cast
++
prawa
dy na~ic _cas t
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 ~ 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 = ' a' ) && (letter
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 ń :
« endl
« endl « "1 Got owane jajk a"
« endl « "2 Sma żo ne j aj ka"
« endl « "3 Jajec znica"
« endl « "4 Jajka na mi ękk o "
« endl « end l « "Podaj numer wybranego dania:
cin » cho ice:
«
end l
switchCchoicel
{
case 1: cout « endl « break:
case 2: cout « end l « break:
case 3: cout « end l « break:
case 4: cout « endl « break : default: cout « endl « j ajek ." « endl:
"Ugot uj ki l ka j aj ek. " "U s maż
"Zrób
ki l ka jajek." jajec z n i c ę . "
«
«
"Ugot uj kilka j ajek na " Podana
«
endl :
end l :
endl :
mi ękko. "
«
endl :
l iczba jest nieprawi d ł owa. spróbuj surowych
}
retur n O:
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 ~
'a' &&let t er
= l && dice = 1 && di ce = l & &dice
, 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
arrayAsamples
=
{
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 w tablicy
mogą być
dowolnego typu, a więc z łatwością możemy utwo-
rzyć tablicę łańcuchów :
arr ayA names
~
{ "Jack". "Jane". "Joe". "Jessi ea". "Ji m". "Joanna"};
Rozdział 4.
• Tablice, łańcuchy znaków i wskaźniki
235
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 ć
arrayAnarnes : 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 Aargs)
r arrayAsamples II Generowanie losowych
=
gcnew array(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
88,30 66,96 48,04 46,37 50 ,44 63,91 47 ,51 54,45 56,83 9.94
Jak to działa Najpierw tworzymy
tablicę
ar rayA samp les
~
50 elementów typu doub 1e: gcnew array(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 Aargs) (
array- St r t nq" >" names arrayA weight s =
= {
{ "Jill ". "Ted". "Mary" . "Eve". "Bi ll ". "Al "} ; 103. 168. 128. 115. 180. 176}:
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 Aargs) arrayLength : 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 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++) (
Console: :Write(fo rmat . vetueslrl) : if(( i+1)%numberPerLine == O) Conso le: :WriteLi neO: ret urn O:
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" :
arrayPadLeft 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- >IndexOfIndexOf (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, 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 Aargs) {
arrayLength){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ć :
arrayIndexOfAny(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
arrayAdata = {l.5 . 3.5. 6.7. 4.2. 2.l }: interior ptr 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
++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 pstart = &dat a[OJ; interior_ptr 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 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 :
arrayA 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 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 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,
if( ( ' found) && item ->CompareTo(element) == O) {
fou nd = t rue : conti nue: }
else (
if(index == newDat a->Length) (
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 (MaxEl ement (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 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.
arrayAchars = gcnew arrayCst 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
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
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
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 ayA buffer = gcnew array(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) {
arrayA subst rChars = gcnew array(*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 :
II Zmni ejsz licznik znaków '(' do dopasowania.
II Zwiększ licznik znakó w
't
do dopasowania.
) ++ ( *i ndex):
Consol e: :WriteL1ne(L "Kon iec exit (l ) : ret urn substr :
wyra żema .
dane wejśc lOwe
mus i ał y być n t ep ra wt d ł o-e . " ) :
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):
II Teraz przesuń w prawo,
cout « endl
« " W spó ł r z ęd n e chaty Hut 2 to "
i"
« Hut2,Left « ". " « Hut 2.Top « « Hut2.Right « ". " « Hut2 .Bottom:
cout « end1
« "Powie rzchnia podwó rka wynosi "
« AreaCYard):
cout « « « «
end1
"Powie rzchnia basenu wynos i "
AreaCPool )
end1:
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 .
więcej
Spój rzmy na
przykładową instrukcję definiującą klasę reprezentującą pudełka :
class CBox {
pub lic:
doub le m_Length:
doub le m_Width:
doub le m_Height:
}: 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 :
poniższy
IICw7_02.cpp II Tworzenie i używanie pudelek.
#i nclude
using std: :cout:
using std: :endl :
cla ss CBox
II Definicja klasy w zasięgu globalnym.
(
publi c: double m_Lengt h: double m_Width : doub le m_Height ;
II Dlugość pudelka w centymetrach. II Szerokość pudelka w centymetrach. II Wysokość pudelka w centymetrach.
);
i nt ma in() {
CBox boxl : CBox box2:
II Deklaracja obiektu box l typu CBox. II Deklaracja obiektu box2 typu CBox.
doub le boxVolume = 0.0:
II Przechowuje pojemność pudelka.
boxl .m_Height = 18.0: boxl.m_Length ~ 78.0: boxl. mWidth ~ 24.0:
II Definicja wartości
II zmiennych składowych
II obiektu boxl .
posłuży
nam
Rozdział7 .•
box2 .mHei ght = boxl .m Height - 10: box2 .m=Length = boxl .m=Length/2.0: box2 .m_Widt h = 0.25*boxl .m_Length:
Definiowanie własnych typÓW danych
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
II Dostarczanie i
używanie
konstruktora
domyślnego.
#include
using std: :cout :
using st d: :endl :
class CBox
II Definicja klasy o zasięgu globalnym.
(
publ ic: double m_Length; double m_Width: double m_He ight:
II Dlugość pudelka w centymetrach. II Szerokość pudelka w centymetrach. II Wysokość pudelka w centymetrach.
II Definicja konstruktora.
CBoxC dou ble lv, doub le by, double hv) (
cout « endl « "Konstruktor m_Length = lv, m_Width ~ by: m_Height = hv :
został wywołany.";
II Ustawianie wartości
II zmiennych skladowych.
II Definicja konstruktora domyślnego.
CBox() (
cout
«
II Funkcja
endl
«
obliczająca pojemność
double VolumeC)
{
} }:
int mai n()
{
"Konstruktor
do myślny został wywoła ny.":
pudelka,
384
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 .
CBoxC double lv = 1.0. doub le bv = 1.0 , double hv = 1.0): m_LengthClv). m_W idt hCbv). m_He ightC hv) cout « endl « "Konstrukt or
zost ał wywołany ." ;
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
"
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 Ą 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.
ref class Box
{
publ ic:
II Konstruktor bezargum ento wy
dostarczający do myślne wartości pól.
BoxC) . Length C1,O ) , Width C1.0) , Height C1.0) {
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 (
publ ic: II Konstruktor bezargum entowy
dostarczający domyślne wartośc i
dla pól.
BoxC ): Lengt h(l .O) , Widt hCl .O ), Height (l ,O) {
Console : :Wr lteLi ne(L"Konstruktor beza rgument owy zosta ł
wywoł a ny . " ) :
} 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
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 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~ 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:
arrayA Names:
publ i c:
Name ( . . .arrayA 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 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
gcnew Name (L" Ebenezer ". L"I saiah". L"Ezra". L"In igo". L"Whel kwhistle" ) :
~
A
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 .
Podsumowanie Zdobyliśmy właśn ie podstawową wiedzę
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 Dlugoś ć p udelka w centymetrach.
II Sz erokoś ć p udełka w centymetrach.
II Wysokość pude łka w centymetrach.
}: II Funkcja
pokazująca dz ia łanie
destruktora klasy CBox.
i nt ma i n( ) (
CBox boxes[5] : CBox cigar (S.O. 5.0 . 1.0) : CBox match(2.2. 1.1. 0.5) : CBox* pBl = &cigar: CBox* pB2 = O:
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 (
defin icję
destruktora (Iisting S_02).
mai n()
II Deklaracja obiektu.
CMessage motto("Lepiej
późn o n i ż
wcal e "):
II Obiekt dynami czny.
CMessage* pM= new CMessage("W szyscy motto .ShowIt () : pM->Showlt() : cout « endl :
II delete pM;
są
sobie równi . "):
II Wyświ etl p ierwszy komunikat.
II Wyświetl drugi komunikat.
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 .
446
Visual C++ 2005. Od podstaw
Rysunek 8.1
CMessage mottol ( " Promieniowani e zabija twoje geny.") ; mottol pmessage
I
adres ~
\
I Promieniowanie zabija twoje geny. I
II
CMessagemotto2 (mottot) ;
Wywołuje domyślny
mottol
konstruktor
kopiujący
motto2 kopiowanie
pmessage ' - - - - - ' I .
Promieniowanie zabija twoje geny. Jeżeli łańcuch
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 .
#i ncl ude II Dla strumienia wejśc ia -wyjśc ia.
usi ng st d: :cout;
using st d: .e ndl :
class CBex
II Defi nicj a klasy o zasięgu globalnym.
(
pub 1i c:
II Definicja konstruktora .
CBex(doub le l v cout
«
II Funkcja
endl
~
«
1.0. double wv = 1.0. doub le hv = 1.0) : m_Length(lv ), m_Widt h(wv ) , m_Height( hv ) "Konst rukt or
obliczająca pojemnoś ć
zo s ta ł wY~lo ł a ny. "
p udelka.
double Vol ume( ) censt (
boa 1 operator>: const CBex& aBox) const:
II Przełado wan ie operatora większośc i .
II Definicja destruktora.
-CBox() {
cout
«
"Destrukt or
z o s t a ł wywoła ny"
«
endl :
}
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.
-CBox ()
{cout « "Destruktor
zo s tał wywo ł a ny . " «
endl :}
pr i vat e :
double m_Lengt h: double m_Widt h: double m_Height :
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.
void ShowBox() const
{
eout « m_Length «
« mW idth «
« m_Heig ht « endl:
privat e: double m_Lengt h: double m_Widt h: double m_Height :
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 myDat aCI O.O ) : Powyższy
kod definiuje ob iekt typu CSample s, 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 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. 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
, >=, ==, < 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
(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 należy wstawić =. 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%( ) .
W tej wersji operacji
mnożenia zmieniliśmy
śmy bezpośredni o u żyć
482
Visual C++ 2005. Od podstaw
Rysunek 8.6 (Sox Multiply : mnożnik n niepar zysty : ]*aSox
t t
\
\
W
1\
Ą "'~'\.---_\. . . .\
] *H
'\.
\
'y \.
\.
(Sox Multiply: mnożnik n parzysty : 6*aSox
1\
r
\
2*W
l
~ "''''''-'' - - _\--'',.\ ]*H
,,'----- - - "
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
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
ShowVol ume() :
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 ;
II D/a klas CBox i CGlassBox.
void Output(const CBox& aBox);
II Prototyp funkcji.
int mai n() (
CBox myBox(2.O. 3. O. 4. O) : CGlassBox myGl assBox (2.O. 3.O. 4. O) : Output( myBox) : Output(myGlassBoxJ:
II Dekla racja obiektu klasy bazo wej. II Deklaracja obiektu klasy p ochodnej o takich samych II wymiarach.
II Wysianie na wyjście pojemności obiek tu klasy bazowej. II Wysianie na wyjście pojemnośc i obiektu klasy pochodnej.
cout « end l ; ret urn O:
I
(Oid Output(const CBox& aBox)
~Sh owV01Ume ( ) : Pliki Box.h i GlassBox.h
- -=
mają t aką s amą zawartość
jak w poprzednim projekcie.
Jak lo działa Teraz fu nkcj a mai n( l składa się przede wszystkim z dwóch wywołań fun kcj i Output O . Pierwsze wywołanie z argumentem w postaci obiektu klasy bazowej , a drugie z argumentem w postaci obiektu klasy pochodnej . Jako że parametr jest referencjądo klasy bazowej, funkcja Out put( l przyjmuje j ako argument obiekty obu klas, po czym następuje wywołanie odpo wiedniej funkcj i wirtualnej Vol ume( l - w zależności od obiektu inicj alizującego referencję. Rezultat działania tego programu jest identyczny z poprzednim, co j est dowodem na to, mechanizm funkcji wirtualnych rzeczywiście działa z parametrami w postaci referencji.
że
Niepelne delinicie klas Na początku poprzedniego przykładu znajduje się deklaracja prototypu funkcji Output( l . Aby móc przetworzyć tę deklarację, kompilator potrzebuje dostępu do definicji klasy CBox, ponie waż parametr jest typu CBox&. W tym przypadku definicja klasy CBox jest już dostępna, ponie
Rozdział 9.•
waż dołączyliśmy
zawiera
za
pomocą
dyrektywy #i ncl ude plik
l -i nc l ude dołączającą plik
dyrektywę
Dziedziczenie i funkcje wirtualne nagłówkowy
nagł ówkowy
547
GlassBox.h, który z kolei
Box.h.
Może s i ę
jednak zdarzyć, że będziemy mieli tak ą deklarację, ale nie będzie możliwo ści do definicji klasy w taki sposób. Wtedy będziemy potrzebować jakiegoś innego sposobu na okre śleni e , że nazwa CBox odnosi się do typu klasowego. W tym celu możemy posłużyć się niekompletną definicją klasy CBox umieszczon ą przed prototypem funkcji Outp ut t ). Instruk cja niek ompletnej defini cj i klasy CBax jest bardzo prosta:
łączen ia
clas s CBox; Instrukcja ta identyfikuje CBax jako nazwę klasy, która jeszcze nie zos tał a zdefiniowana. Kom pilatorowi to wystarczy , gdy ż wie on, że jest to nazwa klasy, co pozwala mu na przetwo rzenie prototypu funkcji Outp ut () . Je żeli nie podamy w jaki ś sposób informacji , że CBax jest nazwą klasy , to komp ilator zgłosi komunikat o błędzie .
Funkcje CZysto wirtualne Możliwe, ż e będziemy chcieli umieścić funkcję w irtualną w klasie bazowej , aby móc j ą póź niej przedefiniować w klasie pochodnej celem dopasowania do obiektów tej klasy, ale w klasie bazowej nie można nadać tej funkcji żadnej znaczącej definicji .
Na przykład możemy mieć klasę CCant ai ner, która mogłaby służyć jako klasa bazowa dla defi nicji klasy CBax lub CBattle, a nawet CTeapat . Klasa CConta i ner nie miałaby żadnych zmien nych składow ych , ale my moglibyśmy zechcieć dostarczyć funkcję wirtualną Vol ume ( ) dla wszystkich klas pochodnych. Jako że klas a CCant ai ner nie zawiera żadnych zmiennych składowych, a co za tym idzie - nie ma żadnych wymiarów, nie można napisa ć żadnej sen sownej definicji dla funkcji Va l ume ( ) w tej klasie. Można jednak zdefiniować tę klasę z funk cją Vol ume () w następujący sposób: II Plik
n agłó wko wy
Container.h projektu Cw9_ l O.
#pragma once
#i nel ude
usi ng st d: :eout:
uSl ng st d: :endl :
elass CCont ai ner
II Generyczna klasa bazowa do tworzenia konteneró w.
(
publ i c:
II Funkcja ob liczająca pojemnos ć - brak tresci.
II Jest to funkcj a czys to wirtualna. co zostało oznaczone przez '= O'
vi rtual do uble Volume () eonst II Funkcj a
~
O:
wyświetlająca pojemn ość .
virt ual void ShowVol ume( ) eonst (
eout « end l
« " P oj emno ś ć wynosi " « Volume():
};
548
Visual C++ 2005. Od podstaw Instrukcja d efiniująca funkcj ę w i rtualną Vol ume () określa jąj ako pustą poprz ez znak równości i zero znajdujące s i ę po nagłówku funkcji . Funkcja taka nosi nazw ę funkcji czysto wirtualnej. Każd a klasa pochodna tej klasy musi zaw ierać definicję funkcji Vo l ume () lub ponownie defi niować jąjako fun kcj ę czysto wirtualną. Jako że funkcję Vo l ume ( ) zad eklarowaliśmy jako const, jej implementacja w klasach pochodnych musi być również const . Należy pamiętać , że funkcje o takiej samej nazwie i l iście parametró w, z kt órych jedna jest const, a druga nie, s ą różnymi funkcjam i. Inaczej mówiąc, za pomocą słowa kluczowego const można przeładować funkcję.
Nasza klasa zawiera również funkcję ShawVol ume () , która wyświetla pojemność ob iektów klasy pochodnej . Ze względu na fakt, że jest wirtualna, można ją podmienić w klasie pochod nej. Ale gdyby nie była, to wywołana zostałaby jej wersja dostarczona w klasie bazowej .
Klasy abstrakcyjne czysto wirtualną zwana jest klasą abstrakcyjną. Nazy wa s ię ona nie można zdefiniować ob iektu klasy zawierającej funkcję wirtualną. Istniej e ona tylko w celu definiowania j ej klas pochodnych. J eżeli w klasie pochodnej funkcja czysto wirtualna klasy bazowej jest również czysto wirtualna, to klasa ta także jest abstrakcyjna. Klasa
zawierająca funkcję
abstrakcyjną, ponieważ
Na podstawie powyższego przykładu klasy CConta ine r można wyciągnąć ni esłuszny wniosek, że klasa abstrakcyjna nie może mieć zmiennych składowych. Klasa abstrakcyjna może mieć zarówno zmienne, jak i funkcje składowe. Jedynym wyznacznikiem tego, że kla sa jest abs trakcyjna, jest obecność w niej funkcji czy sto wirtualnej . Pon adto klasa abstrakcyjna może zawierać więcej n iż j edną funkcję wirtualną. W takim przypadku w klasie pochodnej muszą znajdować s i ę definicje wszystkich funkcji czysto wirtualnych kla sy bazowej albo stanie si ę ona również klasą abstrakcyjną. Jeżeli w klasie pochodnej zapomnimy określić funkcj ę skła dową Vol ume () jako const , to będzie ona nadal klasą abstrakcyjną, ponieważ będzie zawie r ała czysto wirtualną funkcję składową Vol ume ( ) typu const, jak również funkcję Vol ume () , która nie jest typu const.
~ Klasa abstrakcyjna Możemy zaimplementować klasę
CCan reprezentującą puszkę piwa albo coca-coli razem z ory Obie te klasy są pochodnymi zdefiniowanej wcześniej klasy CCont a i ner. Definicja klasy CBox j ako podklasy klasy CCantai ne r wygląda następująco:
ginalną klasą CBox.
II Plik naglówkowy Box.h w projekci e Cw9 10.
# ragma ance
#incl ude "Cant ai ner.h" #incl ude
using std : :cout :
uSlng st d: :endl ;
cl ass CBox: publ ic CCont ai ner publi c:
II Dolącza definicję klasy CConlainer.
II Klasa pochodna.
Rozdział 9.•
Dziedziczenie ifunkcie wirtualne
549
II Funkcj a pokazująca pojemność obiektu.
virt ua l void ShowVo l ume() const
{
cout « endl
« " Poj em n o ś ć
u żyt k owa
obiektu klasy CSox wynosi " « Volume( ) :
II Funkcja obliczajqca pojemn oś ć obiektu klasy CBox.
vi rt ua l double Vol ume( ) const
{ ret urn m_Length*m_Widt h*m_He lght : }
II Konstruktor.
CSox( do uble l v = 1.0. double wv = 1.0. double hv = 1.0)
:m_Lengt h(l v) . m_Widt h(wv ) . m_Helght(hv){ }
protected :
double m_Lengt h:
doub le m_Widt h;
double m_Height,
};
Wiersze na białym tle są takie same jak w poprzedniej wersji klasy CBox. W zasadzie klasa ta jest taka sama jak poprzednio, z wyjątkiem miejsca, w którym określiliśmy, że jest ona pochod nąklasy CConta iner. Funkcja VolumeO w tej klasie została w pełni zdefiniowana (jest to waru nek konieczny, jeżeli chcemy używać tej klasy do tworzenia obiektów). Drugim wyjściem, jakie nam pozostaje , jest określenie jej jako funkcji czysto wirtualnej , jako że została ona w ten sposób zdefiniowana w klasie bazowej, ale wtedy nie moglibyśmy tworzyć obiektów klasy CBox. Definicja klasy CCan znajduje
si ę
w pliku
nagłówkowym
Can.h i jest następująca :
II Plik n agłówkowy Can.h w projekcie Cw9 _la.
#pragma ance
#i nc l ude "Canta i ner. h" exte rn const dauble PI .
II Dołą cza defini cję klasy CCo ntainer.
II Zmienna Pl jest zdefini owan a gdzieś indziej.
class CCan: publ ic CCantainer (
publ ic: II Fun kcj a
ob liczająca pojemność puszki.
virt ual dauble Val ume( ) canst
( retur n 0.25*PI*m- Di ameter*m- Diamete r*m- Hei ght;
II Konstruktor.
CCa n(double hv = 4.0. double dv
=
2.0): m_Height (hv) , m_Diamete r (dv){}
pratected :
do uble m_Height :
dauble m_Diamet er :
I:
W klasie CCan znaj duje się równ ież definicja funkcji Vo7ume() oparta naformule hnr2, gdzie h oznacza wysokość puszki, a r promień puszki w przekroju poprze cznym. Pojem nos ć obliczana jest jako iloczyn wysokości i pola podstawy puszki. Wyrażenie w definicji funkcji zakłada, że stała globalna PI j est zdefi niowana. Z tego względu umieściliśmy na początku instrukcję extern informujqcq, że PI j est zmienną globalną typu const doub7e,
550
Visnal C++ 2005. Od podstaw zos tała zdefin iowana w innym miej scu - w tym p rogramie jej definicja znaj duj e w pliku źródlowym Cw9_ 1O.cpp. Warto równ ież zauważyć, że przedefiniowana zos tała
która się
funk cja ShowVo7ume ( ) w klasie CBox, ale nie w klasie CCan. Rezultat tego zobaczymy, kiedy uruchomimy program i obejrzymy dane wyjś cio we . Powyższe
możemy przetestować
klasy
rając ego funkcję
przy
użyciu następującego
pliku
źródłowego
zawie
main( ):
II Cw9j O. cpp II Używanie klasy abstrakcyjnej.
#lncl ude "Box. h" #incl ude "Can.h" #i ncl ude using st d: :cout : using std: .endl :
II Dla klas CBox i CContainer. II Dla klasy CCan (i CContainer). II Dla strumienia wejścia -wyjś cia .
const doubl e
II Definicja globalna zmie nnej Pl.
PI~
3.14159265:
int ma i n(void) { II Wskaźnik do abstrakcyj nej klasy bazowej II zainicjalizowany adrese m obiektu klasy CBox .
CContainer* pC1 = new CBox (2.0, 3.0. 4.0): II Wskaźn ik do abstrakcyjnej klasy bazowej II zain icjalizowany adrese m obiektu klasy CCan.
CContai ner* pC2
=
new CCan (6 5. 3.0) ;
pC 1->ShowVol ume (); pC2 ->ShowVo l ume() : cout « endl;
II Wysyłanie na wyjście pojemności obu II wskazywa nych obiek tów.
delete pC! : delete pC2:
II Czyszczen ie obszaru wolnej II ...
pamięci.
retu rn O:
Jak lo działa W programie tym zadeklarowaliśmy dwa wskaźniki do klasy bazowej CCanta i ner. Mimo że nie możemy tworzyć obiektów klasy CCanta i ne r (ponieważ jest to klasa abstrakcyjna), to możemy definiować wskaźniki do tej klasy, w których możemy następnie przechowywać ad resy obiektów klas pochodnych. W rzeczywistości w takim wskaźniku można przechowywać adres dowolnego obiektu klasy będącej pośrednią lub bezpośrednią pochodną klasy CConati ner, Wskaźnikowi pC I zostaje przypisany adres obiektu klasy CBox, utworzone go w obszarze wol nej pamięci przez operator new. Dru giemu wskaźnikowi w podobny sposób zostaje przypi sany adres obiektu klasy CCa n. Oczywiście
ze
musimy po
zakoń czeniu
de7ete .
względu
na fakt, że obiekty klasy pochodnej są tworzone dynamicznie, pra cy z nimi wyczyś ci ć wolną pamięć za pomo cą operatora
Rozdział 9.•
Rezultat
działania powyższego
Dziedziczenie i funkcje wirtualne
551
programu jest następujący:
obi ektu klasy CSox wynosl 24 wynos i 45 .9458
P oj emno ś ć u ż y t k owa P oj e mno ś ć
Jako że funkcję ShowVo l ume( ) zdefiniowaliśmy w klasie CBox, wersja tej funkcji w klasie po chodnej wywoływana jest dla obiektu klasy CBox. Nie z d e fi ni o wa l iś my jej w klasie CCa n, a wi ęc dla obiektu tej klasy wywoływana jest wersja tej funkcji znajdująca się w klasie ba zowej , z której dziedziczy klasa CCan. Ze wz gl ęd u na fakt, że funkcja wirtualna Vol umeO zo stała zaimplementowana w obu klasach pochodnych (jest to konieczne, gdyż jest to funkcja czysto wirtualna w klasie bazowej) , jej wersja jest wybierana w czasie działania programu poprzez wskazanie wersji należącej do klasy wskazywanego obiektu. W związku z tym dla w skaźnika pCl wywoływana jest wersja z klasy CBox, a dla wskaźnika pC2 wersja z klasy CCan. Dzięki temu za każdym razem otrzymujemy prawidłowy wynik. tylko jednego wskaźnika i przypisać mu adres obiektu klasy dla obiektu klasy CBox funkcji Vol ume ( ). Wskaźnik klasy bazowej może zawierać adres obiektu dowolnej klasy pochodnej , nawet j eże li je st ich kilka. W ten sposób uzyskujemy automatyczny wybór właściwej funkcji wirtualnej spośró d całego zestawu klas pochodnych. Nieźle , prawda? Równie dobrze
CCa n (po
mogliśm y u żyć
wywołaniu
Pośrellnie klasy bazowe Na początku rozdziału stwierdziłem, że klasa bazowa podkla sy może być pochodną jes zcze innej klasy. Zilustrujemy to na podstawie nieznacznie zmodyfikowanego kodu poprzedniego przykładu, a dodatkowo zobaczymy sposób użycia funkcji wirtualnych w przypadku dwupo ziomowego dziedziczenia.
R!lmIIjI Dziedziczenie wielopoziomowe Jedyne, co musimy
zrobić,
to
kładu . Powiązania pomiędzy
dodać definicj ę
tymi klasami
klasy CGl assBox do klas z poprzedni ego przy zilustrowane na rysunku 9.6.
zostały
Klasa CGl assBox - dokładnie tak samo jak poprzednio - jest pochodną klasy CBox, ale usu nęliśmy z klasy CG l assBox funkcję ShowVo 1umeO, aby pokazać, że jej wersja z klasy bazowej nadal działa poprzez klasę pochodną. Zgodnie z hierarchią widoczną na powyższym rysunku klasa CConat i ner jest pośrednią klasą bazową klasy CG l assBox oraz bezpośrednią klasą bazową klas CBox i CCa n. Plik
nagłówkowy
GlassBox.h w naszym projekcie zawiera następującą treść :
II Plik naglówkowy GlassBox.h w pr ojek cie Cw9_II .
#pragma once #incl ude "Box.h"
II Do lącza klas ę CBox.
class CGl assSox : publi c CSox
II Klasa pochodna.
{
publ t e :
552
VislJal C++ 2005. Od podstaw
Bezpośrednia klasa bazowa klasy CCan
Bezpośrednia klas a ' bazowa klasy CBo x Pośred nla klasa b azowa klasy CGlassBox
c1assCContainer
Bardziej ogólne
~
/
cia ss CCan
Bezpośrednia klasa
bazowa klasy CGlassBox
c1assCBox
\
c1assCGlassBox
Bardziej wyspecjalizowane
Rysunek 9.6 II Funkcj a obliczająca pojemność obiektu klasy CGlassBox
II pozosta wiają ca / 5% miej sca na materiał wype łn iający.
virt ua l doub le Volume() const
( ret urn 0.85*m_Length*m_Width*m_Height :
II Konstruktor.
CGl assBox( doubl e l v . doub le
Wy.
doubl e hv) : CBox(lv .
Wy .
hv){ }
}:
Pliki
nagłówkowe
Container.h, Can.h, i Box.h
Plik źródłowy nowego projektu z funkcją mai n() w hierarchii ma następującą treść:
są takie
same jak w projekcie Cw9jO.
przystosowaną do używania
dodatkowej klasy
// Cw9_ ll. cpp // Używanie klas abstrakcyjnych z wielopoziomowym dziedziczeniem.
#incl ude "Box .h" // Dla klas CBox i CContainer.
#include "Can.h" // Dla klas CCan i Ctlo ntainer).
#i ncl ude "Gl assBox.h" I/ Dla klasy CGlassBox (i CBox oraz CContainer). #i nc l ude I/ Dla strumienia wejśc ia-wyjścia.
using st d: :cout : using st d: .endl : const double PI
=
3.14159265:
// Globalna defini cja zmienn ej PI.
int mai n() ( I/
Wskaźnik
do abstrakcyjn ej klasy bazowej zainicjalizowany adresem obiektu klasy CBox.
CContai ner* pC1 = new CBox(2 .0, 3.0. 4.0):
Rozdział 9.
• Dziellziczenie i funkcje wirtualne
553
CCa n myCan(6.5 . 3. O) ; II Definicja obiektu klasy CCan. CGlassBox myG lassBox(2.D. 3. D. 4.O): II Definicja obiektu klasy CGlassBox. pCl ->ShowVol ume(): delete pcl:
II Wysianie na ekran pojemności obiektu klasy CBox.
II Czyszczenie obszaru wolnej pamięci,
II zainicjalizowan ego adresem obiektu klasy CCan.
pCl = &myCan: II Ustaw wskaźnik na adres obiektu myCan.
pCl->ShowVo l ume( ) ; II Wyślij na wyjście pojemność obiektu klasy CCan.
pC l ~ &myGlas sBox: pCl->ShowVolume():
II Ustaw wskaźn ik na adr es obiektu myGla ssBox.
II Wyślij na wyj scie pojemność obiektu klasy CGlassBox.
cout « end l : return O;
Jak to dziala W programie tym mamy trzypoziomową hierarchię widoczną na rysunku 9.6, w której klasa CConta i ner jest klasą abstrakcyjną, ponieważ zawiera czysto wirtualną funkcję Vo l ume(). Funkcja ma i n( ) wywołuje funkcję ShowVo l ume ( ) trzykrotnie za każdym razem, używając tego samego wskaźnika do klasy bazowej , ale w każdym przypadku zawierającego adres obiektu innej klasy. Jako że funkcja ShowVo l ume( ) nie została zdefiniowana w żadnej z tutejszych klas pochodnych, za każdym razem wywoływana jest jej bazowa wersja . Oddzielne odgałęzienie od klasy bazowej CConat i ner definiuje klasę pochodną CCa n. Rezultat działania programu jest następujący:
obiekt u klasy CBox wynosi 24
wynosi 45.9458
u żyt k owa obiekt u klasy CBox wynosi 20,4
Po j emn o ś ć u żyt k owa P o j emn o ś ć Pojemno ść
Z danych wyjściowych wynika, z trzech wersji funkcj i Vo l ume ( ).
że
w
zależności
od typu obiektu
wywoływana jest
jedna
Zauważ. że prz ed przypisaniem nowego adresu do wskaźnika należy najpierw usunąć obiekt klasy CBox z obszaru wolnej pamięci. Jeżeli tego nie zrobimy, nie będziemy mogli lryczyś cić pamięci. ponieważ nie mielibyśmy żadnego zapisu adresu oryginalnego obiektu. Błąd ten można łatwo popełnić przy ponownym przyp isywaniu adresów wskaźnikom oraz używaniu obszaru wolnej pamięci.
Wirtualne destruktory Jednym z problemów, które mogą się pojawić podczas pracy z obiektami klas pochodnych przy użyciu wskaźników do klasy bazowej , jest wywołanie niewłaściwego destruktora. Mo żemy to zaobserwować, modyfikując poprzedni program.
554
Visual C++ 2005. Od pOlislaw
~ Wywoływanie niewłaściwego destruktora Aby dowiedzieć się, który destruktor jest wywoływany do niszczenia każdego obiektu, wystar czy do każdej klasy w poprzednim projekcie dodać destruktor wysyłający na ekran odpowiedni komunikat. Plik nagłówkowy Container.h wygląda zatem następująco: II Plik naglówkowy Container.h w projekcie Cw9 12.
#pragma once
#i nclude
using std: :cout;
using std : :endl:
class CContainer
II Bazowa klasa generyczna dla poszczególnych kontenerów.
(
public. II Destruktor.
-CConta iner()
{ cout « "Destruk tor klasy CConta iner
został wywoła ny ." «
end l : }
II Funkcja obliczająca pojemność - brak treści.
II Jest to funkcja czysto wirtualna, co zostało oznaczone przez '= O'.
virtua l double Volume() const = O;
II Funkcja
wyświetlająca pojemność.
vir tual void ShowVo lume () const (
cout
end l
« «
"Pojem ność
wynosi "
Vol ume() ;
«
}; Zawartość
II Plik
pliku
nagłówkowego
nagłówkowy
Can.h jest następująca:
Can.h w projekcie Cw9 12.
#pragma once
#incl ude "Conta i ner.h" exte rn cons t double PI;
II Dołącza definicję klasy CContainer.
class CCan: pub l ic CConta iner
(
publ ic: II Destruktor.
-CCan()
{ cout « "Destruktor klasy CCan II Funkcja
obliczająca pojemność
zos tał wywołany."
«
endl : }
pliszki.
virtual doub le Vol ume() const
( return 0.25*P I*m_Diamete r*m_Diameter*m_Height;
II Konstruktor.
CCan(double hv
~
4.0. double dv
protected:
doub le m_Height.
doub le m_Diameter.
};
=
2.0); m_Height( hv). m_Diameter(dv) {}
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
Plik na gł ówkowy Box.h zawiera: II Plik naglówko wy Box.h w pr oj ekcie Cw9 12.
#pragma once
#incl ude "Contai ner .h" #include
using std : :cout:
using std: :endl:
II Dotacza defin icję klasy CCo ntainer.
class CBox: publ i C CContai ner
II Klasa pochodna.
{
Dublic :
II Destrukt or.
-cso« ) { cout
«
"Dest ruktor kl asy CBox zost ał
wywo łany ."
«
endl : }
II Funkcja pokazująca pojemność obiektu.
vi rtual void ShowVo l ume() const {
cout
« «
II Funk cj a
endl "P o j e mn o ś ć u ż yt k owa
obliczająca pojemnoś ć
obiekt u klasy CBox wynosi "
Vol ume() :
«
obiektu klasy CBox.
virtual double Vol ume ( ) const { return m_l ength*m_Widt h*m_Height: II Konstruktor.
CBox(double l v = 1.0. double wv = 1.0. double hv ~ 1.0) :m_lengt h(lv) . m_Widt h(wv) . m_Height( hv ){} prote cted: double m_length: double m_Width : double m_Height : };
Plik
nagłówkowy Glas slł ox.h
II Plik
nagłó wkowy
zawiera:
GlassBox.h w p rojekcie Cw9 12.
#pragma once #i nclude "Box.h"
II Dołącza definicję klasy CBox .
class CG lassBox : publ ic CBox
II Klasa pochodna.
{
Dubli c:
II Destrukt or.
-CGlass Box () { cout « "Dest rukt or klasy CGlassBox
zo sta ł wy'tlO ł a ny ."
II Funkcja obliczająca pojemność obiektu klasy CGlassBox II pozostawiają ca 15% miej sca na materiał wype łn iający.
virt ua l doub le Volume() const { ret urn 0.B5*m_l engt h*m_Widt h*m_Height: }
«
endl : }
555
556
Visnal C++ 2005. Od podstaw II Konstruktor.
CGlassBox(double l v, double
WY,
double hv): CBox(lv.
Wy.
hv){}
}:
Zawartość
ostatniego pliku
źródłowego
-
Cw9_12 .cpp jest następująca:
II Cw9 _12.cpp
II Wywolywanie destruktorów w klasach pochodnych
II przy użyciu obiektów poprzez wskaźnik klasy bazowej.
#1 nc l ude "Box. h" #i nclude "Cen. h" #i nclude "Gl assBox. h" #i nc l ude using std ' :cout:
usi ng st d: end l :
II Dla II Dla II Dla II Dla
const double PI = 3. 14159265 ;
II Globalna definicja zmiennej PI.
klas CBox i Ctlontoiner.
klasy CCan (i CContainer).
klasy CGlassBox (oraz CBox i CContainer).
strumienia wejscia-wyjscta.
int main()
{
II
Wskaźnik
do abstrakcyjnej klasy bazowej zainicjalizowany adresem obiektu klasy CBox.
CContainer* pCl = new CBox(2.0, 3.0. 4.0); CCan myCa n(6.5. 3. O): CGl assBox myGl assBox(2 O, 3 O. 4.O);
II Definicja obiektu klasy CCan.
II Definicja obiektu klasy CGlassBox.
Cl->ShowVolume( );
cout « endl « "Delete CBox"
delete pc!;
pC l = new CG lassBox(4.0. 5.0, 6.0); II Dynamiczne utworzenie obiektu klasy CGlassBox
pC l->ShowVolume(); II ... i wysianie na wyjście jego pojemności...
cout « endl « "Delete CGlassBox" « end l:
delet e pC l; 11...orazjego usunięcie.
pCl = &myCa n; pC l ->ShowVolume();
II Ustaw wskaźnik na adres obiektu myCan.
II Wyślij na wyjście pojemność obiektu klasy CCan.
pCl ~ &myGlassBox, pCl ->ShowVol ume() :
II Ustaw wskaźnik na adres obiektu myGlassBox.
II Wyslij na wyjście pojemnosć obiektu klasy CGlassBox.
cout « endl ;
return O:
Jak to działa Poza dodaniem do każdej klasy destruktora wysyłającego na wyjście komunikat infonnujący o efekcie wykonanej czynności, wprowadzonych zostało kilka zmian w funkcji ma i n() . Poja wiły się dodatkowe instrukcje dynamicznie tworzące obiekt klasy CGl assBox, wysyłające na ekran jego pojemność oraz go usuwające. Jest także komunikat informujący o usunięciu dy namicznie utworzonego obiektu klasy CBox. Rezultat działania tego programu widoczny jest poniżej:
Rozdział 9.
• Dziedziczenie ilunkcie wirtualne
557
P oje mn o ś ć uzyt kowa obiekt u klasy CBox wynosi 24 Delet e CBox Dest ruktor klasy CCont ai ner zost ał wywo łany . Pojemność u ż y t k owa obiekt u klasy CBox w ynosi 102 Delet e CGlassBox Dest ruktor klasy CConta iner zo s ta ł wywoła ny.
wynosi 45.9458 oblektu kl asy CBox wynos i 20.4 Dest ruktor klasy CGlass Box z o s t ał wywo ł a ny. Dest rukt or klasy CBox zo stał wywo ł a ny . Dest ruktor klasy CCont ai ner zos tał wywo łany . Dest ruktor klasy CCan zo sta ł wywo ł a ny . Dest ruktor klasy CCont ainer zos t ał wywoła ny . Pojemn o ś ć
P o j emno ść użyt k owa
Z powyższych danych wynika, że w momencie usunięci a obiektu klasy CBox wskazywanego przez wskaźnik pCl wywoływany jest destruktor klasy bazowej CCont ai ner , ale nie ma infor macji na temat wywołania destruktora klasy CBox. Podobnie w momencie usunięcia dodanego przez nas obiektu klasy CG la ssBox wywołany zostaje destruktor klasy bazowej CCont ai ner , a nie destruktor klasy CG l assBox lub klasy CBox. W przypadku pozostałych obiektów poprawne wywołania destruktora mają miejsce, gdy najpierw wywołany jest konstruktor klasy pochod nej, a po nim następuje wywołanie konstruktora klasy bazowej. Dla pierwszego obiektu klasy CGl assBax utworzonego w deklaracj i wywołane zostały trzy destruktory: jako pierwszy wywo łany został destruktor klasy pochodnej, po nim destruktor bezpośredniej klasy nadrzędnej , a na końcu destruktor pośredniej klasy nadrzędnej. Wszystkie problemy związane są z obiektami tworzonymi w obszarze pamięci wolnej. W obu przypadkach wywoływany jest niewłaściwy konstruktor. Powodem tego jest to, że wiązanie do destruktorów jest tworzone statycznie w czasie kompilacji . W przypadku obiektów auto matycznych nie stanowi to problemu - kompilator wie, czym one s ą, i powoduje wywoła nie odpowiednich destruktorów. W przypadku obiektów tworzonych dynamicznie , do których dostęp uzyskuje się poprzez wskaźnik, rzecz wygląda inaczej. Jedyną informacją, którą dys ponuje kompilator w momencie wykonywania operacji usuwania (del et e), jest to, że wskaź nik jest typem wskaźnika do klasy bazowej. Kompilator nie zna typu obiektu rzeczywiście wskazywanego przez wskaźnik, ponieważ jest on określany w czasie działania programu. A zatem jedyne, co robi kompilator, to upewnienie się , że operacja usuwania zostanie usta wiona w celu wywołania destruktora klasy bazowej. W prawdziwym programie może to spo wodować wiele problemów zależnych od natury samych obiektów (na przykład fragmenty obiektów porozrzucane po obszarze wolnej pamięci ) . Rozwiązanie tego problemu jest proste. Musimy spraw i ć, aby wywołania były dokonywane dynamicznie - w czasie wykonywania programu. Możemy to zorganizować, stosuj ąc w swo ich klasach destruktory wirtualne. Jak już wspominałem przy okazji pierwszego omawia nia funkcji wirtualnych, wystarczy jako wirtualną zadeklarować tylko funkcję w klasie bazo wej, aby wszy stkie funkcje o takiej samej nazwie, li ści e parametrów i typie zwracanym we wszystkich klasach pochodnych były również wirtualne . Ta zasada ma zastosowanie zarówno do destruktorów, jak i zwykłych funkcji składowych . Do definicji destruktora w klasie CCan ta i ner zdefiniowanej w pliku nagłówkowym Conatiner.h należy dodać słowo kluczowe vi r tual. Definicja tej klasy powinna wyglądać następująco:
558
Visual C++ 2005. Od podstaw class CContai ner
II Bazowa klasa generycz na dla poszczegó lny ch kontenerów.
(
pub l i c: II Destruktor.
virt ual -CContal ner()
{cout« "Dest rukto r klasy CCont ai ner
zo sta ł wywo ł any . "
«endl: }
II Reszta klasy jak wcześn iej.
); Dzięki
tej zmianie destruktory we wszystkich klasach pochodnych automatycznie również wirtualne , mimo że nie określiliśmy ich jako takie w sposób jawny. Oczywiście, jeżeli chcemy, aby kod był jeszcze bardziej przejrzysty, możemy słowo kluczowe vi rt ual dodać do wszy stki ch destruktorów . staną s ię
Po dokonaniu
powyższych modyfikacji
rezultat
działania programu j est następujący;
Poj emno ść użyt k ow a ob iektu klasy CBox wynos i 24
De let e CBox
Destrukto r klasy CBox z o s ta ł wywo ła ny .
Destru ktor kl asy CCont ainer z o st ał wywo ła ny .
Po j em n o ś ć u ż yt k ow a ob iektu klasy CBox wynos i 102
Delet e CGl assBox
Dest rukt or kl asy CGlassBox zos ta ł wywo ła ny.
Dest ruktor klasy CBox zos t a ł wywoł a ny.
Dest ruktor klasy CContainer z os ta ł wywo ł a ny .
wynosi 45.9458
obiekt u kl asy CBox wynosi 20 .4
Dest rukt or klasy CGl assBox zost ał wyw ołan y.
Destruktor klasy CBox zost ał wy wo łany .
Dest rukt or klasy CContai ner z os t ał wywoł a ny.
Dest ruktor klasy CCan z o s t a ł wywo ł a n y .
Dest rukt or klasy CContainer zo s ta ł wywo ł a ny .
P o jemn o ś ć
Poj e m no ść u żyt k owa
Jak widać , wszystkie obiekty są teraz niszczone przez destruktory w odpowi edniej kolejnoś ci. Niszczenie obiektów dynamicznych powoduje wywołanie destruktorów w takiej samej kolej ności jak obiektów automatycznych w programie. W tym momencie powst aje pytanie: "Czy konstruktory można deklarować jako wirtualne?". brzmi : "nie" - tylko destruktory i inne funkc je składowe.
Odpowiedź
Dobrym nawykiem jest zawsze deklarowanie destruktora klasy bazowej jako wirtual nego - oczywiście, gdy korzysta s ię z mechanizmu dziedziczenia. Destruktory wirtualne klas działają nieco wolniej , ale w większości przypadków j est to całkowicie niezauważalne. Zastosowanie destruktorów wirtualnych daje gwaran cję , że obiekty zos taną zniszczon e we właściwy sposób i dzięki temu udaje się uniknąć załamań programu, które mogą nastą pić w innym przypadku.
Rozdzial9. - Dziedziczenie i funkcje wirtualne
559
Rzutowanie pomiędzy typami klasowymi
Wiemy już, w jaki sposób można zapisać adres obiektu klasy pochodnej w zmiennej typu klasy bazowej - np. zmienna typu CCont ai ner* może przechowywać adres obiektu klasy CBox. Czy można zatem, mając adres przechowywany we wskaźniku typu CContai ner*, rzutować go do typu CBox*? Odpowiedź brzmi: "tak" - do tego celu służy operator dynami c_cast. Poniżej znajduje się przykład jego zastosowania:
CConta i ner* pContai ner = new CGl assBox( 2.0. 3.0, 4.0);
CBox* pBox = dynami e_east ( pContainer) ;
CGlassBox* pGlassBox = dynamie east ( pContainer ) ;
Pierwsza instrukcja zapisuje adres obiektu klasy CGlassBox utworzonego na stercie we wskaź niku klasy bazowej typu CCont ai ner*. Druga instrukcja rzutuje wskaźnik pCont ai ner do góry w hierarchii klasowej do typu CBox*. Trzecia instrukcja rzutuje adres przechowywany we wskaźniku pContai ner do jego rzeczywistego typu - CGl assBox*. Operatora dynam ic_cast można używać nie tylko do wskaźników, ale i referencji. Różnica operatorami dynami c_cast i stat ic_cast jest taka, że ten pierwszy sprawdza po prawność rzutowania w czasie działania programu, a drugi nie. Jeżeli operacja wykonywana przez operator dynamic_cast jest niepoprawna, to zwracana jest wartość nul l. W przypadku użycia operatora stat ic_cast kompilator całkowicie polega na programiście, jeśli chodzi o poprawność wykonywanej operacji, a więc lepiej jest zawsze używać operatora dynamic_ cast do rzutowania w górę i w dół hierarchii oraz sprawdzać, czy nie zostaje zwrócona wartość nu 11 , jeżeli chcemy uniknąć nagłego zakończenia programu, które może nastąpić w wyniku użycia zerowego wskaźnika. pomiędzy
Klasy zagnieżdżone Definicję
jednej klasy można umieścić wewnątrz definicji innej klasy i nazywa się to zagnież klas. Klasa zagnieżdżona jest statyczną składową klasy ją zawierającej i - podob nie jak inne składowe - podlega działaniu specyfikatorów dostępu do składowych . Jeżeli zagnieżdżoną klasę umieścimy w prywatnej sekcji innej klasy, to dostęp do niej będzie można uzyskać wyłącznie z wnętrza klasy ją zawierającej . Jeżeli natomiast klasę zagnieżdżoną okre ślimy jako publiczną, to dostęp do niej można będzie uzyskać także spoza klasy ją zawie rającej, ale przy użyciu specyfikatora w postaci nazwy klasy zewnętrznej . dżaniem
Klasa zagnieżdżona ma dostęp do wszystkich statycznych składowych klasy nadrzędnej . Do wszystkich składowych egzemplarza dostęp można uzyskać poprzez obiekt typu klasy nad rzędnej, wskaźnik lub referencję do tego obiektu. Klasa nadrzędna ma dostęp tylko do publicz nych składowych klasy zagnieżdżonej . Ale w klasach zagnieżdżonych, które znajdują się w sekcji prywatnej klasy nadrzędnej, składowe często deklaruje się jako publiczne, aby umożli wić wolny dostęp do nich funkcjom klasy nadrzędnej.
560
ViSUiII C++ 2005. Od podstaw Zagnieżdżanie klas jest szczególnie przydatne w przypadkach, gdy chcemy zdefiniować typ, który będzie używany wyłącznie w obrębie innego typu, przy czym klasa zagnieżdżona może być zadeklarowana jako prywatna. Na poniższym listingu przedstawiono przykład zagnież dżania klas:
II Stos prze c howują cy obiekty klasy CBox.
class CStack {
pri vate: II Definicj a elementó w do p rzechowywania na stosie.
struct C!tem {
CBox* pBox; Cltem* pNext:
II II
Ws kaźnik Wskaźnik
do obiektu w tym węźle. do następn ego elementu stosu lub null.
II Konstruktor.
Cltem(CBox* pB . Cltem* pN); pBox(pB) . pNext(p N){ } };
Cltem* pTop;
II Wskaźnik do elementu II na samej górze.
znajdującego się
publlC; II Dodanie obie ktu do stosu.
voi d Push(CBox* pBox) {
pTop = new Cltem( pBox. pTop) ;
II Utworzeni e nowego obiektu i umieszczenie go II na samej górze.
II Wyrzucenie obiektu ze stosu.
CBox* Pop() {
i f (pTop == O) ret urn O: CBox* pBox = pTop ->pBox: Clt em* pTemp = pTop: pTop = pTop->pNext ; delete pTemp;
II Jeżeli s tos jest p usty, II zwra ca null. II Pobierz obiekt Box z elementu. II Zapisuje adres elementu znajdują cego s ię II na samej górze. II Ustawiani e następnego elem entu II na samej gó rze. II Usunięcie starego elementu z sam ej góry l/ze sterty.
retur n pBox: };
Klasa CStack definiuje stos do przechowywania obiektów klasy CBox. Aby być całkowicie pre cyzyjnym , to przechowuje on wskaźn iki do obiektów klasy CBox, a więc wskazywane obiekty nadal są zależne od kodu robiącego użytek z klasy CSta ck. Zagnieżdżona struktura Clte rn definiuje elementy przechowywane na stosie. Zdecydowałem się zdefiniować I te rn jako zagnieżdżoną strukturę, a nie klasę, gdyż składowe struktury 'są domyślnie publiczne. Mogliby śmy zdefiniować CI t em jako klasę, a następnie określić jej składowe jako publiczne, aby były dostępne dla funkcji klasy CSt ack. Stos został zaimplementowany jako zbiór obiektów struk tury CItem, gdzie każdy obiekt tej struktury przechowuje wskaźnik do obiektu klasy CBox wraz z adresem następnego obiektu Cltem położonego niżej na stosie. Funkcja Push( ) w klasie CSt ad przesuwa obiekt klasy CBox w stronę wierzchołka stosu, a funkcja Pop( ) usuwa obiekt ze stosu.
Rozdział 9.
• Dziedziczenie i funkcie wirtualne
561
Dodawanie obiektu do stosu polega na utworzeniu nowego obiektu struktury Cl t em, zawie rającego adres przechowywanego obiektu oraz adres poprzedniego elementu, który b ył na wierzchołku stosu - przy dodawaniu pierwszego elementu do stosu jest to wartość zerowa. Usuwanie obiektu ze stosu zwraca adres obiektu w elemencie pTop. Usunięty zostaje element z wierzchołka i jego miejsce zastępuje element, który znajdował się pod nim . Zoba czmy, jak to działa .
l1I!!mm Używanie zagnieżdżonych klas W programie tym zostały wykorzystane klasy CCont ai ner, CBox oraz CGl assBox z projektu 9_12. Tworzymy więc pusty projekt konsolowy WIN32 o nazwie Cw9_13 oraz dodajemy do niego pliki nagłówkowe zawierające defini cje wymienionych klas. Następnie dodajemy do projektu plik nagłówkowy Stack.h zawierający defin icję klasy CStack, którą mamy wyżej, oraz plik Cw9 j 3.cpp o następującej zawartości: II Cw9_13. cpp
II Używanie zagnieżdżonych klas do zdefiniowania s tosu.
#incl ude "Box.h" II Dla klas CBox i CContainer.
#incl ude "GlassBox.h" II Dla klasy CGlassBox (i CBox oraz CContainer).
#inc l ude "Stack.h" II Dla klasy reprezentują c ej stos z zagn ieżdżoną
II strukturą Item.
#include usi ng std : :cout ;
using st d: :endl :
II Dla strumienia
wejścia-wyjścia.
int mainO (
CBox* pBoxes[]
new CBox(2.0. 3.0. new CGlassBox(2.0. new CBox( 4.0. 5.0. new CGl assBox( 4.0.
=
4.0).
3.0. 4.0).
6.0).
5. 0. 6.0)
};
cout« " Pude łka w t ablicy m a j ą for Ci nt i = O ; iShowVolume() ;
na st ę pu j ą c e poj em nośc i : " ;
II Wysyłan ie na wyjście pojemności pudełka.
cout « endl « endl
« "Dodawanie pu deł e k do st osu.
« endl :
CStack* pSt ack = new CStack ; for (i nt i = O : iPush(pBoxes[i]) ;
II Utworzenie stosu .
cout « "Usuwanie p u d eł ek ze st osu prezent uje j e w odwróconej for (i nt i = O : iPop ()- >ShowVol ume ( ): cout « endl :
return O:
k ol ej n ości
:";
562
lisual C++ 2005. Od podstaw Rezultat działania tego programujest następujący : Pudełka
w tabl icy
ma J ą n a s tę puj ą c e po jemnośc i :
Pojemność użytkowa Pojemność użyt kowa Po jem no ś ć użytk owa Pojemność u ży t k owa
obiektu obiektu obie ktu obiektu
klasy klasy klasy klasy
CBox wynosi CBox wynosi CBox wynosi CBox wynosi
24
20 .4
120
102
Dod awa nie p ud e łek do stosu .. .
Usuwanie pu dełek ze stosu prezentuje je w odwrotnej P ojemno ść u ży tkowa obiekt u klasy CBox wynosi 102
P o j emn o ś ć u ży tkowa obiektu klasy CBox w ynosi 120
Pojemność użytkowa obiekt u klasy CBox wynosi 20 4
Po j emn o ś ć użytk owa obiektu klasy CBox wynos i 24
k olej nośc i :
Jak to działa do obiektów klasy CBox, dzięki czemu każdy element tablicy adres obiektu klasy CBox lub adres dowolnego typu pochodnego od klasy CBox. Tablica jest inicjalizowanaadresami czterech obiektów utworzonych na stercie:
Tworzymy
tablicę wskaźników
może przechowywać
CBox* pBoxes[]
=
(
new CBox(2.0. 3.0. new CGlassBoxC2 .0. new CBoxC4 .0. 5.0. new CGlassBox C4.0,
4.0) .
3.0 . 4.0).
6.0) ,
5.0 . 6.0)
}; Wśród obiektów znajdują się dwa obiekty klasy CBox i dwa obiekty klasy CGl assBox o takich samychwymiarach co obiekty klasy CBox.
Po liście pojemności wszystkich czterech obiektów tworzymy obiekt klasy CStack i umiesz czamy na stosie obiekty za pomocą pętli for: CStack* pSt ack = new CStack ; for Ci nt i = O ; iPush(pBoxes[ i]) ;
II Utworzenie stosu.
Każdy element tablicy pBoxes umieszczany jest na stosie poprzez przekazanie tablicy elementu jako argumentu funkcji Push() obiektu klasy CStack. W rezultacie pierwszy element tablicy znajduje się na samymdole stosu, a ostatni na samej górze.
Obiekty ze stosu usuwamy za pomocą innej
pętli
for :
for Ci nt i = O ; i PopC)->ShowVolume();
Funkcja Pop( )zwraca adres elementu znajdującego się na samej górze stosu. Za pomocą tego adresu wywoływana jest dla obiektu funkcja ShowVol ume O . Jako że ostatni element był na wierzchu stosu, pętla ustawia pojemności obiektów w odwróconej kolejności. Z danych na wyjściu widać, że klasa CSt ack rzeczywiście implementuje stos przy użyciu zagnieżdżonej struktury definiującej elementy do przechowywania na stosie.
Rozdział 9.
• Dziedziczenie i lunkcie wirtualne
563
Programowanie wC++/CLI
Wszystkie klasy w C++/CLI, włącznie z klasami zdefiniowanym i przez programi s tę , są do myślnie pochodne. Dzieje s i ę tak ze względu na fakt, że zarówno klasy warto śc i, jak i klasy referencyjne są poch odnymi klasy bazowej Syst em: :Object. Oznacza to, że klasy wartości i klasy referencyjne d ziedzi czą od klasy Sys t em: :Objeet, a zatem to, co je łączy, to wspólne możliwości odziedziczone po tej klasie. Jako że klasa ToSt ri ng() w klasie System: :Obj ect jest zdefiniowana jako funkcja wirtualna, można ją prze słoni ć we własnej klasie i wywoływać polimorficznie, kiedy jest to wymagane. To właśnie robili śmy za k ażdym razem, gdy wywo ływali śmy tę funkcj ę w klasach w poprzedn ich rozdziałach . Klasa bazowa wszystkich typów klas wartoś ci System : :Object jest również odpow iedzialn a za pakowanie (ang. boxing) i odpakowywanie (ang. unboxing) warto ści typów fundamental nych. Proce s ten sprawia, że warto śc i typów fundamentalnych mogą zachowyw a ć się jak obiekty, ale mogą być używane w działan iach liczbowych bez obciążenia spowodowanego byciem obiektem. Wartości typów fundamentalnych są przechowywane po prostu jako warto śc i dla celów standardowych działań i są konwertowane do obiekt ów wskazywanych przez uchwyt typu System: :Object ", kiedy muszą zachowywać się jak obiekty. W ten sposób mamy możliwość traktow ania warto ści fundamentalnych jako obiektów, kiedy tego potrzebujemy, pom ijając wady, które mają obiekty .
Dziedziczenie wC++ICLI klasy wartości mają zawsze klasę Syst em: :Object jako bazową, nie mo żna zdefi klasy wartości na bazie istniejącej klasy. Mówiąc inaczej, definiując klasę wartości, nie możemy określi ć jej klasy bazowej . Oznac za to, że polim orfizm w przypadku klas warto ś ci ograniczony jest do funkcji wirtualnych klasy System: :Obj ect . Poniższa tabela zawiera funkcje wirtualne dziedziczone przez klasy wartości po klasie Syst em: :Obj ect : Mimo
że
n iować
Funkcja
Opis
St ring ToSt ringC )
Zwraca ła ń cu chow ą r epre z en t a cję ob iektu . Implementacj a tej funkcji w klasie System : :Obj ect zwraca nazw ę klasy j ako łańcu ch . Zazwyczaj we własnych klasach funk cję tę dostosowuje się w taki sposób, aby z wr ac ała łań cuchow ą reprezentację w art o ści obiektu .
bool Equa lsCObject ob i)
Porównuje bie żący obiekt z obiektem obi i zwraca t rue, jeżeli są one równe, lub fa l se w przeciwnym przypadku. Równość w tej funkcji oznacza równ o ść referen cyjną to znaczy obiekty s ą tym samym obiektem. Najczę ściej funkcję tę modyfikuje się, aby zwracała t rue, gdy bieżący obiekt ma taką s a mą wartoś ć co argum ent inaczej m ówiąc , kiedy pola są równe .
int Get Ha shCodeC)
Zwr aca
A
A
liczbę całkow itą będącą kodem miesz ającym.
Kody mies zające klu cze do prze chowywania obiektów w kolekcji przechowującej (klucz, obiekt) pary. N a st ępnie obiekty w takiej kolekcji są odnajdowan e poprzez podanie klucza użytego podczas zapisywania obiektu. używane są jako
564
Visnal C++ 2005. Od podstaw Oczyw i śc ie ze względu na fakt, że System: :Object jest klasą bazową także dla klas referen cyjnych , funkcje te można przesłaniać również w tych klasach. Klasę referencyjn ą można utworzyć jako pochodną innej klasy referencyjnej w taki sam spo sób, jak definiowali śmy klasy pochodne w natywnym C++. Spróbujemy teraz dokonać ponow nej implementacji projektu Cw9_12 jako programu w C++/CLI i w ten sposób zobaczymy równi eż zagnieżdżanie klas w programach CLR. Możemy zacząć od klasy Cont ai ner : nagłówkowy
II Plik
Contain er.h w projekcie Cw9_14.
#pragma once
using namespace Syst em:
II Abstrakcyjna klasa bazowa dla poszczególny ch konten erów.
ref class Contain er abst ract (
publ ic:
II Funkcja ob liczająca pojemność - brak treści.
II Funkcja ta zos ta ła zdefiniowana jako abstrakcyjnafunkcja wirtualna,
II na co wskazuj e słowo kluczowe 'abstract'.
vi rt ua l double Vo l ume() abstract : II Funkcja
wyświe tlająca pojemność.
virtual void ShowVol ume() {
Console: :Wr it el ine(l "Pojemno ś ć wynos i (O}". Volume() ):
}
}:
Pierws ze, na co trzeba zwróci ć uwagę , to słowo kluczowe abstra ct po nazwie klasy . Jeżeli klasa w C++/CLI zawiera odpowiedn ik funkcji czysto wirtualnej w natywnym C++, to należy ją zdefiniowa ć jako abstrakcyjną (abstraet). Jednak jako abstrakcyjną można również okre ślić klasę niezawierającą żadnych funkcji abstrakcyjnych, co powoduje, że nie można tworzyć obiektów tej klasy. Słowo kluczowe abst ract pojawiło się także na końcu deklaracji funkcji składowej Volume( l , co oznacza, że funkcja ta zo stała zdefiniowana dla tej klasy. Na końcu deklaracji funkcji składowej Vo l ume() można także dodać zapis ~O , podobnie jak w natywnym C++, ale nie jest to tutaj wymagane. Zarówno funkcja Vol ume( l , jak i ShowVo l ume() są funkcjami wirtualnymi, a więc mogą być wywoływane polimorficznie dla obiektów typów klasowych pochodzących od klasy Cont ai ner. Definicja klasy Box przedstawia się II Plik
n agłówkowy
następująco:
Box.h w proj ekcie Cw9_J4.
#pragma once #include "Cont ainer .h"
II Dołącza definicję klasy Container.
ref class Box
II Klasa pochodna.
Conta iner
(
publ ic. II Funkcja pokozująca pojemnoś ć obiektu.
virtual void ShowVolume( ) overri de {
Console : :Writ el i ne( L" P oj emno ś ć Volume() ):
użyt kowa
ob iektu klasy Box wynosi {O}" .
Rozdział 9.
• Dziedziczenie i lunkcje wirtualne
565
II Funkcj a obliczająca pojemność obiektu klasy Box.
vi rtual doub le VolumeC ) override { return m_Length*m_Width*m_Height ; }
II Konstruk tor.
BoxC)
m_Lengt hCl .O ). m_W idt hCl .O). m_HeightC l Oli }
II Konstruktor.
BoxCdouble l v. doubl e wy . double hv)
: m_LengthCl v) . m_Width Cwv). m_HeightChv){ }
prot ect ed:
doubl e m_Length:
double m_Wi dt h:
double m_Height :
l: Klasa bazowa klasy referencyjnej jest zawsze publiczna i słowo kluczowe pub l i c jest sto sowane domyślnie . Można taką klasę bazową zdefiniować jako publiczną w sposób jawny, ale nie jest to konieczne . Klasa bazowa klasy referencyjnej nie może być inna niż publiczna. Jako że w przeciwieństwie do wersji klasy w natywnym C++, tutaj nie możemy podać wartości domyślnych parametrów, zdefiniowaliśmy konstruktor bezargumentowy inicjalizujący wszyst kie trzy pola wartością l . O. KJasa Box( ) definiuje funkcję Vol ume( ) jako zmodyfikowanąwer sję jej wersji odziedziczonej po klasie bazowej. Aby przesłonić funkcję klasy bazowej , należy zawsze używać słowa kluczowego overri de. Gdyby klasa Box nie implementowała funkcji Vol ume(), to byłaby klasą abstrakcyjną i konieczne byłoby określenie jej jako takiej, aby można było ją skompilować .
Poniżej
II Plik
znajduje się kod klasy GlassBox: nagłówkowy
GlassBox .h w projekcie Cw9_14.
#pragma once #incl ude .. Box.h"
II Dołqcza klasę Box.
ref cla ss Glass Box : Box
II Klasa p ochodna.
{
publ ic: II Funk cja obliczająca pojemność obiektu Glasslłox, II poz ostawiają ca 15% na materiał wypełniający.
vi rtual double Vol umeC) overr ide { ret urn O.85*m_Lengt h*m_Widt h*m_Height ; } II Konstruktor.
GlassBoxC double lv. double
WY.
double hv) : BoxC l v.
Wy.
hv){}
}:
Klasą bazowąjest klasa Box, która domyślnie jest publiczna. Reszta klasy niczym nie różni od poprzedniej wersj i. Poniższy
kod definiuje
klasę
St ack:
II Plik nagłówkowy Stack.h dla pr ojektu Cw9_ 14.
II Stos przechowujący obiekty dowolnej kłasy referen cyjnej.
#pragma once
się
566
Visual C++ 2005. Od podstaw ref clas s St ack
{
pri vat e:
II Definicja elementów do przechowywania na stosie.
ref struct It ern {
Obj ect A Obj : It .em" Next ;
II Uchwyt do obiektu w tym elemencie. II Uchwy t do elementu na stosie lub nullptr.
II Konstruktor.
It ern(Object A obj . It ern A next l : Obj (obj l, Next (nextl{}
}:
It ern Top: A
II Uchwy t do elementu, który znajduj e s ię na wierzc hu.
public: II Dodani e obiektu do stosu.
void Push(Object obj l A
(
Top = gcnew It ern(obj . Top):
II
Usun ięcie
II Utworze nie nowego elementu i umieszczenie go II na wierzchu.
obiektu ze stosu.
Object " Pop( l (
if( Top == nu l l ptr ) ret urn nu11 ot r :
II Jeżeli stos jest p usty,
Il zwraca nułlptr.
Objeet Aobj = Top ->Obj ; Top = Top->Next : ret urn obj;
II Pobi eranie obiektu z elementu.
II Wstawienie następn ego elementu na wierzch.
} }:
Pierwsza różnica, na którą należy zwrócić uwagę, to fakt, że parametry funkcji oraz pola są teraz uchwytami, ponieważ mamy do czynienia z obiektami klasy referencyjnej . Wewnętrzna struktura It em przechowuje uchwyt typu Coj ect ", dzięki czemu obiekty dowolnej klasy CLR mogą być przechowywane na stosie. Oznacza to, że można przechowywać zarówno obiekty klas wartości , jak i klas referencyjnych, co stanowi znaczne ulepszenie w stosunku do klasy eSt ack w natywnyrn C+t. Nie trzeba zaprzątać sobie głowy usuwaniem obiektów, kiedy zostaje wywołana funkcja Pop( l, ponieważ system usuwania nieużytków zrobi to za nas. Poniżej
znajduje
się
lista
ró żnic pomiędzy
• Tylko klasy referen cyjne
mogą być
klasami w natywnym C++ i w C++/CLI: typami klas pochodnych.
• Klasa bazowa klasy pochodnej typu referencyjnego jest zawsze publiczna. • Funkcja nieposiadająca żadnej definicji klasy referencyjnej jest funkcją abstrakcyjną i musi być zadeklarowana przy u życiu słowa kluczowego abst raet. lub w ięcej funkcji abstrakcyjnych musi zosta ć jawnie jako abstrakcyjna poprzez postawienie słowa kluczowego abst ract po jej nazwie.
• Klasa
zawierająca jedną
okre ślona
RozdzlaJ 9.• Dziedziczenie i funkcje wirtualne Klasa niezawierająca żadnych funkcji abstrakcyjnych może być abstrakcyjna. Nie można two rzyć egzemplarzy takiej klasy.
• •
przed
funkcj ą n adpisującą funkcję odziedziczoną po
561
określona jako
klasie bazowej należy postawić
słowo kluczowe over r-tde,
Jedyne, czego potrzebujemy do wypróbowania naszych klas, to projekt konsolowy CLR z definicją funkcji mai n() .
~ Używallie pochodnych klas referencyjnych Utwórz nowy projekt CLR o nazw ie Cw9 _14 i dodaj do niego klasy zdefiniowane powyżej . dodaj plik źródłowy Cw9_14.cpp o na stępującej treści :
Następnie
II Cw9_J4 .cpp : main p roject file. II Definiowanie stosu za p omocą zagnieżdżonych klas.
#i nc lude #incl ude #incl ude #include
"stdafx.h" "Box.h" "Gla ssBox.h" "St ack.h"
II Dla klas Box i Container. II Dla klasy GlassBox (i Box oraz Container). II Dla klasy reprezen tującej stos z zagnieżdżoną struktura item.
using namespace Syst em: int mainCarray Ą a r g s ) I ar ray< BoxĄ >Ą boxes = ( gcnew BoxC 2.0. 3.0. gcnew Glass BoxC2 .0. gcnew Box C4. 0. 5.0. gcnew GlassBoxC4 .0,
4.0). 3.0. 4.0) , 6.0) . 5.0. 6.0)
}:
Console: : Wr i te L i ne C L " P ude łka w t ablicy ma j ą n a st ę puj ą c e pojem no~ ci :"): for eachC Box box in boxes ) box ->ShowVo l umet ): II Wysyłanie na wyjście pojemności pudelka . A
Console: :WriteLineCL" \nDodawanie st ack = gcnew St ack: for eac h CBo x box in boxes) st ack->PushC box):
Sta ck
Ą
pude łe k
do st osu. . . n ) ;
II Utworzenie stosu.
Ą
Console : :Writ eLi neC L"Usuwanie p u de łek ze st osu prezent uje je w odwróconej Ob j ect item; while CCitem = st ack->PopC) ) != null ptr ) Ą
sa fe_cas t C item) - > S howVo lume C) ;
Console: :WriteLineCL" \nWst awi anie li czb ca łkow ityc h do sto su:"); t orr int i = 2 : i < ~1 2 : i += 2) (
Conso le : :Write CL"{0.5j" .i ); stack->PushC i) :
ko le j no~ c i : ") :
568
Visual C++ 2005. Od podstaw Console: :WriteLi neCL"\ n\ Rezultat u sunięcia l iczb c a ł k ow i tyc h ze st osu je st n as tę p u j ący: " ); whileC Citem = stack ->Pop()) != null ptr ) Console: :Writ eCL "{O, 5}" . item) : Console : :Writ eLi ne(): return O: Rezultat
działania
Pudeł k a
tego programu jest następujący:
w t abl icy
Pojemnoś ć u żytkow a P oj emno ś ć u żytkow a
Pojemność u żytkowa Poj emn o ś ć u ży t k owa
mają na stępują ce pojemno ś c i :
obiektu obiekt u obiektu obiekt u
klasy klasy klasy klasy
Sox wynosi Box wynosi Box wynosi Box wynos i
24 20.4 120 102
Dodawanie pudełek do stosu. . . Usuwani e pud ełek ze st osu prezentuje je w odwróconej Pojemn o ść u ży tk owa obiekt u klasy Box wynos i 102 Pojemność u żytkowa obie kt u klasy Box w ynosi 120 Pojemno ś ć u żyt kowa obiektu klasy Box w ynosi 20,4 Poj em no ść u żytkowa obiektu klasy Box wynos i 24 Wstawianie li czb ca ł k owity c h do st osu: 2 4 6 8 10 12 Rezultat u sunięcia liczb c a łkowi tyc h ze stosu j est 12 10 8 6 4 2
kolejno ści :
na stęp ują cy :
Jak to działa Najpierw
utworzyliśmy tablicę
uchwytów do łańcuchów:
arrayA boxes = ( gcnew BoxC2 .0. 3.0, gcnew Glas sBox(2.0. gcnew BoxC4 .0, 5.0, gcnew GlassBoxC4.0.
4.0) , 3.0, 4.0), 6.0). 5.0, 6.0)
}:
Jako że klasy Box i Gl assBox są klasami referencyjnymi, ich obiekty tworzymy na stercie CLR za pomocą operatora genew. Adresy tych obiektów inicjalizują elementy tablicy boxes. Następnie
tworzymy obiekt St aek i ustawiamy łańcuchy na stosie: A II Utworzenie stosu. St ack st ack = gcnew St ack : for each CBoxA box i n boxes) st ack ->PushCbox):
Parametr funkcji Push( ) jest typu Ooject", a więc funkcja przyjmuje jako argument dowolny typ klasowy. Pętla for eaeh ustaw ia po kolei wszystkie elementy tablicy boxes na stosie. Usuwanie elementów ze stosu odbywa się w pętli whi l e: Ooject " item:
whil eC (item = st ack->PopC)) 1= nu ll ptr) safe cast Ci tem)->ShowVo l ume C);
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
569
Warunek p ętli przechowuje wartość zwróconą przez funkcję Pop( ) wywołaną dla obiektu klasy stack w elemencie item i porównuje ją z null ptr. Instrukcja w ciele pętli whi l e wykonywana jest, dopóki itemnie zostanie ustawion y na nul l ptr. Wewnątrz pętli uchwyt przechowywany w itemjest rzutowany do typu Cont atne r" . Zmienna item jest typu Object i ponieważ w klasie Object nie zdefiniowano funkcji ShowVolume ( ), nie można jej wywoływać przy użyciu uchwytu tego typu. Aby wywołać tę funkcję polimorficznie, musimy użyć uchwytu typu klasy bazowej, w której funkcja ta jest zadeklarowana jako składowa wirtualna. Dzięki rzutowaniu uchwytu do typu Corrt at ner" możemy wywołać funkcję ShowVol umeO polimorficznie. Dzięki temu funkcja ta jest wybierana dla ostatecznego typu klasowego obiektu wskazywanego przez uchwyt . W tym przypadku ten sam efekt można było uzyskać, rzutując item do typu Box Użyli śmy tutaj rzutowania bezpiecznego (saf e_cast), ponieważ odbywa się ono w górę hierarchii, a w takich przypadkach najlepiej jest sprawdzać rzutowanie. Operator safe_cast sprawdza poprawność rzutowania i, jeżeli konwersja się nie powied zie, powoduje wyjątek typu Syst em: : Inval i dCastException. Mogliśmy także zastosować operator dynamic_cast, ale w programach CLR lepiej jest używać operatora sa fe _cast . A
A
•
Klasy interfejsowe Definicja klasy interfejsowej jest podobna do definicji klasy referencyjnej , ale jest to całkiem inny koncept. Interfejs to klasa określająca zestaw funkcji do zaimplementowania przez inne klasy w celu dostarczenia standardowego sposobu realizowania określonej funkcjonalności. Zarówno klasy wartości, jak i klasy referencyjne mogą implementować interfejsy. Interfejs nie definiuje żadnej ze swoich funkcji składowych - są one definiowane przez klasy implementujące interfejs . Spotkaliśmy się już z interfejsem System: :Comparab l e w kontekście funkcji generycznych, gdzie interfejs IComparab le określil iśmy jako ograniczenie. Interfejs ICompar abl e określa funkcję Compa reTo() służącą do porównywania obiektów. W związku z tym wszystkie klasy implementujące ten interfejs mają ten sam mechanizm porównywania obiektów. Interfejs implementowany przez klasę określa się w podobny sposób jak klasę bazową. Poniżej znajduje się przykładowa implementacja interfejsu System: : ICompar able przez klasę CBox z poprzedniego projektu :
ref class Box : Cont ai ner, IComparable
II Klasa pochodna.
publi c: II Funkcja
określona
prz ez interfej s IComparable.
vir tual int Compa reTo(Object A obj) (
i f CVolumeC) < safe_cast Cobj) ->Vo l ume( )) ret urn -l ; else if CVol umeC) > safe_castCobj )->Vol umeC)) ret urn l. else ret urn O: II Reszta klasy jak poprzednio...
}:
570
Visual C++ 2005. Od podstaw Po nazwie interfejsu znajduje się nazwa klasy bazowej Contai ner. Gdyby nie było klasy bazowej, to byłaby tu tylko sama nazwa interfejsu. Klasa referencyjna może mieć tylko jedną klasę bazow ą, ale może implementować dowolną liczbę interfejsów. Klasa musi definiować wszystkie funkcje określone przez wszystkie interfejsy, które implementuje. Interfejs l Compara b1e określa tylko jedną funkcję, ale może ich być dowolna liczba . Teraz klasa Box definiuje funkcj ę Compare To( ) z taką samą sygnaturą, jaką interfejs IComparabl e określa dla tej funkcji . Ze względu na fakt, że argument funkcji CompareTo() jest typu Dbject musimy dokonać rzutowania do typu Box przed uzyskaniem dostępu do składowych wskazywanego przez niego obiektu klasy Box. A
,
A
Definiowanie klas interlejsowych Klasę interfejsową można zdefin iować
za pomocą słowa kluczowego i nterface Cl ass lub i nt erface struct . Bez względu na to, którego z tych słów kluczowych użyjemy do definicji interfejsu, wszystkie jego składowe są zawsze publiczne i nie mogą być inne. Składow e interfejsu mogą być funkcjami, włącznie z operatorami, właściwościami , polami statycznymi oraz zdarzeniami - o nich wsz ystkich b ędziemy jeszcze mówić w tym rozdziale.Interfejs może zawierać także konstruktor statyczny oraz definicje zagnieżdżonych klas dowolnego typu. Pomimo takiej różnorodności możliwych składowych większość interfejsów jest względnie prosta. Warto zauważyć , że tworzenie interfejsu na bazie innego interfejsu zasadniczo odbywa się tak samo jak w przypadku tworzenia jednej klasy referencyjnej na bazie innej klasy referencyjnej . Na przykład:
int erface class IControl le r : ITeleVl son . IRecorder { II Pola interfejsu IContr ol/er...
}:
Interfejs IContro11er zawiera własne pola , a także dziedziczy je po interfejsach !Tel evi ste n i l Recorder . Klasa implementująca interfejs IContro ller musi definiować funkcje składowe interfejsów IController, l I el evi s to n oraz I Recor der. W projekcie Cw9_ 14 zamiast bazowej klasy Contai ner takiego interfejsu wyglądałaby następująco :
mogliśmy użyć
interfejsu. Definicja
II Plik naglówkowy IContainer.h w proj ekcie Cw9_15.
#p ragma once int er face class ICont ainer (
double Vo l ume (): void ShowVol ume( ):
II Funkcja II Funkcja
obliczająca pojemn ość.
wyświet lająca pojemność .
}:
W C++/CLI istnieje konwencja, według której nazwy interfejsów rozpoczynają się wielką literą I - dlatego nazwa naszego interfejsu to ICont ain er . Interfejs ten zawiera dwie składowe: funkcję Vol ume () oraz funkcję ShowVol ume( ), które są publiczne, ponieważ wszystkie składowe interfejsu są publiczne. Obie funkcje są abstrakcyjne, ponieważ interfejs nigdy nie zawiera definicji funkcji - można do znajdujących się w interfejsie funkcji dodać słowo kluczowe abstraet, ale nie ma takiej potrzeby . Funkcje obiektowe w definicji interfejsu mogą być określone jako wirtualne lub abstrakcyjne, ale nie jest to konieczne, gdyż i tak są one takie.
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
571
Każd a jeżel i
klasa implementując a interfej s rContainer musi implementować obie jego funk cje , nie chcemy, aby była klasą a bs trakcyj ną. Spójrzmy na d e finicję klasy Box:
II Plik naglówkowy Box.h w proj ekcie Cw9 15.
#pragma once #include "ICont ai ner .h"
II Dotacz a defi nicję interfejsu .
using namespace System: ref class Box : ICont ai ner pub1ic: II Funkcia
vi rt ual void ShowVol ume() Console : : Wr i te L i n e C L " PoJem ność Vo lume C) );
u ży tk owa
obiektu klasy CBox wynosi {O}" ,
II Funkc 'a oblicza ' ca o 'emnos ć obiektu klas Box.
virt ual doub le Vol umeC) II Konstruktor.
Box() : m_Length(l .O). m_Widt h(l .O). m_Height (l .O){} II Konstruktor.
BoxCdouble lv. doub le
Wy.
double hv) mLengthClv ). m_Wldt h(wv) . m_Height (hv ){}
protected: double m_Length: double m_Width: double m_He ight : }:
Nazwa inter fejsu znajduje si ę po dwukropku w pierwszym wiers zu defini cji klasy, tak jakby to była klasa bazowa. Oczywi ści e m ogłaby to być klasa bazowa. Wtedy nazwa interfejsu występow ałaby po nazw ie klasy bazo wej i była od niej oddziel ona przecinkiem. Kla sa może implementować wiele interfejsów - nazwy poszczególnych interfejsów oddzielane są przecinkami. Klasa Box musi implementować obie funkcje składowe klasy interfejsowej If.ontamer. W przeciwnym przypadku będzie klasą abstrakcyjną i musi zostać jako taka zadeklarowana. Do definicji funkcj i w klasie Box nie dołączono słowa kluczowego overr i de, pon ieważ istniejące funkcj e nie są w niej przesłaniane , a implementowane po raz pierwszy . Klasa Gl ass Box jest pochodną klasy Box, a zatem dziedziczy i mp le m e n tacj ę interfejsu rCont a-j ner. W klasie Gl assBox nie są wymagane żadne zmiany do wprowadzenia klasy interfejsowej rCont ainer .
572
Visual C++ 2005. Od podstaw Interfejs rCont ai ner
pełn i tę samą rolę
co klasa bazowa w polimorfizmie. W uchwycie typu adres obiektu dowolnego typu klasowego implementują cego interfejs. A zatem uchwytu typu rCont ai ner można użyć do wskazywania obiektów typu Box lub Gl assBox oraz wymuszania zachowania polimorficznego podczas wywoływania funkcji, które są składowym i klasy interfejsowej . Wypróbujmy to.
rContai ner
mo żna przechowywać
~ Implementacja klasy interfeisuwej Utwórz projekt konsolowy CLR o nazwie Cw9 _15 i dodaj do niego pliki nagłówkowe IContainer .h oraz Box.h o poniższej tre ści . Następnie dodaj także kopie plików Stack.h oraz GlassBox.h z projektu Cw9_14 . Na koniec zmodyfikuj zawartość pliku źródłowego Cw9_15 w następujący sposób : II Cw9_1 5.cpp: main project fi le.
#i nclude #include #i nclude #incl ude
"stdafx.h" "Box.h" "GlassBox.h" "St ack.h"
II Dla klas Box i /Conta iner. II Dla klasy GlassBox (i Box oraz / Container). II Dla klasy reprezentują c ej stos z zagn ieżdżoną II s t ru k tu rą /tem.
using name space Syst em; int main(arr ay Aargs) {
arrayA cont ainers
=
gcnew Box(2.0, 3,0. gcnew Glass Box(2.0. gcnew Box(4.0. 5.0. gcnew GlassBox(4.0.
(
4.0). 3.0. 4.0). 6.0) , 5.0. 6.0)
};
Console: : W r i t e L i n e ( L " Pu d e ł k a w t abl icy maj ą na st ę puj ą ce poj emno ś ci :" ) ; for each(IConta inerA cont ai ner in cont ainers) conta t ner ->ShowVol ume( ); II Wysyłani e na wyjście pojemnośc i pudełka. Console : :WriteL ine(L"\ nDodawanie
pud eł ek
do st osu.. .");
Stec k" stack = gcnew Stack; II Utworzenie stosu. for each (ICont ai ner A conta iner in cont ai ners ) sta ck· >Push(container) ;
Console : :Writ eLine( L" Usuwanie pud eł e k ze st osu prezentuje je w odwróconej Object " item; whil e«(it em = st ack->Pop()) != null ptr l safe_cast(item) ->ShowVol ume( ); Console : :Writ eLine(); ret urn O;
k ol ejn o ś c i
:" );
Rozdział 9.•
Rezultat
działania
P ude ł k a
Dziedziczenie i funkcje wirtualne
513
tego programu jest następujący:
w t ablicy
Pojemność u żytkowa
Pojemność użyt ko wa Pojemn ość u żyt k owa
Poj emn o ś ć u ży t k owa
ma ją nastę puj ące pojem no ś c i:
obiekt u obiektu obiekt u obie kt u
klasy klasy kla sy klasy
CBox wynosi CBox wynosi CBox wynosi CBox wynos l
24 20 .4 120 102
Dodawanie pu deł ek do st osu.. Usuwani e pud e ł ek ze st osu prezent uje je w odwróconej Poj emn o ść u żytk owa obiektu klasy CBox wynosi 102 Pojem n ość u żytk owa obiekt u klasy CBox w ynosi 120 Po jemno ść uży t kow a obiekt u klasy CBox wynosi 20.4 Poj emn o ś ć użyt k owa obiektu klasy CBox wynosi 24
kolejno ści :
Jak to działa Utworzyliśmy tablicę
elementów typu rContai ner i zainicjalizowaliśmy jej elementy adresami obiektów klas Box i GlassBox: A
arrayA contai ners = { gcnew Box(2.0. 3.0. gcnew Gl assBox(2.0. gcnew Box(4, O. 5.0. gcnew GlassBox(4.0,
4.0) , 3.0. 4.0) , 6.0) , 5.0. 6.0)
}:
Klasy Box i Gl assBox implementują interfejs rContain er, dzięki czemu adresy obiektów tych typów możemy przechowywać w zmiennych będących uchwytami do rContaine r . Zaletą takiego podejścia jest możliwość polimorficznego wywoływania funkcji składowych interfejsu rCont ai ner . Listę pojemności
obiektów klas Box i GlassBox tworzymy w pętli f or each: for each(ICont ai ner A container in containers) II Wysyłan ie na wyjście pojemnoś ci pu del/w. cont ai ner- >ShowVol ume():
Ciało pętli
pokazuje polimorfizm w akcji. Wywoływana jest w nim funkcja ShowVo l ume() dla obiektu wskazywanego przez conta i ner , co widać w danych wyjściowych.
określon ego
Elementy tablicy conta i ner s ustawiamy na stosie w dokładnie taki sam sposób jak w poprzednim przykładzie. Usuwanie elementów ze stosu również odbywa się podobnie jak poprzednio:
Object item: whil e(i t em = stack- >Pop() ) 1= nu llpt r) safe_cast (it em) ->ShowVo lume() : A
C i ało pętl i
pokazuje, że za pomocą operatora saf e_cast można rzutować uchwyt do typu interfejsowego w dokładnie taki sam sposób jak w przypadku typu klasy referencyjn ej. Następ nie możemy użyć tego uchwytu do polimorficznego wywołania funkcji ShowVo l tmeO . Klasy interfejsowe są nie tylko wygodnym sposobem definiowania zestawów funkcji represtandardowe interfejsy klas, ale także potężnym mechanizmem pozwalającym na zastosowanie polimorfizmu w programach.
zentujących
574
Yisual C++ 2005. Od podstaw
Klasy i asemblacje Program napisany w języku C++/CLI zawsze rezyduje w jednej lub większej liczbie asemblacji , a więc klasy w C++/CLI rezydują w asemblacjach. Wszystkie klasy definiowane do tej pory zawierały się w prostej pojedynczej asemblacji w postaci pliku wykonywalnego, ale można także tworzyć asemblacje zawierające własne klasy biblioteczne. W języku C++/CLI dla klas dostępne są specyfikatory widoczności, które określają, czy dana klasa jest dostępna na zewnątrz asemblacji , w której się znajduje, a która nosi nazwę asembla cji nadrzędn ej . Poza znanymi z natywnego C++ specyfikatorami dostępu do składowych publ te, pri vate oraz protec ted, w C++/CLI dostępne są jeszcze inne specyfikatory określające miejsce, z którego można uzyskać dostęp do składowych w różnych asemblacjach.
Specyfikatory widoczności klas i interfejsów klasy, interfejsu lub wyliczenia można określić jako pri vat e lub publ i c. Klasa publiczna jest widoczna i dostępna na zewnątrz asemblacji, w której się znajduje. Klasy prywatne natomiast dostępne są tylko wewnątrz nadrzędnej asemblacji . Aby określić klasę jako publiczną, należy posłużyć się słowem kluczowym publ i c: Widoczność niezagnieżdżonej
pub1 ie interfaee elass IContai ner { II Kod interfejsu...
};
Kontener ICont ai ner w powyższym przykładzie jest dostępny w asemblacji zewnętrznej, gdyż został zdefiniowany jako publiczny. Pominięcie słowa kluczowego pub l i c spowodowałoby, że interfejs ten byłby domyślnie prywatny i dostępny wyłącznie wewnątrz swojej nadrzędnej asemblacji. Klasę, wyliczenie i interfejs można jawnie zdefiniować jako prywatne, ale nie jest to konieczne.
Specylikatory dostęplI do składowych klas i interfejsów W C++/CLI dostępne są trzy dodatkowe specyfikatory dostępu do składowych klasy; interna l , publ i c prot ect ed i pri vat e protected. Efekt ich działani a został objaśniony w komentarzach do poniższej definicji klasy:
publ t e ref e1ass MyC1ass
II Klasa widoczna poza
asemblacją .
{
pub1t e : dla klas
wewnątrz
i na zewnątrz asembla cji
n adrz ędn ej.
II Składowe dostępne są dla klas
wewnątrz
i na zewnątrz asemblacji
n adrzędnej.
II Skladowe
do stępne są
internal ; publi C proteeted; II Składo we dostępne są dla typów po chodnych klasy MyClass p oza Il oraz dla klas wewnątrz asembla cji nadrzędnej.
asemblacją nadrz ędną
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
575
private prat ect ed: II Składowe dostępne dla typów p ochodnych klasy MyClass
wewną trz
asemblacj i nadrz ędn ej.
}: Oczywiście ,
aby specyfikatory do stępu do składowych umożliwiały dostęp spoza asemblacji klasa musi być publiczna. W przypadku słów kluczowych składających się z dwóch członów, takich jak pri vat e protected, mniej restrykcyjne słowo odnosi się do wnętrza asemblacji, a bardziej restrykcyjne do tego, co jest na zewnątrz. Kolejnoś ć członów można zmieniać . W związku z tym słowo kluczowe protecte d pri vate ma takie samo znaczenie co pri vat e nadrzędnej,
prote ct ed. tych słów klu czowych , trzeba mieć program zawierający więcej niż jedną Przerobimy więc projekt Cw9_15 do postaci asembla cji biblioteki klas oraz utworzymy asemblację programu korzystającego z tej biblioteki .
Aby móc
użyć
asemblację.
~. Tworzenie biblioteki klas W celu utworzenia biblioteki klas utworzymy najpierw za pomocą szablonu Class Library projekt CLR o nazwie Cw9_16lib. Po utworzeniu projekt zawiera plik nagłówkowy Cw9_ 16lib.h o następującej treści :
#pragma ance using namespace System: namespace Cw9_16l i b { publi c ref class Classl { II Tutaj pow inny znaleźć s ię metody tej klasy . }:
Biblioteka klas ma swoj ą własną przestrzeń nam i w naszym przypadku jest to domy ślnie Cwg_161i b. Jeżeli chcemy, to można tę nazwę zmienić na coś bardziej odpowiedniego. Nazwy klas w bibliotece opatrzone są kwalifikatorem w postaci nazwy przestrzeni nazw, a zatem w każdym źródłowym pliku ze w nętrznym korzystającym z którejkolwiek klasy z biblioteki musi znajd ować s i ę dyrektywa using dla nazwy tej przestrzeni nazw. Definicje klas mających znajdować się w bibliotece wstawiane są pomiędzy nawiasami klamrowymi w przestrzeni nazw namespace. W przestrzeni nazw została już domyślnie utworzona klasa referencyjna, ale zamienimy ją własnymi klasami . Warto zwróci ć uwagę , że klasa C1assl jest publiczna. Wszystkie klasy, które mają być widoczne w innych asemblacjach, muszą być zdefiniowane jako pub l i c. W pliku Cw9_16lib.h dokonujemy II Cw9 16lib.h
#pragma ance using namespace Syst em:
następujących
modyfikacj i:
576
Visnal C++ 2005. Od podstaw names pace Cw9_161ib II Plik n agłó wko wy IContainer.h z Cw9_16.
publ iC int erface cla ss IContainer {
virtua l doub le Volume () : virtual void ShowVol ume() :
II Fun kcj a obliczająca pojemność. II Fun kcj a wysyłająca pojemność na
wyjście .
·l : II Plik nagló wko wy Box. h z Cw9_ 16.
public ref clas s Sox : IContai ner {
pub l ic: II Funkcja
pokazująca pojemność
obiektu.
virtual void ShowVo l ume() {
Console : :Writ eLine (L " Po jemność Volume() ); II Funkcja
obliczająca pojemność
uży tkowa
obiekt u klasy CBox wynosi {O)" .
obiektu klasy Box.
virtua l double Vo lume( ) ( return m_Length*m_Width*m_Height; II Konstruktor.
Sox() : m_Length(l .O) . m_Wi dth (l .O) . m_Height (l .O){l II Konstruktor.
Box(double Iv. doub le
Wy.
doub le hv) m_Length(lv ) . m_Wi dth(wv) . m_Height(hv){l
pub l ic protected : double m_Length : doub le m_Widt h: double m_Height :
l: II Plik naglówkowy Stack.h z Cw9_ 16.
publiC ref class Stack {
privat e : II Definiowanie elementów do ustawie nia na stos ie.
ref struct It em {
Object Obj: l t em" Next ; A
II Uchwyt do obiektu w tym elemencie. II Uchwyt do następn ego ele mentu na stosie lub nullptr.
II Kons truktor.
Item (Object obj. Ite m next ): Obj( obj ) . Next (next ){ l A
A
l: It em Top: A
II Uchwy t do elementu
publ ic: II Dodawanie obiektu do stosu.
void Push(Object obj) A
znajdującego się
na wierzc hu.
Rozdział 9.
Top = gcnew Item(obj . Top) : II
Usunię cie
• Dziedziczenie i funkcje wirtualne
577
II Utworzenie nowego elem entu i wstawienie go na wierz ch.
obiektu ze stosu .
Obj eet A Pop() {
i f (Top == nul l pt r ) return nu ll pt r :
II Jeż eli stos jest p usty, II zwróć nullptr.
Objeet Aobj = Top->Obj ; Top = Top- >Next : ret urn obj;
II Pobierz obiekt z elementu. II Ustaw następny element na wierz chu.
} }: }
W bibliotec e znajdują się teraz klasa interfejsowa ICont ai ner, klasa Box oraz klasa St ack. Zmiany dokonane w stosunku do ich poprzednich wersji zostały wyróżnione na szarym tle. Każda z tych klas jest teraz publiczna, dzięki czemu s ą one dostępne z asemblacji zewnętrz nych . Pola klasy Box są publiczne chron ione (pub l i c prot ect ed), co oznacza, ż e w klasie pochodnej są one dziedziczone jako pola chronione, ale są publiczne dla klas znajdujących się w asemblacji nadrzędnej . Do pól tych nie odnosimy się jednak z innych klas wewnątrz asemblacji nadrzędnej, a więc mogliśmy pozostawić je w tym przypadku jako chronione. Jeżeli
projekt skompilował się poprawnie , to asemblacja zawierająca bibliotekę klas znajduje w pliku o nazwie Cw9_16lib.dll. Jest on umieszczony w folderze debug w katalogu projektu , jeżeli skompilowana została wersja testowa, lub w folderze releas e w katalogu projektu, jeżeli skompilowana została wersja ostateczna. Rozszerzenie .dl! oznacza, że jest to biblioteka dołączana dynamicznie (ang. dynamie link library) , czyli DLL. Potrzebujemy teraz jeszcze jednego projektu , który korzystałby z naszej biblioteki . si ę
~ Uzywanie IJilIlioleki klas Dodaj nowy projekt konsolowy CLR o nazwie Cw9 j 6 w taki sam sposób jak zawsze. Następ nie do pliku źródłowego Cw9 _16. epp wprowadź następujący kod :
#i ncl ude "stdafx.h" #i ncl ude "GlassBox .h" #usi ng usi ng names ace System ; usi ng namespace Cw9 161i b; i nt main (array Aargs) (
arr ayAcont ai ners
= {
gcnew Box(2.0. 3.0. 4.0) . gcnew Gl assBox(2.0. 3.0. 4.0). gcnew Box(4.0. 5.0. 6. 0).
578
Visual C++ 2005. Od podstaw gcnew Gl assBox(4.0. 5.0. 6.0) };
Console: : WriteLine(L"Pud ełk a w t ablicy ma j ą na st ępując e p ojemn o śc i: ") : for each(IContalner A contai ner in cont ai ners) II Wysyłanie na wyjście p ojemności pudełka.
cont ai ner->ShowVol ume ( ): Conso le: :Writ eLi ne(L"\ nDodawanie
pu deł ek
do st osu. . . "):
Stac k" stack = gcnew St ack; II UM orzenie stosu. for each (IContainer A container i n conta i ners ) st ack->Push(cont ainer):
Console : :WriteL i ne( L"PUsuwanie pud e ł e k ze st osu prezentuje je w odwróconej Object " item: wh ile «item - st ack->Pop( )) 1= nullptrl safe_cast (item)->ShowVolume() :
k o l ej n oś ci
:") :
Console:: Writ eLl ne() ; ret urn O, Musimy jeszcze dodać do projektu plik nagłówkowy GlassBox.h, którego kod jest identyczny jak w projekcie Cw9_15. Można ten plik skopiować do katalogu tego projektu i dodać go, klikając prawym przyciskiem myszy w zakładce Solution Explorer Header Fi/es, a następnie wybierając z menu kontekstowego Add/Existing Item. Oczywiście klasa Gl assBox jest pochodną klasy Box, a więc kompilator musi wiedzieć, gdzie można znaleźć definicję tej klasy. W tym przypadku znajduje się ona w bibliotece, którą stworzyliśmy w poprzednim projekcie. W związku z tym do pliku nagłówkowego GlassBox.h po dyrektywie #pragma once dodajemy poniższą dyrektywę:
#usi ng Nazwa klasy Box jest zdefiniowana w przestrzeni nazw Cwg_161 i b, a zatem musimy jeszcze dodać instrukcję us i ng dla tej przestrzeni nazw po dyrektywie #us ing:
usi ng namespace Cw9 16lib ; Aby kompilator mógł odnaleźć naszą bibliotekę, należy skopiować plik Cw9_16lib.dll z katalogu debug rozwiązania Cw9 j 6lib do folderu projektu Cw9_16. Można także podać pełną ścieżkę dostępu do asemblacji za pomocą dyrektywy #usi ng, ale najczęściej wszystkie biblioteki klas używane przez program umieszcza się w tym samym folderze, w którym znajduje się plik wykonywalny programu. Łatwo się poplątać w tych wszystkich nazwach folderów . Plik Cw9_16lib.dll znajduje się w folderze debug rozwiązania Cw9_16Iib, a nie w folderze debug projektu Cw9_16lib. Plik biblioteczny kopiujemy do folderu projektu Cw9_16. Upewnij się, że plik .dll znajduje się we właściwym folderze, w przeciwnym przypadku biblioteka nie zostanie odnaleziona. Jako tywę
że
klasy w
zewnętrznej
asemblacji
są
we
własnej
przestrzeni nazw,
dodaliśmy
dyrek-
usi ng dla nazwy przestrzeni nazw Cwg_161 i b. Gdybyśmy nie użyli tej dyrektywy, to musielibyśmy używać nazw IContainer, Box oraz Stack z kwalifikatorem w postaci nazwy przestrzeni nazw . Zamiast na przykład pisać Box, musielibyśmy pisać Cwg_161 i b; ; Box.
Rozdział 9.
• Dziedziczenie i funkcje wirtualne
579
Reszta kodu jest identyczna jak funkcji ma i nO w projekcie Cw9_ I5 . Nie są potrzebne żadne zmiany , ponieważ używamy klas z zewnętrznych asemblacji . Rezultat działania tego programu jest taki sam jak programu z projektu Cw9_15 .
Deliniowanie nowych funkcji Wiemy j uż, w jaki sposób używa s ię słowa kluczowego overr i de do prze słaniania funkcji klasy bazowej. Funkcje w klasie pochodnej można również zdefiniować jako nowe (new). Taka funkcja przesłania funkcję klasy bazowej mającą taką s a mą s ygnatu rę i nie zachowuje się polimorficznie. Poniżej znajduje się defmicja funkcji Vo l ume( ) jako nowej w klasie NewBox, która jest pochodnąklasy Box:
ref class NewBox
Box
II Klasa p ochodna .
o b liczająca pojemn o ś ć
obiektu Newlł ox.
{
publ t e: II Nowa f unkcja
vi rt ual double Volume() new { return 0.5*m_Lengt h*m_Widt h*m_Height ; } II Ko nstruktor.
NewBox(double l v. doub le
Wy.
double hv): Box(lv .
Wy.
hv ){}
};
Ta wersja funkcji
przesłania wersję
wywołamy funkcję
Vo l ume() za
funkcji Vo l ume() zdefiniowaną w klasie Box. Jeżeli zatem uchwytu typu NewBox to zostanie wywołana jej
pomocą
A
,
nowa wersja.
NewBox newBox ~ gcnew NewBox(2.0. 3.0. 4.0); Console: :Wr iteLi ne(newBox->Vol ume () ): II Wynik A
Rezultat wynosi 12, ponieważ funkcja Vol umeO o d z i e dz i c zył a klasa NextBox po klasie Box.
wynos i 12.
przesłon iła
jej
polimorficzną wersję , którą
Nowa funkcja Vo l ume( ) nie jest funkcją polimorficzną, a więc przy wywołaniach polimorficznych za pomocą uchwytu do typu klasy bazowej nowa wersja nie jest wywoływana. Na przykład:
Box newBox = gcnew NewBox(2.0. 3.0,4.0): Consol e: :WriteLi ne(newBox->Vol ume() ): A
II Wynik wy nosi 24.
Jedyna polimorficzna funkcja Vol ume() w klasie NewBox to funkcja odziedziczona po klasie Box i to właśnie ona zo st ał a wywołana w tym przypadku.
Delegaty izdarzenia Zdarzenie jest składową klasy , która umożliwia obiektowi zasygnalizowanie wystąpienia określonego zdarzenia. Proces sygnalizowania zdarzenia obejmuje deJegaty. Kliknięcie przyciskiem myszy jest przykładem typowego zdarzenia, a obiekt, od którego pochodzi to zdarzenie, zasygnalizowałby je poprzez wywołanie jednej lub większej liczby funkcji odpowiedzialnych za jego obsługę . Najpierw przyjrzymy się delegatom, a następnie przejdziemy do zdarzeń.
580
Visual C++ 2005. Od podstaw Koncepcja delegatu jest bardzo prosta - jest to obiekt zawierający jeden lub większą liczbę do funkcji o danej liśc ie parametrów i danych typach zwracanych . A zatem delegat w C++/CLI spełnia podobne zadania jak wskaźnik funkcji w natywnym C++. Mimo że sama koncep cja delegatów jest prosta , to szczegóły dotyczące ich tworzenia i używan ia mogą być nieco zaw i łe, a więc czas się skoncentrować . wskaźników
Deklarowanie delegatów Deklaracja delegatu wygląda jak prototyp funkcji poprzedzony słowem kluczo wym de l egat e, ale w rzeczywistości definiuje ona dwie rzeczy: nazwę typu referencyjnego dla obiektu delegatu oraz list ę parametrów i typ zwracany funkcj i, które mogą być skojarzone z tym deleg atem. Typ referencyjn y delegatu jest pochodną klasy Syst em: :Del egat e, a więc zawsze dziedz iczy składowe tej klasy. Deklaracja delegatu wygląda jak prototyp funkcji poprzedzony słowem kluczowym del egate, ale w rzeczywistoś ci definiuje typ referencyjny delegatu oraz sygnaturę funkcji, które mogą być z tym delegatem skojarzone. Poniżej znajduje s ię przykładowa deklaracja delegatu: publ ic del egate voi d Ha ndler(int val ue):
II Deklaracj a delegatu .
Powyższy kod definiuje typ referencyjny delegatu Handler , który jest pochodną klasy Sys tern : :Del egat e. Obiekt typu Handler może zawierać wskaźniki do jednej lub większej liczby funkcji mających jeden parametr typu i nt oraz typ zwracany voi d. Funkcje wskazywane przez delegat mogą być funkcjami obiektowymi lub funk cjami statycznymi.
Tworzenie delegatów Mając już
zdefiniowany typ delegatu , możemy tworzy ć obiekty delegatu tego typu. W przypadku delegatów mamy do wyboru dwa konstruktory: konstruktor przyjmujący jeden argument i konstruktor przyjmujący dwa argumenty. Argument konstruktora delegatu przyjmującego tylko jeden argument mu si być statyc zną funkcją skład ową klasy lub funkcją globalną, której typ zwracany i lista parametrów zo s tały określone w deklaracji delegatu. Przypu śćmy, że zdefiniowaliśmy klasę o nazwie Handl erCl ass : publ i c ref cl ass HandlerCl ass { publ ic: stat ic void Funl(i nt m) { Console : :Wri t eLi ne(L"Funkcj a Functionl
z
wart oś ci ą
{O)" . m):
wywala na z
wa rt ośc ią
{O)" . m) :
voi d Fun3(i nt m) { Consol e: :Wri t eLi ne(L"Funkcj a Function3 zastal a wywal ana z
wa rt o ś ci ą
{O )" .
war to ś c i ą
{Ol " ,
stat ic void Fun2(i nt m) { Console : :Wr iteLi ne(L"Funkcja Funct i on2
z o s t a ła wywoła n a
zo s ta ła
m-valu e) : }
voi d Fun4(i nt m) { Console : :WriteLine(L"Funkcj a Funct i on4 m-value ) : }
zo st a ł a wywoł ana
z
l
Rozdzial9. • Dziedziczenie i funkcje wirtualne
581
HandlerClassC) :val ueCU{} Ha ndle rClassCi nt m) .valu eu n{} protect ed: int val ue: }: Powyższa klasa ma cztery funkcje z parametrem typu i nt oraz typem zwracanym voi d. Dwie z nich to funkcje obiektowe, a drugi e dwie to funkcje statyczne. W klasie znajdują się także dwa konstruktory, z których jeden jest bezargumentowy. Klasa ta nie robi nic z wyjątkiem wy świetlania informacji, która funkcja została wywołana , i w przypadku funkcji obiektowych - informacji na temat obiektu.
Delegat Handl er
możemy utworzyć
Handler handler A
=
za pomocą następującego kodu :
gcnew Ha ndler CHandlerClass: :Funl):
II Obiekt delegatu.
Obiekt handl er zawiera adres funkcji statycznej Funl należącej do klasy Hand l erCl ass. W momencie wywołania delegatu wywoływana jest funkcja Handl erCl ass: :Funl () za pomocą argumentu takiego samego jak przekazany w wywołaniu delegatu . Wywołan ie delegatu można zap isać następująco :
handler ->Invoke(90); Powyższy
kod wywołuje wszystkie funkcje znajdujące s i ę na liście wywoławczej dele gatu han dler. W tym przypadku lista wywoławcza zawiera tylko jedną funkcję - Handl er Class : : Funl O, a więc rezultat jest następujący:
Funkcj a Funct ionl Delegat
z os t a ła wywo ła na
można także wywołać
z
wa rt oś c ią
90
za pomocą poniższej instrukcji:
handler (90): Jest to skrócona wersja poprzedniej instrukcji , która jawnie wywoływała Tego sposobu wywoływania delegatu używa się najczęściej .
funkcję
I nvoke O .
Operator + jest dla typów delegatowych przeładowany w celu umoż liwienia połączenia list dwóch delegatów w nowy obiekt delegatu. Na przykład listę wywoławczą delegatu ha nd l er możemy zmodyfikować za pomocą poniższej instrukcj i:
wywoławczych
handler
+=
gcnew Handler (HandlerCl ass: :Fun2):
Zmienna handl er odnosi się teraz do obiektu delegatu z listą wywoławczą zawierającą dwie funkcje: Funl i Fun 2. Jest to j ednak nowy obiekt delegatu. Listy wywoławczej delegatu nie można zmieniać, a w ięc operator + działa tu w podobny sposób jak z obiektami klasy St r ing zawsze tworzony jest nowy obiekt. Ponownie możemy wywołać deklarację za pomocą poniż szej instrukcji :
handler (BO ); Teraz rezultat jest następujący:
Funkcja Functi on l Funkcja Functi on2
zo s t a ła wywołan a zos ta ła wywoła n a
z z
wartoś ci ą war t o ś c ią
BO BO
582
lisnal C++ 2005. Od podstaw Obie funkcj e, które znalazły s ię na liście wywoławczej , w jakiej były dod ane do ob iektu delegatu. . Można
efektywnie
usunąć
wpis z listy
wywoławczej
zos tały wywołan e
delegatu za
w takiej
pomocą
kolejności ,
operatora - :
hand l er -= gcnew Handler(Hand lerClass: :Funl) : Powyższy
kod tworzy nowy obiekt delegatu , na którego l iście wywoławczej znajduje się tylko funk cja HandlerCl ass: :Fun2( ), ponieważ w efekcie jego działania usuwane są funk cj e listy wywoławczej z prawej strony z listy delegatu handl er oraz tworzony nowy obiekt wskazujący funkcje, które pozostają. Należy zwrócić uwagę, że
lista wywolawcza delegatu musi zawierać co najmniej j eden
wskaźnik do funk cji. Jeżeli za pomocą operatora odejmowania usunięte zostaną wszystkie wskaźn iki do funkcji, to rezultat b ędzie nu 77pt r.
W konstruktorze delegatu z dwoma parametrami pierwszy argument je st referencją do ob iektu na stercie CLR, a drugi obiekt jest adre sem funkcji obiektowej typu tamtego obiektu. A zatem poniższy konstruktor tworzy delegat zawierający wskaźnik do funkcji obiektowej określonej prz ez drugi argument obiektu wskazanego przez pierwszy argument:
HandlerC lass obj = gcnew HandlerClass; Handler hand ler2 = gcnew Hand ler (obj . &Hand le rC lass : :Fun3J; A
A
Pierwsza instrukcja tworzy obiekt, a druga delegat wskazujący funkcję Fun3( ) obiektu obj klasy Handl er Cl ass. Delegacja spodziewa się argumentu typu i nt , a ją wywołać za pomocą poniższej instrukcji:
należącą
do
więc można
handler2(70J ; W rezultacie zostaje wywołana funkcja obiektu obj Fun3() z argumentem o jest następujący:
Fun kCJ a Funct ion3
zo s ta ł a
wywolana z
wa rto ści ą
wartości
70. Wynik
71
Warto ść przechowywana w polu Value obiektu obj to l, ponieważ obiekt ten został utworzony za pomocą konstruktora domyślnego. Instrukcja w ciele funkcji Fun3( ) dodaje wartoś ć pola va l ue do argumentu funkcji - stąd wynik 71.
Ze
względu
można
je
na fakt, że listy
wywoławcze
delegatów handler i handl er2
są takiego
samego typu,
połączyć:
Ha ndl er handler = gcnew Ha ndl er (HandlerCl ass: :Funl J; handl er += gcnew Handl er (Handl erCl ass: :Fun2J; A
II Obiek/ de/ega/u.
Handl erCl ass obj = gcnew HandlerC lass: Handler ha ndler2 = gcnew Handler (obj. &Handl erCl ass : :Fun3J: hand le r += handler2; A
A
W powyższym kodzie ponownie tworzony jest obiekt handl er w skazujący delegat zawi eraj ący do statycznych funkcji Funl () i Fun2() . Następn ie tworzymy nowy delegat wskazywany przez obiekt handl er, zawierający wspomniane funkcje statyczne plus funkcję obiektową Fun3() obiektu obj. Delegat ten możemy teraz wywołać za pomocą poniższej instrukcji: wskaźniki
Rozdział 9.•
Dziedziczenie i funkcje wirtualne
583
handler(50l:
Rezultat tego wywołania jest Funkcj a Funct ionl Funkcj a Funct ion2 Funkcja Funct ion3
następujący:
zo stał a wywoł an a
z
warto śc i ą
zo st ał a wywoła n a zwar t o ś c l ą z o s t a ł a wywoła na
z
wart o ś c i ą
50 50 51
Jak widać, wywołanie delegatu spowodowało wywołanie dwóch funkcji statycznych orazfunkcji Fun3( ) będącej składową obiektu obj . A zatem najednej liście wywoławczej de1egatu można używać zarówno funkcji statycznych,jak i niestatycznych. Złożymy
teraz kilka fragmentów kodu w jedną całość i sprawdzimy, czy to rzeczywiście działa.
Rllml!:mI Tworzenie i wywoływanie deleuatu Poniższy
kodjest zbieraniną fragmentów, które
widzieliśmy , odkąd zaczęliśmy mówić
legatach:
#lnclude "st dafx.h" using namespace Syst em: publi C ref class Ha ndl erCl ass {
publ i c: st ati c void Funl (int m) { Console : :WriteLine(L" Funkcj a Funct ionl
zo s ta ł a wywołan a
z
wart o ścią
{O)" . ml :
stat ic void Fun2(int m) {Console : :Wri t eLine(L"Funkcj a Funct ion2
z o st ał a wywoł a n a
z
wart o ś c i ą
{Ol " , ml : l
wywo ł an a
z
wa rto śc i ą
{O)" .
z os t a ł a wywo ł an a
z
war t o ś c i ą
{O}" ,
void Fun3(int m) { Console: :WriteLine(L "Funkcja Funct ion3 zos ta ła m-val ue) :
l
vOld Fun4(int ml { Console : :Writ eLi ne(L"Funkcj a Function4 m-val ue) :
l
HandlerCl ass( ):val ue(l ){} HandlerCla ss(int ml:val ue(m l{} protected: int val ue: }:
public delegate void Ha nd ler (int valuel; i nt mai n(array Aargsl {
II Deklaracja delegatu.
o de-
584
VisIlai C++ 2005. Od podstaw Handler hand ler = gcnew Ha ndler CHandlerClass : .Funl ) : // Obiekt delegatu. Console: :WriteLineCL"Delegac ja z j ednym w s kaźni k iem do funkcj i st atycznej :") ; handler ->Invoke(90): A
handler += gcnew Hand le rCHa ndlerC lass: :Fun2); Console: :WriteLi neCL"\ nDelegacja z dwoma ws ka ź n i k a m l do funkcji sta tycznych:"); handler ->Invoke(80); Handle rCl ass obj = gcnew HandlerClass ; Handler handler2 = gcnew Hand ler Cobj, &HandlerClass ; :Fun3); ha ndl er += handler2; Console: :Writ eLine(L"\nDe legacja z trzema ws kaź n i ka mi do funkcj i :" ) ; handler (70); A
A
Console : :Wrl tel1ne(L"\ nSkracanie li sty wywo ł awcze j .. ."): handler - = gcnew Handler (HandlerClass: :Funl ); Console : :WriteL ine (L"\nDelegacj a ze ws k aź nika mi do jednej funk cji st atycznej funkcji obiekt owej: "); hand l ert 60); Rezultat
1
jednej
działania powyższego programu jest następujący:
Delegacj a z jednym ws k aźn lk iem do funkcji statycznej : Funkcja Functionl z o s t ała wywołana z wartością 90 Delegacja z dwoma w skaźnikami do fun kc j i statycznych: Funkcj a Functionl zo stała wywołana z wa rtością 80 Funkcj a Functi on2 z os t a ł a wywołana z wart oś cią 80 Delega cja z t rzema ws k a źnik a mi do funkc j i : Funkcj a Functi on l z ost a ł a wywołana z wa rto ś cią 70 Funkcj a Funct ion2 zo s t a ł a wywoł ana z wa rto ś cią 70 Funkcja Funct ion3 zos ta ł a wywo ł a n a z wa r t ośc i ą 71 Skracanie l isty
wywoł awc z ej
...
Delegacj a ze w s ka ź n i k a m i do jednej funkcj i statyc znej i j ednej funkcj i obiekt owej : Funkcja Funct ion2 z os t a ła wywo ł a n a z warto ś c i ą 60 Funkcja Funct ion3 zo s t a ła wywo ła n a z warto śc ią 61
Jak to działa Wszystkie operacje w funkcji mai n() widzieliśmy już wcześniej, Delegat został wywołany jawnie za pomocą funkcji I nvcke t ) przy użyciu uchwytu delegatu z listą argumentów. Z danych na ekranie wynika, że wszystko działa prawidłowo. Mimo że delegat w powyższym przykładzie może zawierać wskaźniki do funkcji jednoargumentowych, de legaty mogą wskazywać funkcje o dowolnej liczbie argumentów, Na przykład deklaracja delegatu może być następująca:
delegate void M yHandl er Cdoubl e x. St ri ng description ); A
RozlJział 9.
• Dzi8IJziczenie i funkcie wirlualne
585
P owyższa instrukcja deklaruje delegat MyHandl er , który może wskazywać tylko funkcje z typem zwracan ym voi d oraz dwoma parametrami - pierwszy typu doubl e, a drugi typu St r inq".
Delegatv niewiązane Delegaty, których przykład y oglądal i śmy do tej pory, były delegatami wiązanymi. Są one nazywane wiązan ymi, ponieważ mają stały zestaw funkcji w swoich listach wywoławczych. Można tworzyć także delegaty n iewiązane . Delegat taki wskazuje funkcję ob i ektową o okreś l o n ej liście parametrów i typie zwracanym dla danego typu obiektu. A zatem za pomocą jednego delegatu można wywołać funk cję obiektową dla dowolnego obiektu o k reś l o n e g o typu . Pon iżej znajduje się przykład deklaracji delegatu niewiązanego :
publl C delegat e void UBHa ndl er (ThisCl ass
A •
int value):
określa
typ wskaźnika t hi s, dla którego delegat typu UBHandl er może Funkcja ta musi mieć jeden parametr typu i nt oraz typ zwracany voi d. A zatem delegat typu UBHandl er może wywoływać funkcje dla dowolnego obiektu typu Thi sClass. Na pierwszy rzut oka wydaje się to dość sporym ograniczeniem, ale w rzeczywistości okazuje si ę bardzo przydatne. Można na przykład użyć tego deleg atu do wywołania funkcji dla wszystkich elementów tablicy, które s ą typu Thi sC l as s". Pierwszy argument
wywołać funkcję obiektową.
Delegat typu UBHandl er
UBHandl er ubh A
=
można utworzyć
za pomocą poniższego kodu:
gcnew UBHandler (&Th isCl ass : :Sum):
Argument konstruktora jest adresem funkcji klasy Thi sC l ass parametrów i typ zwracany. Poniżej
znajduje
s ię
posiadając ej wym aganą li stę
definicja klasy Thi sCl ass:
publ iC ref class ThisC lass {
publ lC: void Sum(i nt n. St r ing st r ) { Console : :Wr it eLine(L"Suma = {O)" . va lue A
+
n):
void Product Cint n. St ri ng st r ) { Console: :WriteLine(L" I1aczyn = {O )". val ue*nl : } A
ThisCl ass (doub le v) pri vate: double value:
valueCv ){}
}:
Funkcja Sum C) jest publi czn ą funkcją składową klasy Th i sCl ass, a zatem wywołanie delegatu ubh spowoduje wywołanie funkcji Sum() dla wszystkich obiektów tego typu klasowego. W wywołaniu delegatu niew iązanego pierwszy argument jest obiektem, dla którego funkcje z listy wywoławczej będą wywoływane, a następne argumenty są argumentami tych funkcji. Poni żej znajduje się przykładowe wywołanie delegatu ubh :
586
Visual C++ 2005. Od podstaw Thi sClassA obj = gcnew Thi sCl ass(99 .0): ubhrobj . S):
Pierwszy argument jest uchwytem do obiektu klas y Thi sCl as s utworzonego na stercie CLR poprzez przekazanie do konstruktora klasy wartości 99 . O. Drugi argument wywołania delegatu ubh to 5. W rezultacie zostani e wywołana funkcja Sum() z argumentem 5 dla obiektu wskazywanego przez obj . Del egaty niewiązane można łączyć za pomo cą operatora +, tworząc delegaty mogące wykilka funkcji. O czywi ście wszystkie funkcje muszą być kompatybilne z tym dele gatem. A więc dla delegatu ubh funkcje te musiałyby być funk cjami obiektowymi klasy Thi sCl ass, mającymi jeden parametr typu i nt oraz typ zwracany voi d. Poniżej znajduje się przykład:
wołać
ubh
+~
gcnew UBHa ndler(&Th isCl ass: :Product ) :
Wywołanie nowego delegatu wskazywanego przez ubh powoduje wywołanie zarówno funkcji Sum( ), jak i ProductC ) dla obiektu typu Thi sCl as s. Zobaczmy, jak to działa .
R!lml!::mI Używanie delegatów niewiązanYCh W przykładzie tym wykorzystano fragm enty kodu z poprzednich monstrowania działania delegatów niewiązanych :
podrozdziałów w
#i ncl ude "st dafx. h" using namespace System : publiC ref class Th isClass {
public: void Sum(i nt n) { Con sole . :WriteLi ne(L"Suma = {O} void Product(int n) { Console : :WriteLi ne t tT l oczyn = {O}
va l ue+n) : } val ue*n) :}
ThisClass(double v) : va lue(v){} pri vat e: do ub le value: }:
publ ic delegate void UBHa nd ler( Th isClass A, i nt va lue) : int mai n(array Aargs) {
arr ay