Visual C++ 2005: od podstaw
 8324606521, 9788324606528 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

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



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





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 .



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



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



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





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





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



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;



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



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



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 ayA thi ngs

~

{ gcnew Thi sCl ass(S.O) .gcnew ThisCl ass(lO.O). gcnew Thi sClass(lS .O) .gcnew Th isCl ass(20.0). gcnew ThisCl ass(2S.0) }:

celu zade-

Rozdział 9.•

UBHandl er" ubh = gcnew UBHandler (&Thi sC lass : :Sum) :

Dziedziczenie ifunkcje wirtualne

587

II Utworzenie obiektu delegatu.

II Wyw alanie delegatu dla każdego elementu tablicy things.

for each(This Class A t hing in things) ubh(thi ng, 3):

ubh += gcnew UBHandler (&Thi sClass : :Product): II

Wywołanie

II Dodawaniefunkcji do delegatu.

nowego delegatu dla wszystkich elementów tablicy things.

for each(ThisClassA t hing i n t ht nqs) ubh(t hing, 2):

ret urn O: Rezultat

działan i a powyższego

programu przedstawia się następująco:

Suma = 8 Suma = 13 Suma = 18 Suma = 23 Suma = 28 Suma = 7 Iloczyn = 10 Suma = 12 Iloczyn ~ 20 Suma = 17 Il oczyn = 30 Suma = 22 Iloczyn = 40 Suma = 27 Iloczyn = 50

Jak to działa zadeklarowany za pomocą poniższej instrukcji : publi c delegate void UBHandler(ThisClass A, int va l ue):

Typ delegatu UBHandl er

został

Obiekt y delegatu UBHa ndl er są delegatami n iewiązanymi mogącymi wywoływać funkcje obiektow e dla obiektów typu ThisCl ass , które mająjeden parametr typu i nt oraz typ zwracany voi d. Definicja klasy Thi sCl ass jest tutaj taka sama jak w poprzednim programie . Zawiera ona dwie funkcje obiektowe - Sum( ) i Produet ( ), Funkcje te mają parametr typu i nt oraz zwracają typ voi d, a wię c obie mogą by ć wywoływane przez delegaty typu UBHandl er . Tablicę

uchwytów do obiektów klasy Thi sClass w funkcji mai n() instrukcji :

utworzyliśmy

następującej

arrayA thlngs

= (

}:

gcnew ThisCl ass(5.0) ,gcnew ThisC lass(lO .O), gcnew ThisClass( 15.0),gcnewThi sCl ass(20.0J, gcnew ThisClass(25. 0)

za pomocą

588

Visual C++ 2005. Od podstaw Każd y z p ięc iu obiektów na liście inicjalizacyjnej zawiera inną wartość typu doubl e. Dz ięki temu łatwo jest w danych wyj ściowych rozpoznać obiekt, dla którego zostały wywołane funkcje Sum( ) i Produet ( ).

Obiekt delegatu tworzymy za pomocą poniższej instrukcji : UBHandler ubh A

=

gcnew UBHandler (&Thi sC las s : :Sum) ;

II Utworzenie obiektu delegatu .

Wywołanie obiektu delegatu wskazywanego przez uchwyt ubh powoduje wywołan ie funkcji Sum( ) dla wszystkich obiektów typu Thi sCl ass i jest to robione dla każdego obiektu w tablicy thi ngs:

for each(Thi sCl ass t hing i n t hi ngs) A

ubht t tnn q. 3) : Pętla

pętli delegat jest z elementem tej tablicy jako pierwszym argumentem. To powoduje wywołanie funkcj i Sum() dla obi ektu thi ng z argumentem 3. W ten sposób utworzonych zostaje pięć pierwszych wiersz y danych wyj ściowych .

f or each przechodzi przez wszystkie elementy tablicy t hi ngs. W ciele

wywoływany

N astęp n i e

ubh

+=

tworzymy nowy delegat:

gcnew UBHandle r (&Thi sCla ss : :Product ) :

II Dodawanie funkcji do delegatu.

Powyższa

instrukcj a tworzy nowy delegat typu UBHandl er , w skazujący funkcj ę Product O, oraz łączy go z istniejącym delegatem wskazywanym przez ubh. Rezultatem jest nowy delegat po siad ający na swojej liście wywoławczej wskaźniki do funkcji Sum() i Pr oduct ( ). Ostatnia pętla wywołuje delegat ubh dla każdego elementu tablicy t hi ngs z argumentem 2. W rezultacie zarówno funkcja Sum( ), jak i Pr oduct ( ) zostaną wywołane dla każdego obiektu klasy Thi sCl ass z argumentem 2. W ten sposób powstaje następne dziesięć wierszy danych wyjściowych .

delegatów w bardzo prostych przykładach, to do starczają one niedo programów. Można na przykład przekazać niewiązany delegat do funkcji jako argument, aby umożliwić tej funkcji wywoływanie różnych kombinacji funkcji obiektowych w różnym czasie - a więc delegat może działać jako selektor funkcji. Kolejnoś ć wywoływania funkcji przez delegat odpowiada ich kolejności na liście wywoławczej , a więc za pomocą delegatu można kontrolować kolejność wywoływania funkcji . Mimo

że

my

używali śmy

zwykłej elastyczności

Tworzenie zdarzeń Jak już wcześniej pisałem, sygnalizowanie zdarzenia angażuje delegat, który zawiera wskaź­ niki do funkcji mających być wywołanymi w momencie wystąpienia zdarzenia. Większoś ć zdarzeń w programach to zdarzeni a pochodzące od elementów sterujących, takich jak przyciski lub elementy menu, i są one wynikiem interakcji użytkownika z programem, chociaż można także zdefiniować i uru chamiać zdarzenia we własnym kodzie programu. Zdarzenie jest składową klasy referencyjnej , którą definiuje event oraz nazwy klasy delegatu:

się

za

pomocą s łowa

kluczowego

Rozdzial9. • Dziedziczenie i funkcie wirtualne public delegate void

Oo or Handler CS tr i ng

Ą

589

st r);

II Klasa ze zda rzeniem.

pu blic ref class Ooor (

publ ic: II Zdarzenie wywo lujące fu nkcje skojarzo ne II z obiektami delegatu DoorHandl er.

event

O o or Ha nd l er

Knock :

Ą

II Funk cja uruchamiajqca zdarz enia.

void TriggerEvent s C) (

Knock ("Alfred" ); Knock( "Janina"); }:

Klasa Ooor ma zdarz enie składowe o nazwie Knock, które odpowiada delegatowi typu Ooor Ha ndl er. Knock jest obiektową składową klasy, ale zdarzenia można także defin iować jako składowe statyczne za pomocą słowa kluczowego st at i c. Można także zadeklarować zdarzenie wirtualne. Kiedy uruchomione zostaje zdarzenie Knock, to może ono wywoływać funkcje z listą parametrów i typem zwracanym określonymi przez delegację OoorHandl e. W klasie Ooor znajduje się także funkcja publiczna Tri ggerEvent C) uruchamiająca dwa zdarzenia Knock - każde z innymi argumentami. Argumenty przekazywane są do funkcji, które zostały zarejestrowane w celu otrzymywania powiadomień o zdarzeniach Knock. Jak widać, uruchamianie zdarzenia odbywa się w taki sam sposób jak wywoływanie delegatu. Poni żej

znajduje

się przykładowa definicja

klasy

obsługującej

zdarzenia Knock:

publ ic ref class AnswerOoor (

public: void

Im ln CS tri ng

name)

Ą

(

Console: :Wrlt eLi neCL"W ejdt {O}. drzwi



otwarte. ".name):

}

void

I mO u t C S t r i ng

Ą

name)

{

Conso le : :WriteLi neC L"Odej dt (O) . ni e ma mnie w domu .".name) ; } }:

Klas a An swerOoor ma dwie publiczne funkcje składowe, które mogą potencjalnie obsłużyć zdarzenie Knock, ponieważ obie mają listy parametrów i typ zwracany taki sam jak podano w deklaracji delegatu OoorHandl e r . Przed zarejestrowaniem funkcji do otrzymywania powiadomień o zdarzeniach Knock nale ży utworzyć obiekt Ooa r : O oo r

Ą

door

=

gcnew Ooor ;

590

Visual C++ 2005. Od podstaw Teraz m ożna zarejestrować funk cję , aby w obiekcie Ooor w następujący sposób :

otrzymywała

powiadomienia o zdarzeniach Knock

An swerDoor answer = gcnewAnswe rDoor ; door ->Knock += gcnew OoorHandle rC answer . &AnswerDoor ; ; Imln); A

Pierwsza z powyższych instrukcji tworzy obiekt typu AnswerOoor - jest to konieczne, poniefunkcje Iml n( ) i ImOut () nie są statycznymi skład owymi klasy. Następn ie dodajemy egzemplarz typu delegatu OoorHandl er do składowej Knock obiektu Ooor . Jest to analogiczne z procesem dodawania wskaźników do funkcji delegatu. W podobny sposób można dodawać następne funkcje uchwytowe do wywołania w momencie uruchomienia zdarzenia Knock. Zobaczymy na przykładzie, jak to działa .

waż

~ Obsluga zdarzeń Poniższy

program definiuje, uruchamia i obsługuje zdarzenia za

pomocą

wc ześniej :

II Cw9_19.cpp : main p roj ect jile. II Definiowani e, uruchamianie i obsługa zdarzeń .

#include "st dafx.h" using namespace Syst em; publ lC delegate vo id DoorHand lerC St r i ng st r) ; A

II Klasa ze zdarzeniem składowym .

publ ic ref class Ooor {

publ t e : II Zdarzenie wywołujące f unkcje skojarzone z Ilo biektem delegatu Doorlła ndler.

event OoorHandler Knock; A

II Funkcja

uruch amiająca

zdarzenia.

void TriggerEventsC) (

KnockiL"Alfred"); KnockCL "Janina"); }

l; II Klasa

dejiniująca funkcje obs ługujące

zdarzenia Knock.

publi c ref class AnswerDoor {

pub li e: void Iml n(St rlng name ) A

{ C on so le ; ;W r iteL i neC L"Wejd ź

{Ol . drzwi



ot wart e." .name) ;

l void ImOut( St ri ng name) A

{ Conso l e:; W r it e L i n eC L " Od e j d ź

(O l . nie ma mnie w domu.".name) ;

klas stworzonych

Rozdzial9. • Dziedziczenie i lunkcje wirtualne

591

} }:

i nt ma i n(array Aargs) {

Door door ~ gcnew Door : AnswerDoor A an swe r = gcnew Answe rDoo r : A

II Dodawanie procedury obsług i dla zdarzenia Kno ck

door- >Knock

+=

door->Trigge rEvents() ; II Zmiana sposobu

będącego składo wą

obiektu door.

gcnew DoorHandle r( answer , &An swerDoor: :Iml n) ,

obsługi

II Uruchomi enie zdarzeń Kno ck.

zdarzenia Kno ck.

door ->Knock -~ gcnew OoorHa ndler(answer , &AnswerOoor: : Imln); door- >Knock += gcnew DoorHandler( answer . &AnswerDoor: :ImDut ) ; door ->TriggerEvent s() : II Uruchomienie zdarzeń Kn ock. ret urn O: Rezultat

działania

tego programu widoczny jest poniżej:

Alfred. drzwi są otwarte. Jani na . drzwi są otwarte . Od e j d ź Alfred. nie m a mnie w domu. Od e j d ź Janina, nie m a mnie w domu. W e j dź W ejdź

Jak to działa Najpierw tworzone

są dwa

obiekty w funkcji ma i n( ):

Door door = gcnew Ooor : AnswerDoor A answer = gcnew AnswerDoor; A

Obiekt door ma zdarzenie składowe Knock, a obiekt answer ma funkcje składowe, które zarejestrować do wywoływania dla zdarzeń Knock. Następna

można

instrukcja rejestruje funkcję składową I mln( ) obiektu answer do otrzymywania zdarzeniach Knock dla obiektu door:

powiadomień o

door ->K noc k += gcnew DoorHandler(a nswer . &AnswerDoor : :Imln): Jeśli będzie ływane,

to miało jakiś sens, to można zarejestrować kiedy uruchamiane jest zdarzenie Knock.

Następna

także

inne funkcje, aby

były

wywo-

instrukcja wywołuje funkcję składową obiektu door Tri ggerEvents( ):

door->Tr iggerEvent s () ;

II Uruchomienie zdarze ń Knock.

W rezultacie powstają dwa zdarzenia Knock - jedno z argumentem Al fred, a drugie z argumentem Janin a. W wyniku tego funkcja Imlnt ) zostaje wywołana dwukrotnie - po jednym razie dla każdego zdarzenia, co daje dwa pierwsze wiersze danych wyjściowych .

592

VisIlai C++ 2005. Od pollstaw Oczywi ście możemy potrz ebować różnych

w zależności od cji mat nt ):

okoliczności .

reakcj i na dane zdarzenie w różnych sytua cjach, Takiej sytuacj i dotyczą właśnie następne trzy instrukcje w funk-

door->Knock -~ gcnew DoorHandler (answe r, &Answe rDoor: : Iml n); door->Knock +~ gc new DoorHandler (answer, &Answe rDoor : :ImOut ) ; door ->TriggerEvent s( ); II Uruchomienie zda rze ń Knock. Pierwsza z powyższych instruk cji usuwa wskaź n i k do funkcji Iml ru ) ze zdarzenia, a druga instrukcja rejestruje funkcję l mOut ( ) dla obiektu answer w celu otrzymywania przez n iąpowia­ domień o zdarzeniu. Kiedy zdarzenie Knock zostaje uruchomione przez trzecią instrukcję, to wywoływan a jest funkcja ImOut( ) i rezultat jest nieco inny.

Finalizatory idestruktory wklasach referencyjnych Destruktor klasy referencyjnej definiuje się w taki sam sposób j ak konstruktor w natywnym C++. Destruktor klasy referen cyjnej wywoływany jest w momencie, gdy uchwyt wychodzi poza zasi ęg lub obiekt jest częścią innego obiektu , który jest właśnie niszczony. Do uchwytu do klasy referencyjnej można także zastosować operator del ete , czego skutkiem będzi e wywołanie destruktora. Głównym powodem implementacji destruktora dla klasy w natywnym C++ jest usuwanie danych alokowanych na stercie. Oczywiście w przypadku klas referencyjnych nie jest to problemem, a więc w tego typu klasach istnieje o wiele mniejsza potrzeba definiowania destruktorów. Destruktor może być w klasie referencyjnej potrzebny, gdy j ej obiekty używają innych zasobów, które nie są zarządzane przez mechanizm usuwania nieużytków­ takich jak pliki , które muszą zostać zamknięte w odpowiedniej kolejności, kiedy obiekt jest niszczony. Tego typu zasoby można także czyści ć za pomocą innego rodzaju składowej klasy, zwanej finalizatorem . Finalizator jest specjalną funkcją składową klasy referencyjnej , która jest wywoływana automatycznie przez mechanizm usuwania nieużytków podczas niszczenia obiektu. Należy pamię­ tać, że finalizator nie zostanie wywołany dla obiektu klasy, jeżeli destruktor został wywołany j awnie lub w wyniku zastosowania operatora del ete . W klasach pochodnych finalizat ory s ą wywoływane w takiej samej kolejno ści , w jaki ej byłyby wywoływane destruktory. A zatem jako pierwszy wywoływany jest finalizator klasy znajdującej się w hierarchii najwyżej , a następnie finalizatory klas pochodnych. Finalizator klasy na naj niż szym poziomie jest wywoły­ wany jako ostatni. Klasę

z finalizatorem definiuje

s ię

w następujący sposób:

Dubl ic ref class MyCla ss ( II Definicj a fi nalizatora.

IMyClass() { II Kod czyszczenia, kiedy obiekt jes t niszczony ...

} II Reszta definicji klasy...

};

Rozdział 9.•

Dziedziczenie ifunkcje wirtualne

593

Funkcję finalizatora w klasie definiuje s ię w podobny sposób jak destruktor, ale zamiast znaku tyldy (-) przed nazwą destruktora używa się wykrzyknika ( !). Podobnie jak w przypadku destruktora, dla final izatora nie można podawać typu zwracanego, a s pecyfikator dostępu jest ignorow any. Sposób d ziałania destruktorów i final izatorów przedstawimy na krótkim przykładzie .

~ Finalizatory idestruktory Poniżs zy

program pokazuje, kiedy

są wywoływan e

destruktory i finalizatory :

#incl ude "stdafx.h" using namespace Syst em. ref class MyClass (

publ lC: II Konstrukto r.

M yCl assCint n)

val ueC n){}

II Destruktor .

-MyCla ss() (

Conso le: :Wri te Li neC "Destru kt or obiekt u C{O)) klasy M yClass val ue):

z o s t a ł wywoła ny".

II Finalizator.

IM yClass() (

Console: :Writ eLineC"Fi nali zat or obiektu C{ O}) klasy M yCl ass value): }

private: lnt val ue : }:

int mai nCarray Aargs ) {

M yClassAobJl = gcnew M yCl assCl ): M yCl assAobJ2 = gcnew M yClass(2); M yCl assA obj3 = gcnew M yClass(3): delete obj 1: obJ2->-MyClassC) , Conso le : :WriteLi neC L"Kon iec programu ."): ret urn O;

zo s t a ł wywo ła ny .".

594

Visual C++ 2005. Od podstaw Rezultat działania tego programu jest następujący:

Dest rukt or obiekt u (l ) klasy M yCl ass zo s ta ł wywoła ny .

Dest rukt or obiektu (2) klasy M yCl ass zos t a ł wywala ny. Koni ec programu. Final izat or obiekt u (3) klasy M yCl ass zos ta ł wywal any.

Jak to llziała Klasa MyClass ma konstruktor, destruktor i finalizator. Destruktor i finalizator wysyłają tylko komunikaty na wyjście , dzięki którym wiemy, kiedy każdy z nich został wywołany. Dodatkowo wiadomo jest, dla którego obiektu wywołany został finalizator lub destruktor, ponieważ wysyłają one na wyj ś cie także wartość pola val ue. W funk cji ma i n( l tworzone są trzy obiekty typu MyClass różniące się przechowywanymi wartościami 1, 2 i 3. Dla obiektu obj 1 można zastosować operator del et e, a następn ie jawnie wywołać destruktor dla obiektu obj2. W rezultacie otrzymujemy dwa pierwsze wiersze danych wyjściowych wygenerowanych przez wywołania destruktora dla obiektów. Następny

wiersz danych wyjściowych został utworzony przez instrukcję poprzedzającą inr et urn w funkcji main ( l . A zatem ostatni wiersz danych wyjściowych, wygenerowany przez finalizator dla obiektu obj3, znajduje s ię już po zakończeniu funkcji mai n( l. Z danych wyjściowych wyn ika, że konstruktory są wywoływane w momencie usuwania obiektu lub jawnego wywoływaniajego destruktora. Destruktor obiektu zostaje wykon any . Operacje te wstrzymują wykonanie finalizatorów dla obiektów. Obiekt wskazywany przez obj 3 jest niszczony przez mechanizm usuwania nieużytków, gdy program kończy działanie, a finalizator wywoływany jest w celu wyczyszczenia wszelkich niezarządzanych zasobów. strukcję

Jeżeli

zatem klasa ma zarówno finalizator, jak i destruktor, to w czasie niszczenia obiektu wytylko jeden z nich. Destruktor jest wywoływany, gdy niszczenie obiektu zostało zaprogramowane, a fmalizator, gdy obiekt w sposób naturalny wychodzi poza zasięg . W związ­ ku z tym można dojść do wniosku , że jeżeli po zniszczeniu obiektów pamięć jest czyszczona za pomo c ą finalizatora, nie powinno się jawnie usuwać obiektów. woływany jest

Jeżeli usuniemy komentarz w funkcji mai nt l z instrukcji, które niszczą obiekty obj l i obj2, to zobaczymy, że finalizatory tych obiektów wywoływane są przez mechanizm usuwania nieu żytków, kiedy program kończy działanie. Jeżeli natomiast usuniemy finalizator z klasy MyClass, to stwierdzimy, że destruktor obiektu obj3 nie zostanie wywołany przez mechanizm usuwania nieużytków, a więc pamięć nie jest czyszczona. Nasuwa się zatem wniosek, że jeżeli chcemy, aby niezarządzane zasoby używane przez obiekt były obsłużone bez względu na to, w jaki sposób kończy się czas życia obiektu, powinniśmy zaimplementować w klasie zarówno destruktor, jak i finalizator.

Klasy generyczne W języku C++/CLI istnieje możliwość definiowania klas generycznych. Określona klasa konkretyzowana jest z klasy generycznej w czasie działania programu. Można definiować generyczne klasy wartości, generyczne klasy referencyjne, generyczne klasy interfejsowe oraz

Rozdzial9.• Dziedziczenie ilunkcie wirtualne

595

generyczne delcgaty. Klasę generyczną definiuje się za pomocą jednego lub większej liczby parametrów typu w podobny sposób jak w przypadku funkcji generycznych, o których pisałem w rozdziale 6. Poniżej

znajduje

się przykładowa definicja

klasy generycznej Stack z projektu e w9_14:

II Plik naglówkowy Stack.h w projekcie Cw9 j l. II Generyczna klasa reprezentują c a stos.

gener ic ref class Stack {

privat e: II Definicj a elementów do przechowywania na stosie.

ref st ruct !t ern (

T ObJ : It ern Ne xt : A

II Uchwy t do obiektu w tym elemencie. II Uchwy t do n astęp n ego elementu na stosie lub nullpt r.

II Konstruktor.

Item(T obj. It em" next i : Obj (obj ), Next (next) {) }:

It ern Top: A

II Uchwyt do elementu znajdującego się na wierzchu.

publ t e : II Usta wianie obiektu na stosie.

void Pu sh(T obj ) {

Top = gcnew It ern( obj, Top):

II Utworzenie nowego elementu i ustawienie go na samej górze.

} II Usuwanie obie ktu ze stos u.

T Pop () (

i f( Top == nu l l pt r ) retu rn H ):

II Jeżeli stos j est pu sty, II zwróci wartość zerową.

T obj = Top->Obj : Top = Top- >Next : ret urn obj;

II Pobieranie obiektu z elem ent u. II Wstawienie następn ego elementu na wierzch.

} }:

Klasa ta w wersji generycznej ma parametr typu T. Należy zwrócić uwagę, że zamiast słowa kluczowego typename do określenia parametru można by było użyć słowa kluczowego c lass w tym kontekście nie ma między nimi żadnej różnicy . Argument typu zostaje podstawiony w miejsce parametru T, kiedy używany jest typ klasy generycznej . Parametr T zamieniany jest na argument typu poprzez definicję klasy, a wię c główną zaletą w porównaniu z oryginalną wersją klasy jest fakt, że klasa generyczna jest o wiele bezpieczniejsza, nie tracąc przy tym na elastyczności. Funkcja składowa oryginalnej klasy Push( ) przyjmuje każdy uchwyt, a więc można by było na jednym stosie umieścić obiekty różnego typu, jak M yCl as SA, Str i ng czy jakiegokolwiek innego . A

596

Visual C++ 2005. Od podstaw Spójrzmy na implementację funkcji Popt ) . W wersji oryginalnej zwracała ona nul l pt r, j eże l i element na wierzchu stosu był zerowy. Dla parametru typu nie można jednak zwrócić null pt r, ponieważ argument typu może być typem wartośc i . Rozwiązaniem j est zwrócenie Te l, czyli wywołanie konstruktora bezargumentowego dla typu T. Daje to w wyniku ekwiwalent Odla typu wartości i null ptr dla uchwytu . Zauważ, że

na parametry typu klas generycznych można nakładać ograniczenia za pomocą kluczow ego where w taki sam sposób jak w przypadku funkcji gen erycznych, omawianych w rozdziale 6.

słowa

z typu generycznego przechowującego uchwyty do obiektów klasy Bax można utworz yć stos w następujący sposób:

Argument typu znajduje się w trójkątnym nawiasie. Instrukcja ta tworzy na stercie CLR obiekt St ack-Box"> , Obiekt ten pozwala na ustawianie uchwytów typu Bax na stosie, a także wszelkich innych uchwytów, których bezpo średnią lub po średnią klasą bazową jest klasa Box. Wypróbujemy to na zmodyfikowanej wersji programu Cw9_14 . A

~ Uźywanie generycznegO typU klasowego Utwórz nowy program konsolowy CLR o nazwie Cw9j l i skopiuj do niego pliki nagłówkowe Container.h, Box.h oraz GlassBox.h z projektu Cw9_14. Dodaj te pliki do projektu, klikając prawym przyciskiem myszy Header Fil es w zakładce Solution Explorer i wybi erając Add/Existing ltem z menu kontekstowego. Następnie dodaj do projektu nowy plik nagłówkowy Stack.h i wpisz do niego definicję generycznej klasy St ack, która została utworzona powyżej. Nie zapomnij o dodaniu dyrektywy #pr agma ance na początku pliku. II Cw9_2 I.cpp : main proj ect file. II

Używan ie

#include #include #incl ude #include

klasy

zagnieżdżonej

do definicji stosu .

"stdafx.h" "Box.h" "Gl assBox.h" "St ack.h"

II Dla klas Box i Container. II Dla klasy GlassBox (oraz Box i Container). II Dla ge nerycznej klasy reprezentującej stos.

using names pace System: int mai n(array Aargs) (

arrayA boxes

= {

gcnew Box(2.0, 3.0. gcnew GlassBox(2.0, gcnew Box(4.0. 5.0. gcnew GlassBox(4.0,

4.0) . 3.0, 4.0). 6.0), 5.0. 6.0)

};

Conso1e: :W riteLi ne ( L" P u d e ł ka w t ab1icy

ma j ą n as t ępu j ą c e

pojemnośc i :"):

Rozdział 9.

• Dziedziczenie ifunkcie wirtualne

for each(BoxA box in boxes) box ->ShowVolume() ;

II

Con sole : ;Writ eLi ne(L"\nDodawa nie

pu dełek

Wysyła nie

na

wyjście pojemnoś ci pudełka.

do stosu. .. "):

StackA st ack = gcnew Stack; for each(BoxA box i n boxes ) st ack->Push(box);

II Utworze nie stosu.

Console: ;WriteLine( L"Usuwani e pudel ek ze st osu prezent uje je w odwróconej BoxA item; whil e«item = st ack->Pop() ) 1= nul l pt r ) safe_cast(it em)->ShowVol ume ( );

k o l e j n o ś ci :" ) ;

II Wypróbowywanie generycznego typu Stack przechowującego liczby ca łkowite.

Stack" numbers = gcnew Stack; II Utworzenie stosu.

Console: :Writ eLi ne(L"\ nDodawanie for (i nt i = 2 ; iPop ()) !- O) Console: ;Write( L" {O,5} ".number );

c a ł k ow ityc h

ze st osu daje wwyniku ' '') ;

Console : :WriteLi net i : ret urn O;

Rezultat działan ia powyższego programujest n astępujący : Pud e ł ka

w t abl icy

Po jem no ś ć użytkowa Pojemno ść u żytkowa Pojemno ś ć u ż ytkowa Pojemność użytkowa

maj ą n astę p uj ąc e pojemn ośc i:

obiektu obiektu obiektu obiektu

klasy klasy klasy klasy

Box wynosi Box wynos i Box wynosi Box wynos i

24 20.4 120 102

Dodawanie pude ł e k do st osu.,. Usuwanle pud e łe k ze stosu prezentuje je w odwróconej Poj emn o ść użyt k owa obiektu klasy Box w ynosl 102 Poj em no ść uż yt k owa obiektu klasy Box w ynosi 120 Po jem n o ś ć użytkowa obiektu klasy Box wynosi 20 ,4 P ojem n o ść użytkowa obiektu klasy Box w ynos i 24 Dod awanie pude łe k do st osu. . . 2 4 6 8 10 12 li czb c ał k ow ityc h ze stosu daje wwyniku: 10 8 6 4 2

U s u n i ęc i e

12

kol ejno ś c i :

597

598

Visual C++ 2005. Od podstaw

Jak lo działa Stos przechowujący uchwyty do obiektów klasy Box definiowany jest w

poniższej

instrukcji:

II Utworze nie stosu.

Parametrem typu jest Box"; a w ięc stos przechowuje uchwyty do obiektów klasy Box lub uchwyty do obiektów klasy Gl ass Box. Kod wstawiający obiekty na stos i u suwający je ze stosu niczym nie różni się od tego w projekcie 9_14 . Różnica polega na tym, że nie mo żna by było na stos wstawić obiektu , jeżeli typ nie miałby jako pośredniej lub bezpo średniej klasy bazowej klasy Box. A zatem typ generyczny gwarantuj e, że wszystkie obiekty są obiektami klasy Box. Przechowywanie liczb

całkowitych

wymaga utworzenia nowego obiektu Stack :

StackA numbers = gcnew Stack ;

II Utworzenie stos u.

W oryginalnej wersj i ten sam niegeneryczny obiekt Stac k przechowywał referencje do obiektów klasy Box i liczby całkowit e , pokazując w ten sposób , że działania związane ze stosem nie były bezpieczne dla typów. Tutaj określiliśmy argum ent typu dla klasy generycznej jako typ wartości i nt . Dzięki temu funkcja Push() przyjmuje tylko obiekty tego typu. Pętle usuwające

obiekty ze stosu pokazują, że zwracanie T() w funkcji Pop() Odla typu int i null pt r dla typu uchwytowego Box" .

rzeczywiście

daje

Generyczne klasy inter1ejsowe Interfejsy generyczne definiuje się w podobny spo sób co generyczne klasy referencyjne. Ponadto generyczną klasę referencyjną można zdefiniować na bazie interfejsu generycznego. W celu zademonstrowania, jak to działa , możemy zdefiniowa ć interfejs generyczny, który może być zaimplementowany przez klasę generyczną Stack . Poniżej znajduje się definicja interfejsu generycznego: II Interf ej s dla

dzia lań

na stosie.

generic publ ic int erf ace class ISt ack {

vaid Push(T abj) : T PapO :

II Wstawianie elem entu na stos.

}: Powyższy

interfejs ma dwie funkcje i usuwania ich.

identyfikujące

Definicja generycznej klasy Stack

implementującej

stępująca:

generic ref class Stack : IStack {

private ; II Defini owanie elem entów do przechowywania na stosie.

ref struct Item {

operacje wstawiania elementów na stos

interfejs generyczny ISt ack jest na-

Rozdział 9.

T Obj : It em" Next .

• Dziedziczenie i funkcie wirtualne

599

II Uchwyt do obiektu w tym elemencie. II Uchwy t do następnego obiektu na stosi e lub nullptr.

II Konstrukt or.

Item(T obJ . It em next) : Obj(obj) . Next(next){ } A

}:

It em Top: A

II Uchwyt do obiektu znajdującego s ię na wierzchu .

public: II Wstawienie obiektu na s tos.

virt ual void Pus h(T obj ) Top = gcnew It em (obj . Top) :

II

Us un ięcie

II Utworzenie nowego obiektu i umies zczenie go II na wierzchu .

obiektu ze stos u.

vir tual T Pop() if (Top == null pt r ) ret urn TO:

II Jeżeli stos jest pusty, II zwróć wartość zerową.

T obj = Top->Obj : Top = Top->Next : return obj:

II Pobranie obiektu z elementu. IlUstawienie n ast ępn ego elementu na wierzchu.

}:

Zmiany w stosunku do poprzedniej wersji definicji klasy generycznej St ack zostały wyróż­ nione na szarym tle. W pierwszym wierszu definicji klasy generycznej parametr typu T został użyty jako argument typu interfejsu IStack , a więc argument typu użyty dla egzemplarza klasy St ack ma zastosowanie również do interfejsu. Funkcje Push () i Pop() muszą być teraz zdefiniowane w klasie jako wirtualne, ponieważ są one wirtualne w interfejsie. Jeśli chcesz zobaczyć, jak program działa z interfejsem generycznym, możesz dodać do poprzedniego przykładu plik nagłówkowy zawierający interfejs I St ack oraz poprawi ć definicję klasy generycznej St ack, tak aby była zgodna z tym programem, a następnie dokonać ponownej kompilacji.

Kolekcie generyczne Kolekcja to klasa organizująca i przechowująca obiekty w określony sposób. Lista powiązana i stos są typowymi przykładami kolekcji . Przestrzeń nazw System : :Co 11 ect i ons : :Gener i c zawiera szeroki zestaw kolekcji generycznych implementujących kolekcje o ściśle określonych typach. Dostępne kolekcje generyczne są pokazane w tabeli na następnej stronie .

Nie będę zagłębiał się w szczegóły dotyczące powyższych klas, ale krótko przedstawię trzy z nich, które mogą być najbardziej przydatne. Dla uproszczenia w przykładach będę używał typów wartościowych , chociaż kolekcje działają również z typami referencyjnymi.

600

VisIlai C++ 2005. Od podstaw

Typ

OpiS

Li st

Przechowuje elementy typu T w prostej automatycznie powiększać.

LinkedLi st

Przechowuje elementy typu T w

Stack

Przechowuje elementy typu T na stosie, który stanowi mechanizm przechowywania typu .first-in last-out" .

Queue

Przechowuje elementy typu T w kolejce, która je st mechanizmem przechowywania typu .first-in first-out" ,

Dict iona ry

Przechowuje pary

liście

klu cz-wartość,

li ście ,

która w razie potrzeby

podwójnie

gdzie klucze

może się

wiązanej.

są typu

K, a wartości typu V.

List -lista generyczna Li st definiuje listę generyczną, która w razie potrzeby automatycznie zw ię ks za swoje rozmiary . Elementy do tej listy można dodawać za pomocą funkcji Add( ), a dostęp do nich można uzyskać za pomocą indeksów - podobnie jak w tablicach. Poniżej znajduje się definicja listy przechowującej warto ści typu i nt: List numbers = gcnew List: Powyższa

lista ma określoną pojemność domyślną, ale można znajduje się definicja listy o pojemności 500:

podać własną wymaganą

war-

tość . Poniżej

Li st numbers = gcnew List; Obiekty do listy dodaje

się

za pomocą funkcji Add():

for(i nt i = O ; iAdd( 2*i+1); Powyższy kod dodaje 1000 liczb całkowitych do listy nurnbers. Lista powiększa się automatycznie, jeżeli jej pojemność jest mniejsza niż 1000. Aby dodać element do istniejącej już listy, można użyć funkcji Inse rt( ) w celu dodania elementu określonego przez drugi argument w miejscu wskazanym przez indeks w pierwszym argumencie. Elementy listy są indeksowane od zera, podobnie jak w tablicach. Zawartość

listy

można zsumować:

i nt sum= O; for t i nt l = O : i Count : i ++) sum += numbers[i ] : Count jest właściwością zwracającą bieżącą liczb ę elementów w liście . Dostęp do elementów listy można uzyskać poprzez domyślną właściwość indeksowaną. W ten sposób można także pobierać i ustawiać wartości. Należy zauważyć, że za pomocą domyślnej właściwości indeksowanej nie można zwiększyć pojemności listy. W przypadku użycia indeksu spoza bieżącego zbioru elementów listy wywołany zostaje wyjątek.

Rozdział 9.

Elementy listy

można zsumować także

w

• Dziedziczenie ifunkcie wirtualne

następujący

601

sposób :

for eachCi nt n in numbers) sum+=n ;

Istnieje wiele funkcji, których można używać do operacji na listach. Należą do nich funkcje usuwające elementy, sortujące elementy, a także przeszukujące zawartość listy.

LinkedList - generYCZna lista podwójnie wiązana listę wiązaną z przednimi i wstecznymi wskaźnikami , a więc można w obie strony. Poniżej znajduje się definicja listy wiązanej przechowującej liczby zmiennopozycyjne:

Li nked Li st definiuje

ją iterować

Link edList ~

Wartości

do tej listy

values = gcnew Li nkedL i st;

można dodać

w

następujący

sposób:

forCint i = O ; iAddLastC2 .5*i);

Funkcja AddLast( ) dodaje elementy na końcu listy. Aby należy posłużyć się funkcją AddF i rst( l .

dodać

element na

początku

listy,

Funkcja Fi nd (l zwraca uchwyt typu Li nkedLi sttłode-T>" do wierzchołka listy zawierającego która jest przekazywana jako argument do funkcji Fi nd( ). Uchwytu tego można użyć do wstawienia nowej wartości przed znalezionym wierzchołkiem lub po nim. Na przykład :

wartość,

Li nkedL i stNadeA nade

~

val

ues->Fi nd C20 .O) ; II Znajdź wierzchołek zawierający II

ifCnade 1= nullp tr ) va l ues->AddBeforeCnade. 19 9):

wartość

II Wstaw

20.0.

19.9 przed tym wierzchołkiem.

Pierwsza z powyższych instrukcji znajduje wierzchołek zawierający wartość 20. O. Jeżeli taki nie istnieje, funkcja Fi nd( l zwraca wartość null ptr . Ostatnia instrukcja, wykonywana, jeżeli wierzchołek node nie ma wartości zerowej, dodaje nową wartość 19.9 przed tym wierzchoł­ kiem. Za pomocą funkcji AddAfter ( l można dodać nową wartość za danym wierzchołkiem. Przeszukiwanie listy powiązanej jest względnie wolne, ponieważ konieczne jest przejście po kolei przez wszystkie elementy. Elementy listy

można zsumować

w

następujący

sposób:

dauble sumd = O; for eachC dauble v i n values) sumd +~ v; Pętla for each przechodzi przez wszystkie elementy listy i zapisuje ich w zmiennej sumd. Właściwość

łączną wartość

liczbę elementów w liście powiązanej, a właściwości Head i Tai l odpowiednio pierwszego i ostatniego elementu. Właściwości Fi rst i Last mają takie samo działanie jak Head i Tai l.

Count zwraca

zwracają wartości

602

Visual C++ 2005. Od podstaw

Oiclionary - generyczny slownik przechowuiący pary klucz-warlość Kolekcja generyczna Dict ionary wymaga podania dwóch argumentów typu - pierwszy z nich określa typ klucza, a drugi typ wartości skojarzonej z tym kluczem. Słownik jest szczególnie przydatny w przypadku, gdy dysponujemy parami obiektów , które chcemy przechowywać , gdzie jeden obiekt jest kluczem dostępowym do drugiego obiektu. Przykładem pary klucz-wartość , którą możemy zechcieć przechowywa ć w słowniku , mo że być nazwisko i numer telefoniczny, ponieważ numeru telefonicznego zazwyczaj poszukuje się za pomocą nazwiska jako klucza. Przypu ś ćmy, że mamy zdefiniowane klasy Name i PhoneNumber zaw ieraj ące odpowi adając e sobie nazwiska i numery telefonów . Słow n i k przechowując y pary nazwisko-numer można zdefi n iować w następujący sposób : C===DietionarYA phonebook = genew Diet ionary: A

Dwa argumenty typu to Name" oraz PhoneNumber a zatem klucz jest uchwytem do nazwiska, a wartoś ć uchwytem do numeru telefonicznego. A

,

Nowy wpis do

sł o w n i ka

phonebook można

dodać

w następujący sposób:

Na me name ~ genew Name("Jim". "Jones") : PhoneNumber number = genew PhoneNumber (914. 316. 2233): phonebook->Add( name . number ): II Dodawani e do słown ika pary nazwisko -numer. A

A

Aby pobra ć wartość danego wpisu w słowniku, wanej - na przykład:

należy użyć domyślnej właściwości

indekso-

try {

PhoneNumber t heNumber A

=

phonebook[name ] :

}

eate h(KeyNot FoundFoundException kn fe) A

(

Console: :Write Line(knfe) :

Klucz podawany jest jako wartość indeksu domyślnej właściwości indeksowanej , która w tym przypadku jest uchwytem do obiektu Name. Jeżeli klucz istnieje, zwracana jest jego warto ść . J eż el i klucz nie zostanie odnaleziony w kolekcji, to powodowany jest wyjątek typu KeyNot FoundExcept i on. W związku z tym przy każdej próbie uzyskania dostępu do wartości klucza, który może nie istni eć , kod powinien znajdować się w bloku try . Wła ś ciwo ś ć

śc iwoś ć

keys obiektu Di ct iona ry zwraca kolekcję zawierającą klucze sło w n i ka, a wła­ Val ues kolekcję zawieraj ącąjego wartości . Właściwość Count zwraca liczbę par klucz-

-wartość znajdujących s ię

w słowniku.

Spójrzmy teraz na omawiane zagadnienia w przykładowym programie.

liI!lmI!WI Zaslosowanie kolekcji generycznych Pr zykład

ten ilustruj e u życie trzech omawianych

powyżej

kolekcji generycznych:

Rozdział 9.

• Dziedziczenie i funkcje wirtualne

#i nc l ude "st dafx. h" usi ng namespace Syst em: using namespace Syste m: :Col lect ions : :Generic :

II Dla kolekcj i generycz nych.

II Klasa zawierają ca nazw isk o.

ref class Name {

publ te: NameCSt ring namel , St ri ng name2) : Fi rst Cname1), Second(name2){} virt ual St rinq" ToSt rlng() over ride{ ret urn Fl rst +L" "+Second:} privat e: Str lngA First: St ri ng A Second: A

A

}: II Klasa zawierająca numer telef oniczny ,

ref class PhoneNumber {

publ te: PhoneNumber Cint area. int local, i nt number ): AreaCarea) ,Local Clocal ). Number Cnumber ){} vir t ual Str ingA ToSt r ing() ov erride { ret urn Area +L" "+Local +L " "+Number : } private: i nt Area: int Local : int Number: }:

int main(array Aargs ) (

II

Użyc ie

listy List « I> .

Console: :WriteL ineCL"Tworzenie l ist y l iczb Li st A numbers = gcnew Li st : for Ci nt i = O : iAddC2*i+l ):

ca ł k ow ityc h

Lt st -T> ; " ) :

II Sum owanie zawartoset listy.

int sum = O: for Cint i = O : iCount : i++) sum+= numbers [i J: Console : : ~ r ite L ineCL"Suma = (O)" , sum): II

Użyc ie

listy LinkedList.

Console: :WriteLi neC L" \nTwo rzenie li st y Li nkedLis t podwój nych wart o ś c i :" ): LinkedList Ava l ues = gcnew Li nk edL i st : forC int i = O : iAddLastC 2.5*i) : doub le sumd = 0.0:

&03

604

VisIlai C++ 2005. Od podstaw for each(double v in values) sumd +~ v; Conso le: ;WriteLine(L"Suma

=

{O }", sumd);

Li nkedListNodeA node = va lues->Fi nd (20.O) ; values->AddBefore(node, 19.9); values->AddAfter(values->F ind(30.0), 30.1);

II Znajdź wierzchołek zawierający 20.0.

II Ponownie zsumuj zawartość listy powiązanej.

sumd = O O; for each(double v in values) sumd += v; Console : :WriteLine(L"Suma po dodaniu II

Użycie słownika

wartości

{O }", sumd);

Dictionary,

Console: :Wri t eL i ne(L "\nTworzenie słow nika Dictionary par nazwisko -numer:"); DictionaryA phonebook = gcnew Dictionary; II Dodawanie par nazwisko-numer do

słownika,

Name A name = gcnew Name("Jan" , "Kowals ki"); PhoneNumber A number = gcnew PhoneNumber(914, 316 , 2233 ); phonebook->Add (name, number); phonebook->Add(gcnew Name( "Stanisław" , "Nowa k"), gcnew PhoneNumber(l23, 234.3456) ) ; phonebook->Add Cgcnew Name( "Mari a", "Kwi atkowska" ), gcnew PhoneNumber(515 ,224,6864) ) ;

II Lista wszystkich numerów.

Conso le: :WriteL i neC L"Li sta wszystkich numerów :" ) ; for each(PhoneNumber A number in phonebook->Values) Console: :Wri teLine(number); II Lista nazwisk i numerów.

Console: :WriteLine(L"Uzyskanie do stępu do kluczy w celu s por ząd zen i a listy ws zystkich par na zwisko-numer:"); for each(Name A name i n phonebook->Keys) Console :WriteLineCL"{O } : {l }", name , phonebook[name]); retur n O; Tworzenie listy licz b całkowityc h L1St : Suma = 1000000 Tworzenie l isty LinkedL ist podwójnych Suma = 1248750 Suma po dodan iu wartośc i = 1248800

wartości:

Tworzen ie s łownika Dict ionary par nazwisko-numer: Lis ta wszystkich numerów: 914 316 2233 123 234 3456 515 224 6864 Uzyskanie dost ępu do kluczy w celu sporządzenia listy wszystk ich par nazwisko-numer: Jan Kowal ski : 914 316 2233 Stanisław Nowak: 123 234 3456 Maria Kwiat kowska : 515 224 6864

Rozdzial9. • Dziedziczenie i funkcje wirtualne

&05

Jak lo działa Zwróć uwagę na dyrektywę using names pa ee dla przestrzeni nazw Syste m: : Coll eet i ons: : Gener i e. Jest ona niezbędna , jeżeli chcemy używać generycznych kolekcji z określeniem w pełni zakwalifikowanych nazw klas .

W pierwszym bloku kodu w funkcji mai n() użyta została kolekcja Li st , której kod jest taki sam jak w poprzednim podrozdziale. Tworzy ona klasę przechowującą liczby całkowite w liście, a następnie zapisuje do niej 1000 liczb. Pętla sumująca zawartość listy odszukuj e dane za pomocą domyślnej właściwości indeksowanej. W tym miejscu można by także zastosować pętlę for eaeh. Nie zapomnij - domyślna wartość indeksowana uzyskuje dostęp tylko do elementów znajdujących się już w liście. Za pomocą domyślnej wartości indeksowanej można zmienić wartość istniejącego elementu listy, ale nie można za jej pomocą dodać nowego elementu . W celu dodania elementu na końcu listy należy użyć funkcji Add ( l . Aby wstawić element w określonym indeksie, należy użyć funkcji In ser t( l. Następny

blok kodu w funkcji main ( l demonstruje użycie kolekcji Lin kedL i st do sortowania typu doubl e. Wartości typu doub l e dodawane są na końcu listy powiązanej za pomocą funkcji AddLast( l w pętli f or. Wartości odnajdywane i sumowane są w pętli f or eaeh. Zauważ , że w liście powiązanej brakuje domyślnej wartości indeksowanej dającej dostęp do elementów listy. W kodzie zostały także użyte funkcje Find() , AddBefo r e() oraz AddAfter () w celu demonstracji dodawania nowych elementów w określonym miejscu w liście. wartości

Ostatni blok kodu w funkcji mai n( l pokazuje sposób użycia kolekcji Dict iona ry do przechowywania numerów telefonicznych z kluczami w postaci nazwisk. Klasy Name i PhoneNumber implementują funkcję ToSt ri ng() przesłaniającąjej odziedziczoną wersję w celu umożliwie­ nia funkcji Consol e: :WriteLine () wysłania na wyjście odpowiednich reprezentacji obiektów tych typów. Do słownika phonebook dodano trzy pary nazwisko-numer. Następnie sporządzona zostaje lista numerów znajdujących się w słowniku. Służy do tego pętla fo r eaeh przechodząca przez wartości zawarte w obiekcie kolekcji, zwróconym przez właściwość Va l ues słownika phonebook . Ostatnia pętla przechodzi przez nazwiska w kolekcji zwrócone przez właściwość Keys i uzyskuje dostęp do wartoś ci słownika phonebook za pomocą domyślnej właściwości indeksowanej . Nie ma tutaj potrzeby stosowania bloku t ry, ponieważ mamy pewność, że wszystkie klucze w kolekcji Keys są obecne w słown iku - jeżeli nie są, to oznacza to, że istnieją poważne błędy w implementacji klasy generycznej Diet i ona ry !

Podsumowanie Rozdział

ten omawia najważniejsze zagadnienia dotyczące mechanizmu dziedziczenia w klasach w natywnym C++ oraz w klasach w C++/CLI. Najważniejsze informacje, które nale ży zapamiętać, zostały wyszczególnione poniżej . •

Klasa pochodna dziedziczy wszystkie składowe klasy bazowej z wyjątkiem konstruktorów , destruktora oraz przeładowanego operatora przypisania.

&0&

Visual C++ 2005. Od podstaw •

Składowe klasy bazowej zadeklarowane jako prywatne nie są dostępne w klasach pochodnych. Aby uzyskać efekt podobny do użycia słowa kluczowego pr i vate, ale z zachowaniem dostępu do składowych w klasach pochodnych, należy zamiast niego użyć słowa kluczowego protected.



Kla sę bazową



Pisząc konstruktor klasy pochodnej, należy pamiętać o prawidłowej inicjalizacji zmiennych składowych klasy bazowej, a także klasy pochodnej .



Funkcje w klasie bazowej można deklarować jako wirtualne. Pozwala to na ponowne zdefiniowanie tych funkcji w klasach pochodnych oraz ich wybór w zależności od typu obiektu , dla którego nastąpiło wywołanie funkcji.



Destruktor w klasie bazowej w natywnym C++, zawierającej funkcje wirtualne, należy deklarować jako wirtualny. Zapewnia to wybór odpowiedniego destruktora dla dynamicznie tworzonych obiektów klas pochodnych.



W natywnym C++ jedna klasa może być zaprzyjaźniona z inną klasą. W takim przypadku wszystkie funkcje składowe klasy zaprzyjaźnionej mają dostęp do składowych tej drugiej klasy . Jeżeli klasa Ajest zaprzyjaźniona z klasą B, to nie oznacza to, że klasa Bjest zaprzyjaźniona z klasą A- chyba że tak wynika z jej deklaracji.



dla klasy pochodnej można określ ić za pomocą słowa kluczowego publ i c, pr i vate lub protect ed. W przypadku niepodania specyfikatora domyślnie stosowany jest pri vat e. Poziom dostępu do odziedziczonych składowych może być modyfikowany w zależności od użytego słowa kluczowego dla klasy bazowej.

Funkcja wirtualna w klasie bazowej w natywnym C++ może być czysto wirtualna, na końcu jej deklaracji zostanie umieszczony zapis = O. Klasa taka staje się abstrakcyjna i nie można tworzyć jej obiektów. We wszystkich klasach pochodnych wszystkie funkcje wirtualne muszą być zdefiniowane. W przeciwnym razie są one klasami abstrakcyjnymi.

jeśli



W C++/CLI klasa referencyjna może być pochodną innej klasy referencyjnej . Klasy wartości nie mogą być pochodnymi.



zbiór deklaracji funkcji publicznych reprezentujących klasy referencyjne . Interfejs może zawierać funkcje publiczne, zdarzenia oraz właściwości . Interfejs może również definiować statyczne zmienne składowe, funkcje, zdarzenia i właściwości. Wszystkie one są dziedziczone przez klasę implementującą interfejs . Interfejs to klasa

zawierająca

określoną funkcjonalność, którą mogą implementować



Interfejs może być obu interfejsów.

klasą pochodną innego



Delegat to obiekt zawierający jeden lub więcej wskaźników do funkcji o takim samym typie zwracanym i listach parametrów. Wywołanie delegatu powoduje wywołanie wszystkich funkcji przez niego wskazywanych.



Zdarzenie składowe klasy sygnalizuje zdarzenie poprzez wywołanie jednej lub większej liczby funkcji należących do procedury obsługi zdarzenia, które zostały zarejestrowane dla tego zdarzenia.

interfejsu,

zawierającą składowe

Rozdział 9.•

Dziedziczenie i tunkcie wirtualne



Klasa generyczna jest typem parametrycznym, którego egzemplarze tworzone są w czasie działania programu. Argumenty przekazywane do parametrów podczas konkretyzacji typu generycznego mogą być typami klas wartości lub typami klas referencyjnych.



Przestrzeń



607

nazw System: :Co1 1ect ions: :Generic zawiera kolekcje generyczne, będące klasami definiującymi bezpieczne dla typów kolekcje obiektów dowolnego typu CH /CLI. W C++/CLI można utworzyć bibliotekę klas w oddzielnej asemblacji i umieścić w pliku z rozszerzeniem .dl! .



Omówiliśmy już wszystkie ważniejsze właściwości języków C++ ANSVISO oraz C++/CLI. Bardzo ważne jest, aby biegle posługiwać się mechanizmami definiowania i tworzenia klas pochodnych oraz zrozumieć proce s dziedziczenia w obu tych językach . Programowanie dla systemu Windows w środowisku Visual C++ 2005 wymaga znajomości tych technik oraz umiejętności posługiwania się nimi.

Ćwiczenia Kod źródłowy wszystkich listingów w tej książce oraz rozwiązania do ze strony www.helion.pl. 1 Co jest nie tak z

poniższym

ćwiczeń można pobrać

kodem?

class CBadCla ss (

private : int len: char* p: public: CBadCl ass(con st cha r* st r) pt st r ). len(str len(p) ) {} CBadClass(){} }:

2.

Przypuśćmy, że

mamy

klasy pochodne

reprezentujące różne

poniższą klasę

o nazwie CPta k i chcemy od niej ptaki :

class CPt ak (

prote ct ed: int rozpiet oscSkrzyde l : int wiel koscJaja: i nt predkoscLot u: int wysokoscLotu: publ ic: virtual void lot () { wysokoscLot u ~ 100; } }:

utworzyć

&08

Visuał

C++ 2005. Od podstaw Czy miałoby sens utworzenie klasy pochodnej CJast rzab od tej klasy? A klasy CSt r us? Uzasadnij swoje odpowiedzi. Stwórz hierarchię klas pochodnych, która poradzi sobie z tymi dwoma gatunk ami ptaków.

I.

Mając poniższą klasę :

cl ass CBase

( pr ot ect ed : i nt m_anlnt ; publi c: CBase ( int n) : m_anlnt ( n) { cout « virt ual void Pr i nt ( ) const = O:

"Konstrukto r podstewosyx n" :

i Jakiego rodzaju klasąjest klasa CBa se i dlaczego? Utwórz klasę po chodną klasy CBa se, która będzi e ustawiała wartość swojej odziedziczonej zmiennej przechowującej liczbę całkow i tą oraz drukowała ją na życzenie . Napisz program sprawdzający poprawność Twoj ej klasy.

l

Drzewo binarne to struktura składająca się z wi erzchołków , z których każdy zawiera wskaźnik do lewego i prawego wierzchołka oraz p orcję danych. Struktura ta została przedstawiona na rysunku 9.7. Drzewo ma początek w w ierzchołku pnia, który jest punktem początkowym do uzyskiwania dostępu do wierzchołków drz ewa. Jeden lub dwa w sk aźniki w wierzchołku mogą być zerowe. Drzewo na rysunku 9.7 zostało zorganizowane w taki sposób, że wartość każdego wierzchołka jest zawsze większa lub równa warto ści wierzchołka zn ajdującego się po jego lewej stronie oraz mniejsza lub równa wartości wierzchołka po prawej stronie. Przedstaw w postaci klasy w natywnym C++ takie drzewo binarne przechowujące liczby całkowite. Musisz także zdefiniować klasę Nade, ale może ona być zagnieżdżona w klasi e Bin aryTree. Napisz program t estujący działanie Twojej klasy Bin aryTree poprzez zapisanie w nim określonej sekwencji liczb całkowitych oraz ich odszukanie i wysłanie na wyjście w rosnącej kolejności. Wskazówka: nie bój

s..

s i ę u żywać

rekurencji.

Zaimplementuj ćwiczenie 4. jako program CLR . Jeżeli nie udało Ci tego ćw i c zen i a, to skorzystaj z odpowiedzi.

się ukończyć

8. Zdefiniuj generyczną klasę BinaryTree dla każdego typu, który implementuje interfejs Icamparabl e, oraz zademonstruj, jak działa, popr zez użycie egzemplarzy klasy generycznej do przechowywania i odszukiwania kilku losowych liczb całkowitych oraz elementów poniższej tablicy : ar ray< Strl ng Ą >A wor ds = { L"Suckces" . L"to" . L"przec hodzeni a" . L"od" . L" jedne J " , t' bez". t'utrety". L" entuzjazmu" } :

7.

Wyślij wartości

L" zd o l n o ś ć ". L"po r a ż k i ' ,

L" z a p ał u".

L"do" .

L"do" . L"n ast epnej " . l." a" . L "t a k ż e " .

odszukane w drzewie binarnym do wiers za poleceń .

Rozdział 9.• Wierzchołek Wartość

Dziedziczellie i funkcie wirtualne

pnia

= 120

lewy wskaźnik prawy wskaźnik wierzchołka

I

w ierzchołka

Wierzchołek/

~ Wierzchołek

Wartość=43

Wartość

lewy wska źnik I prawy wska źn ik

lewy wskaźnik I prawy wskaźnik wierzchołka

Wierzchołek null

wierzchołka

wierzchołka

Wartość

= 17

I prawy w skaźni

null

w ierzchołka I

Wart oś ć

null

I

~Wierzchołek Wartość

= 57

I prawy wskaźni k

~Wierzchołek = 24 null

Uporządkowane

Rysunek 9.7

wierz chołk a

~Wierzchołek

/

Wartoś ć

= 437

null

w ierzchołka

~Wierzchołek Wartość =88

null

I

drzewo binarne

null

I

= 766 null

689

610

Visual C++ 2005. Od podstaw

10

Debugowanie

Rozw iązując ć w i cze n i a

w poprzednich rozdz iała ch, prawdopodobnie niera z zdarzyło Ci s i ę z błędami w kodzie. Rozdział ten po święcony jest wykorzystaniu wbudowany ch w Visual C++ 2005 narzędzi wspomagających w tej walce programistę . Poznasz także kilka dodatkowych narzędzi, za pomocą których można znajdować i eliminować błędy z programów. Ponadto nauczysz się kilku sposobów pisania specjalnego kodu w programach, który pomaga stoczyć bitwę

odnajdywać niepraw idłowośc i .

W rozdzi ale tym dowie sz s ię: • Jak uruchomić program pod kontrolą programu uruchomierriowego.Idebuggera) / " wbudowanego w Visual C++ 2005. / "

• Jak

wykonać

• Jak

ś ledz ić

• Jak

ś l e d z i ć warto ść wyrażenia

kod programu instrukcja po instrukcji.

lub zmienia ć

wartości

zmiennych w programa ch.

w progr amie .

• Czym jest stos wywołań . • Czym



asercje i j ak je

• Jak

dodawać

• Jak

wykrywać

s to s ować

kod specjalnie wycieki

do sprawdzania kodu .

wspomagający

pamięci

znajdowanie

błędów.

w programach w C++.

• Jak używać narzędzi ś l e dz en i a wykonywania oraz generować dane programu uruchomieniowego w programach w C++/CLI.

wyjściowe

Co ZnaCZY debugowanie Debugowanie to proces znajdowania i usuwania błędów (ang. bugs) programu. Niewątpliwie wiesz już, że debugowanie j est integralną częścią procesu programowania - j est to zestaw, jak to się mówi, obowiązkowy . Fakty dotyczące błędów w programach brzmią raczej pesymi­ stycznie:

612

Visual C++ 2005. Od podstaw •

Każdy program, który nie jest dziecinnie prosty, zawiera jakieś błędy . Jeżeli chcemy, aby program był niezawodny i efektywny, trzeba wykryć te błędy, znaleźć je i wyeliminować . Zwróćmy uwagę na trzy wymienione fazy . Po pierwsze, błąd w programie wcale nie musi być widoczny od razu . Nawet jeśli jest widoczny, to możemy nie wiedzieć , gdzie się on znajduje w kodzie. A nawet jeżeli z grubsza wiemy , gdzie leży źródło problemu, to i tak możemy mieć trudności z określeniem, co dokładnie jest nie tak, a tym samym z wyeliminowaniem go.

• Wiele programów zawiera wszystkich testów .



błędy

nawet po ich

ukończeniu

i przeprowadzeniu

Błędy

w programie mogą pozostać ukryte. Program taki moż e działać prawidłowo nawet przez kilka lat. Zazwyczaj ujawniają się one w najbardziej nieodpowiednim momencie.

• Programy przekraczające pewne rozmiary i pewien stopień komplikacji zawsze zawierają błędy, bez względu na to, ile czasu i wysiłku poświęcono na ich testowanie. Rozmiar i stopień komplikacji programu, które gwarantują obecność błędów, nie są ściśle określone, ale Visual C++ 2005 i system operacyjny, którego używasz, z pewnością do tej kategori i należą! Jeśli należysz

do tych bardziej nerwowych osób, lepiej nie rozmyślaj zbytnio nad ostatnim punktem, w szczególności jeżeli często latasz samolotami lub regularnie znajdujesz się w po­ bliżu jakichkolwiek proces ów uzależnionych od właściwej pracy komputerów. To mo że się poważnie odbić na Twoim zdrowiu w razie jakiejś awarii . /

błędów

wyłapanych

k~;lidowania

Wiele potencjalnych zostaje w fazach kompilacji i I programu, choć zawsze może kilka z nich się prześlizgnąć, nawet gdy udało się utworzyć moduł wykonywalny programu . Niestety , pomimo że błędy w programach sąnieuniknione, tak samo jak śmierć czy podatki, to debugowanie nie jest rozpoznanądziedziną nauki . Można jednak przyjąć podejście strukturalne do eliminowania błędów. Aby znajdowanie błędów w progra­ mie było jak najmniej bolesne, można zastosować jedną z czterech strategii : • Nie odkrywaj ponownie Ameryki . Postaraj się opanować i stosować narzędzia biblioteczne dostarczane jako część środowiska Visual C++ 2005 (lub komponenty innych komercyjnych produktów, do których masz dostęp) . Dzięki temu program składa się z możliwie dużej ilości już przetestowanego kodu . • Rozwijaj i testuj kod stopniowo. Dzięki indywidualnemu testowaniu każdej ważniejszej klasy i funkcji oraz stopniowemu składaniu programu z przetestowanych komponentów proces tworzenia programu może być znacznie łatwiejszy i pozbawiony wielu trudnych do odnalezienia błędów. • Pisz kod defensywny - to znaczy pisz kod , który broni się przed potencjalnymi błędami. Na przykład deklaruj funkcje składowe klas w natywnym C++, które nie modyfikują obiektu jako typu eonst. Używaj parametrów typu eonst , gdzie jest to uzasadnione. Nie używaj w kodzie tak zwanych magicznych liczb - obiekty stałe definiuj z odpowiednimi wartościami. • Od samego początku dodawaj kod debugujący, który sprawdza i waliduje dane oraz warunki w programie. Przyjrzymy się temu trochę później w tym rozdziale .

Rozdział 10.

• Debugowallie

613

Ze względu na fakt, że bardzo ważne jest, aby programy zawierały tak małą liczbę błędów , na ludzkie możliwości, Visual C++ 2005 dostarcza potężną broń oraz narzędzia służące do znajdowania błędów . Zanim jednak przejdziemy do nich, przyjrzymy się dokładnie procesowi powstawania błędów.

jaką pozwalają

BlędJ OprOgramOwania Oczywiście, głównym źródłem błędów

w programie jest programista. Błędy popełniane przez od zwykłych literówek (wciśnięcie niewłaściwego klawisza) po zastosowanie nieprawidłowej logiki. Mnie też trudno uwierzyć , że robię takie głupie błędy, ale jako że nikt jeszcze nie wpadł na sensowny pomysł, co innego mogłoby być ich źródłem , musi to być prawda. Istota ludzka to stworzenie podatne na przyzwyczajenia, a więc prawdopodob­ nie odkryjesz, że niektóre błędy powtarzasz wielokrotnie. Co gorsze, niektóre błędy są dla innych oczywiste, a dla nas niewidoczne. W ten sposób komputery dają nam lekcję pokory. Błędy , które może popełnić programista, a których skutkiem są błędy w programie, można podzielić na dwa rodzaje : programistę rozciągają się



Błędy składni - powstają w wyniku zastosowania nieprawidłowej składni w instrukcjach. Przykładami mogą tutaj być pominięcie średnika na końcu instrukcji albo użycie dwukropka w miejscu, gdzie powinien być przecinek. Błędami składni nie trzeba się zbytnio przejmować. Kompilator rozpoznaje je wszystkie i zazwyczaj podaje całkiem precyzyjne wskazówki co do źródła problemu, a więc łatwo się ich pozbyć.



Błędy semantyczne - w przypadku tych błędów kod jest poprawny składniowo, ale nie robi tego, czego oczekujemy. Kompilator nie wie, co było naszym zamiarem, a więc nie może wykryć błędów semantycznych. Jednak częstą wskazówką wystąpienia takiego rodzaju błędu jest niespodziewane kończenie działania programu. Przeznaczeniem narzędzi debugowania w Visual C++ 2005 jest pomóc programiście w odnalezieniu błędów semantycznych. Mogą one być bardzo subtelne i trudne do odnalezienia, na przykład gdy program czasami generuje nieprawidłowe wyniki lub ulega nieregularnym przerwaniom. Prawdopodobnie najtrudniejsze są błędy w programach wielowątkowych, w których równoległe ścieżki wykonywania programu nie są właściwie sterowane.

Oczywiście , w środowisku systemowym, w którym pracujemy, także są błędy (włącznie z Vi­ sual C++ 2005), ale jest to ostatnie miejsce, które należy podejrzewać, kiedy program nie chce działać . Nawet kiedy mamy pewność, że to musi być kompilator lub system operacyjny, to w dziewięciu przypadkach na dziesięć nie mamy racji. Oczywiście w Visual C++ 2005 są błędy i jeżeli chcesz być na bieżąco w tych, które znaleziono do tej pory, razem z dostępnymi poprawkami, informacji możesz poszukać na witrynie Microsoftu poświęconej Visual C++ (http://msdn.microsojt.com/visualcl) lub - jeżeli stać Cię na subskrypcję biuletynu Microsoft Development Network - możesz co kwartał otrzymywać poprawki najnowszych błędów.

Dobrze jest zrobić sobie listę błędów odkrytych we własnym kodzie do późniejszego wglądu. Analizując nowy kod pod kątem błędów popełnianych wcześniej, często można znacznie zre­ dukować czas potrzebny do usunięcia błędów z nowych projektów.

614

Visual C++ 2005. Od pOdstaw Z natury programowania wynika, ż e różnorodność błędów jest nieskończona. Ale istnieje kilka ich rodzajów, które są szczeg ólnie często spotykane. Możliwe, że już o nich wiesz , ale i tak zachęcam Cię do rzucenia na nie okiem.

Najczęściej · spotykane blędy Użytecznym sposobem katalogowania błędów jest przyporządkowywanie ich do objawów, jakie wywołują, ponieważ tak wygląda nasze pierwsze zetkn i ę c i e się z nimi. Poniższa lista pię­ ciu często spotykanych objawów nie jest w żadnym przypadku wyczerpująca. Z pewności ą w miarę zbierania doświadczeni a można do niej dodać wiele punktów:

Objaw

Prawdopodobne powodr

Zniekształcone

dane

Nieudana inicjalizacja zmiennej . Przekroczenie

zas ięgu

typu

całkowitego .

Nieprawidłowy wskaźnik. Błędne wyrażenie

indeksowe tablicy .

Bł ąd

warunku

pętli .

Bł ąd

rozmiaru dynam icznie alokowanej tablicy.

Nieudana próba implementacji konstruktora kopiującego klasy, operatora przypisania lub destruktora. Nieprawidłowy wska źnik

Nieobsłu żone wyj ątki

Brak proced ury Program zawiesza

się

lub

załamuj e

obsłu gi

lub referencja.

catc h.

Nieudana próba inicjalizacji zmiennej . N ieskończona pętla .

Nieprawidłowy wskaźnik.

Dwukrotne zwalni anie tej samej

pamięci .

Nieudana próba implementacji lub Nieprawidłowe

błąd

destruktora.

dane ze strumienia

Wczytywanie za pomocą operatora ekstrakcji i funkcji get l mer l.

wyniki -

Błąd

wejściowego Nieprawidłow e

typograficzny:

-=

zamiast

==

lub i zamiast j itd.

Nieudana próba inicj alizacj i zmienn ej. Przekroczenie zas ięgu typu

całkowitego .

Nieprawidłowy wskaźnik. Pominięcie

jak wiele różnego rodzaju błędów mogą spowodować nieprawidłowe oraz jak różnorodne mogą być tego objawy . Są one chyba naj częstszym źródłem trudnych do zlokalizowania błędów, a więc zawsze należy dokładnie sprawdzać wszystkie

Warto

zwrócić uwagę,

instrukcji br eak w instruk cj i swi tch.

wskaźniki

Rozdział 10.

operacje z nimi

• DebuDowanie

615

związane. Dzięki

wskaźników można uniknąć

wiedzy na temat sposobów powstawania nieprawidłowych wielu pułapek. Najczęstszymi powodam i powstawania niepra­

widłowych ws kaźn i kó w są:

• Nieudana inicjalizacja wskaźn ika podczas jego deklaracji. • Nieudana próba ustawienia wskaźnika do obszaru podczas usuwania przydzielonej pamięci . • Zwracanie adresu zmiennej lokalnej przez

pamięci

wolnej na warto ść nu11

funkcję .

• Nieudana próba implementacji konstruktora kopiującego i operatora przypisania w klasach przydzielających pamięć w wolnym obszarze. Nawet jeśli dopilnujemy wszystkiego, o czym mowa powyżej, w kodzie nadal pozostaną błędy . Przyjrzyjmy się zatem narzędziom dostarczanym przez Visual C++ 2005 w celu usprawnien ia procesu ich znajdowania.

Podstawowe operacje debugowania Do tej pory wielokrotnie tworzyliśmy wersję testową tworzonych programów, ale mimo tego nie używaliśmy debuggera. Debugger to program pozwalający na kontrolę wykonywania pro­ gramu poprzez wykonywanie kodu wiersz po wierszu lub tylko jego określonej części. W każ­ dym punkcie zatrzymania dcbuggera, można sprawdzić, a nawet zmienić wartości zmiennych przed kontynuowaniem operacji. Można także zmienić kod źródłowy , skompilować go ponow­ nie, a następnie jeszcze raz uruchomić program od początku . Kod źródłowy można modyfiko­ wać nawet podczas wykonywania programu krok po kroku. Przed przejściem do następnego kroku po pojawieniu się modyfikacji w kodzie debugger automatycznie ponownie kompiluje program przed wykonaniem następnej instrukcji . podstawowe możliwości debugowania Visual C++ 2005 , dostępnego w tym debuggera użyjemy z programem , co do działania którego jesteśmy raczej pewni. Następnie będziemy mogli tylko pociągać za d źwignie, aby zobaczy ć , jak wszystko działa. Weźmy prosty przykład z rozdziału 4., w którym użyto wskaźników: Aby

zrozumieć

środowisku

II Cw4 05.cpp

II Ćwiaen ie użycia wskaźn ików.

#incl ude·

us ing std: :CDUt ;

us i ng st d: : endl:

using st d: .hex:

usi ng st d: .dec :

tnt mairu ) {

long* pnumber = NULL : long numberl = 55. number2 pnumber = &numberl : *p number += 11 ;

II Deklaracja i inicjalizacja wskaźnika .

~

99 :

II Zapisyw anie adresu do wskaźnika . II Zwiększen ie zmiennej numb eri o 11.

616

Visual C++ 2005. Od podstaw cout « endl

« "numberI = " « number l

«" &numberl = « hex« pnumber;

pnumber = &number 2 ; numberI = *pnumber*lO ;

II Zmiana wskaźnika na adres zmiennej number2. II Pomnożenie zmiennej numb er Z przez /0 .

cout « endl

« "number l = «dec « numberl

« pnumber = " « hex « pnumber

« *pnumber ~ " « dec « *pnumber ;

cout « endl ;

r et urn O:

Jeżeli masz ten projekt w swoim systemie, to wystarczy go teraz tylko nym razie trzeba będzie go wprowadzić jeszcze raz.

otworzyć.

W przeciw­

Kiedy piszemy program, który nie zachowuje się tak jak powinien, debugger pozwala na jego prze śledzenie krok po kroku w celu odnalezienia źródła i natury problemu oraz analizy stanu danych programu na każdym etapie wykonywania. Kod z powyższego listingu wykonamy instrukcja po instrukcji w celu prześledzenia zawartości interesujących nas zmiennych . Tym razem chcemy przyjrzeć się wskaźnikowi pnumber , treści przez ten wskaźnik wskazywanej (* pnumber) oraz zmiennym number l i number2. Najpierw należy się upewnić , że program został skompilowany w konfiguracji Win32 Debug, a nie Win32 Release (konfiguracja Debug jest domyślna, chyba że ją sami zmienimy). Konfi­ guracja kompilacji programu korzysta z ustawień, które można obejrzeć, wybierając z menu opcję Project/Properties . Bieżąca konfiguracja kompilacji projektu widoczna jest w dwóch znajdujących się obok siebie listach na standardowym pasku narzędzi. Aby włączyć lub wyłą­ czyć określony pasek narzędzi, należy prawym przyciskiem myszy kliknąć pasek standardowy, a następnie zaznaczyć wybrany pasek na li ście lub usunąć jego zaznaczenie . Upewnij się , że pasek narzędzi Debug jest włączony. Pojawia się on automatycznie w momencie rozpoczęcie pracy debuggera, ale dobrze by było zapoznać się z nim wcześniej - zanim uruchomimy debugger. Konfigurację kompilacji można zmienić poprzez rozwinięcie listy rozwijanej i wy­ branie innej konfiguracji. Można do tego celu użyć opcji menu Build/Configuration Manager . Standardowy pasek narzędzi pokazano na rysunku 10.1. - !Wi132

' i1deks

Rysunek 10.1 do czego służą poszczególne elementy paska narzędzi, wystarczy na nie kursorem myszy. Wtedy pojawi się chmurka z objaśnieniem funkcji poszczególnych przycisków.

Aby

dowiedzieć się ,

najechać

Konfiguracja Debug powoduje dodanie podczas kompilacji do modułu wykonywalnego pro­ gramu dodatkowych informacji, które umożliwiają późniejsze użycie narzędzi debugowania. Informacje te przechowywane są w pliku z rozszerzeniem .p db znajdującym się w folderze Debug projektu . W wersji ostatecznej brak tych informacji , gdyż stanowią one niepotrzebne

Rozdzial10.• Debugowanie

617

dodatkowe obciążenie , zbędne w wersji finalnej programu . Visu al C++ 2005 w wersji Pro­ fessionallub Enterprise optymalizuje nawet kod programu podczas kompilacji wersji osta­ tecznej. W wersji testowej optymalizacja nie jest wykonywana, gdyż może być związana ze zmianą kolejności elementów kodu źródłowego w celu zyskania na efektywności lub nawet całkowitym pomijaniem zbędnych partii kodu. Ze względu na fakt, że optymalizacja zaburza odwzorowanie w kodzie maszynowym kodu źródłowego , może ona spowodować co najmniej zamieszanie przy uruchamianiu programu krok po kroku . Pasek

narzędzi

Debug pokazano na rysunku 10.2.

Rysunek 10.2

Debuq "

Sprawdzając

.' .

','

.~

.... :. .y,:'-x '

·l

chmurki podpowiadające przeznaczenie poszczególnych przycisków paska, można do czego służą - niektórych z nich niedługo będziemy używać. Przy użyciu przykładowego programu z rozdziału 4. nie uda nam się przetestować wszystkich narzędzi debugowania, ale wypróbujemy niektóre ważniejsze z nich. Po zapoznaniu się z tech­ niką uruchamiania programu krok po kroku za pomocą debuggera nauczymy się korzystać z innych jego narzędzi na programie zawierającym już błędy. wstępnie dowiedzieć się

Oebugger można uruchomić, klikając pierwszy po lewej przycisk na pasku narzędzi Debug i wybierając z menu Debug/Start Debugging lub naciskając klawisz F5. W tym przykładzie sugeruję użyć przycisku z paska narzędzi . Oebugger może działać w jednym z dwóch try­ bów - wykonując kod krok po kroku (co zazwyczaj oznacza wykonywanie kodu instrukcja po instrukcji) lub wykonując kod do określonego miejsca. Miejsce, w którym debugger powi­ nien się zatrzymać, można oznaczyć poprzez umieszczenie w nim kursora lub w bardziej uży­ teczny sposób poprzez wyznaczenie tzw. punktu wstrzymania (ang. breakpoint). Zobaczmy, jak definiuje się punkty wstrzymania.

Ustawianie punktów wstrzymania Punkt wstrzymania to miejsce w programie, w którym debugger automatycznie zawiesza wykonywanie. Można określić wiele punktów wstrzymania i uruchomić program, za­ trzymując go w interesujących , wybranych przez nas miejscach. W każdym punkcie wstrzy­ mania można podejrzeć zawartość zmiennych w programie i ją zmienić , jeżeli nie zawierają wartości takich jak powinny. Program z projektu Cw4_05 będziemy uruchamiali po jednej instrukcji, ale w przypadku dużych programów podejście takie nie jest praktyczne . Zazwyczaj zachodzi potrzeba przyjrzenia się określonej partii programu, co do której są jakieś podej­ rzenia. W związku z tym punkty wstrzymania zazwyczaj ustawia się w miejscach, w których jest podejrzenie wystąpienia błędu. Uruchomiony program zatrzymuje się w miejscu ozna­ czonym przez pierwszy punkt wstrzymania. Następnie dalszą część kodu można wykonywać instrukcja po instrukcji.

Aby ustawić punkt wstrzymania dla określonej instrukcji, wystarczy na początku wiersza kodu źródłowego kliknąć w szarej kolumnie po lewej stronie numeru wiersza. Pojawi się okrągły czerwony symbol zwany glifem sygnalizujący obecność punktu wstrzymania w tym

618

Visual C++ 2005. Od podstaw wierszu. Aby punkt wstrzymania usunąć , należy dwukrotnie kliknąć symbol glifu. Na rysun­ ku 10.3 widoczne jest okno edytora z dwoma punktami wstrzymania zaznaczonymi w pro­ gramie Cw4_05.

05.cppL

Cw4

(Global 5cope)

lii

~I

-

l ;I I I

CM4_ 0 S . c PP

~

Ć ~ i c z e n l e uz y c i a ", "k a Źn lkó~.

3 'l

# l nc lud e

.-

... x ­

t;l

"'mainO

=!~:

< io~ t r e am>

'I

us t n q nemeapece 5t d ;

5

6· - int 7 (

O

main ()

Lo nq" p numbe r

8 9 10 11 12 13

I o nę

nUl\"lber l

p n umb e r

.

.

-

NULL ;

55. n umbe r

«

1/ De k l arac ja i i n i c J ::Il i za c j a

.

II ż a p t evva me ad r e s u do tlskai n i ka . II ZtJięk:!lzFni.:: zn'l1e n ne:J numbe r j. o 1lo

. . c-c .

& m .unb er 1

li :

u e k e ź n t ka .

99 ;

&nUIl1b e:r 1 ;

e o u t -c e e nd! « " n umbe r t

15

.

+= 11 ;

TJ:lnumbe r

H

ż

=

L

n wnb er l

«

he K «

pn1..UTll::l er ;

h~ J

O

I I zma a n e, tJs kainika n: "SI. actr e a znu en ne j n nmb e r 2 . I I P om n o ż e n i e zmi e n ne j nurobe r ż pcz. ez 10 .

= &n umb e r 2 ; number 1 = .., p m .unb e r '"" 10 :

17 " 18 19

20

pnumo e r'

co ut; names[j] ) ph rase = " wię c ej n i ż "; el se i f( names [ i] ~= names[j ] ) II Zbyt eczna, ale wywolujefun kcję op erator==(). phrase = " równe "; j Name = new char[names[ j]. get NameLengt h()+1] ; II Tablica przechowująca nazwisko . cout « endl « names[i] .getName(iName) « " jest" « phrase « names[ j ] .get Name(j Name) : cout « endl :

return O:

Funkcja i ni t ( ) pobiera następujące po sobie kombinacje imion i nazwisk z tablicy nazw w celu inicjalizacji obiektów tablicy Name. Nazwy są generowane w grupach po 25, ale nam tutaj potrzeba tylko la .

Rozdzial10.• DebuDowanie

Odnajdywanie następnego

641

błędu

Jeżeli

uruchomimy program pod kontrolą debuggera za pomocą przycisku Start Debuging na pasku narzędzi, to ulegnie on znowu załamaniu . Ukaże się okno widoczne na rysunku 10.18. znajdującego się

Ryslmek 10.18

M1Ct OSOrt

vrsuel Studio

-

~

.

,\ lklhondled except jon ot OxOOi l lb69 in CwIO_win.oxo : OXCOOOOOFD:

..lJ stockovorf1aw .

Komunikat w oknie informuje , że została przekroczona ilość dostępnej pamięci na stosie . Jeżeli klikniemy przycisk Break, to w oknie Cal! Stack będziemy mogli zobaczyć, co spowodowało tę sytuację . W kodzie znajdują się następujące po sobie wywołania funkcji ope retor-O , a więc musi ona wywoływać samą siebie. Jeśl i zajrzymy do kodu, to zobaczymy dlaczego : jest lite­ rówka. Ten wiersz w ciele funkcji powinien wyglądać następująco:

ret urn name


Enabled) Debug: : W r H e L i n e l L" B ł ąd w funkcji Do l tt )" ) : II

Więcej

kodu...

} II Reszta klasy ...

}: Powyż szy kod pokazuje obiekt er ro rs jako statyczną składową klasy M yCl ass . Pierwszy argument przekazywany do konstruktora Boa l eanSwitch to nazwa przełącznika używan ego do inicjalizacji właśc iwości Di spl ayName, a drugi argument ustawia wartość właściwości Descr i pt i on przełącznika . Jest jeszcze jeden konstruktor przyjmujący trzeci argument typu St r i nq" , który ustawia właściwo ść Val ue przełącznika. Właściwość

fa l se . Aby

Enabl ed przełącznika Boal eanSwi t ch je st typu logicznego i domy ślnie ma warto ś ć na true, należy odpowiednio zmienić jej wartość :

ustaw ić ją

errors ->Enabled = true: Funkcja Do l t O w klasie MyClass wysyła komunikat debugowania o padku , gdy przełącznik e r rors jest włączony.

błędzie

tylko w przy-

Klasa referencyjna Tr aceSwi t ch ma dwa kon struktory, których parametry s ą takie same ja k kon struktorów klasy Boa l eanSw i t ch. Obiekt klasy TraceSwit ch tworzymy w n ast ępujący sposób:

654

Visual C++ 2005. Od podslaw TraceSwitch t raceCtrl A

gcnew TraceSwitch(L"Update".

L" Śl edzi

operac je

uaktualniania." ): Pierwszy argument tego konstruktora ustawia wia wartość właściwości Descript i on. Właściwość

Di splayName, a drugi usta-

Level obiektu TraceSwitch jest typu klasowego wyliczeniowego TraceleveL Wła­

ściwość tę można ustawiać

wych

wartość właściwości

na jedną z

poniższych wartości

w celu kontroli danych

wyjścio­

śledzenia:

Wartość

Opis

Tracelevel : :Off

Brak danych

Tracel evel : :Info

Komunikaty informacyjne,

Tracel evel : :W arning

Ostrzeże,nia

Tracel evel : :Error

Komunikaty o błędach .

Tracel evel : .Verbose

Wszystkie rodzaje komunikatów .

śledzenia . ostrzeżenia

i komunikaty o błędach .

i komunikaty o błędach.

Podana wartość określa rodzaj generowanych danych. Aby otrzymać komunikaty wszystkich rodzajów, należy właściwość ustawić w następujący sposób:

traceCtrl->Level = Tracel evel : :Verbose:

To, czy komunikat

określonego rodzaju powinien być wysyłany przez nasz kod śledzący i debugujący, określamy za pomocą sprawdzenia stanu jednej z czterech właściwości typu logicznego obiektu TraceSwitch:

Właściwość

Opis

TraceVerbose

Zwraca

wartoś ć

t rue, gdy wszystkie rodzaje komunikatów mają być wysyłane.

Tracelnfo

Zwraca

warto ść

t rue, gdy komunikaty informacyjne mają być wysyłane.

TraceWarning

Zwraca

warto ść

t rue, gdy

TraceError

Zwraca

wartość

t rue, gdy komunikaty o błędach

ostrzeżenia mają być wysyłane .

mają być wysyłane .

Ze znaczenia wartości tych właściwości widać, że ustawienie właściwości Level oznacza również ustawienie stanów tych właściwości . Jeżeli na przykład właściwość Level ustawimy na TraceLevel : :Warni nq, to właściwości TraceWarni nq i TraceError zostaną ustawione na true, a TraceVerbose i Trace Info na fa l se. Aby

zdecydować,

czy

wysłać

na

wyjście określony

komunikat, wystarczy

sprawdzić

odpo-

wiednią właściwość:

if (t raceCt r l- >T raceWarning) riteLtnett.To jest ost at nie Debug : :W Powyższy

komunikat zostanie

t raceCtrl ma

wartość

true.

wysłany

ost rz eż eni e!") :

na

wyjście

tylko wtedy, gdy

właściwość

TraceWarning

Rozdział 10.

• Debugowanie

655

Asercie Klasy Oebug() i Assert () zawierają statyczną funkcj ę Asser t ( ), której możliwoś ci podobne są do funkcji Assert () w natywnym C++. Pierwszym argumentem funkcji Debuq: :Assert () jest w artość logiczna lub wyrażenie , które zmusza program do wygenerowania asercji, kiedy argument ma warto ść f al se. Stos wywołań jest pokazany w oknie widocznym na rysunku 10.20.

Rysunek 10.20

Assertion Failed: Abort=Quit, Retry=Debug, Ignore=Continue

at TraceTesl,Fu nc O d: \translat irn s\hel ion\ ivor hor1ons vlsUal c++ 200S-"'rzykładY\c:W 10_0 3\CW 10_03\c:w lO _03 . c pp (50)

at Tr aceTestFunB O d :\translations\h ellon\ ivor hor1Dns vlsual c++

2Ó05-..,rzyklady\c:w l O_03\cwlO_03\cwlO_03.cpp(39) at TraceTesl,FunA O d:\trans latlons\he lion\ivo r hOr tons vlsUal c++

2005\przyklady\c:w1O_03\c:wl O_!13\cw lO_03.cpp(213) at , main(5 tr ing[J args) d:\translatirns\h ehon\r,.or horton s visual c++

2005\przyklady\c:wl O_03\c:w 10_!13\cwl O_03.cpp(l7) . at .mainCRT5 tar tup5 trArray(5tr h:jll argurrents) f :v-tm\ vctools\c:rCbld\seICx 86\c:rt\sr c\ mcrlE xe.cpp(324)

Na rysunku 10.20 pokazano asercję wygenerowaną przez n astępny przykład. Kiedy program generuje asercję dla stosu wywołań prezentowanych w oknie dialogowym , to pokazuje numery wierszy w kodzie oraz nazwy funkcji , które są w tym momencie wykonyw ane. W tym przypadku, wł ącznie z funkcją mai nt ), wykonywane są cztery funkcje . W przypadku wystąpienia asercj i możliwe s ą trzy kierunki działania . Kliknięcie przycisku Przerwij spowoduje natychmiastowe zamknięcie programu, kliknięcie Ignoruj pozwala programowi kontynuow a ć działan ie, a Ponó w prób ę umo żliwi a wykonanie programu w trybie debugowani a. Dostępne są

trzy

przeładowane

wer sje funkcji Asser t ( ) :

Funkcja

OpiS

Oebug : :Assert ( bool warunek )

Je żeli

warunek ma wartość f al se, pojawi a stos wyw ołań.

się

okno dialo gowe

pokazuj ące bieżący

Debug : :Assert (bool warunek . St ri ng

A

komunikat) Debug : :Asse rt Cbool warunek . St ri ng komunikat , St r i ngA s zczegó ły ) Najłatwiej

kładowego

A

Podobnie j ak pow yżej, ale z komun ikatem w okn ie dialogowym nad informacją o stosie w ywoł ań .

Podobnie jak powyższa wer sja , ale z dodatki em w okni e dial ogowym.

jest to zrozumieć, patrząc na działający przykład. Poniżej znajduje programu, pokazujący kod debugowania i ś l edzen i a w akcj i.

s zczegółów

si ę

kod przy-

656

Visual C++ 2005. Od podstaw

RI1mmiI Slosowanie kodu debugowania i śledzenia

Poniższy program służy tylko jako ćwiczenie niektórych funkcji debugowania i ś ledze nia, które opisałem powyżej. Utwórz nowy projekt konsolowy CLR i w pliku Cw10_03.cpp um ieść nas tępującą tre ś ć:

#i ncl ude "s t dafx. h" using namespa ce Syst em; usi ng namespace Syst em: : Oiagnost i cs: publ i c r ef class Tr aceTest

( publ ic: TraceTest( i nt n) :v alue(n ) { } proper t y Tr acelevel Level

{ VOl d set (T r aceLevel 1eve1) {sw->Leve1 = 1eve1; } Trace l evel get (){ re t ur n sw->Level : }

voi d FunA( )

{ ++val ue; Tr ace: : I ndent O: Tr ace : :Writ eL l n e ( L " P o c z ą t e k f unkcj i FunA. " ) ; if ( sw- >Tr acelnfo) Oebug : :Wr iteLi ne(L "Funkcj a FunA w t rakc le dzi a ła nia , .. U) ; FunB() : Tr ace: :WriteL i net L" K o ń c z en i e fu nkcj i FunA." ) . Tr ace : :Uni ndent ( ) ;

void FunB()

( Tr ace . : I ndent O: Tra ce: :Wr iteL i n e( L "P o c ząte k funk cji FunB." ) ; i f( sw->TraceWa rn ing) Oeb ug:: W riteL i ne( L "O s trzeżen ie w f unkcji FunB.. . FunC( ) ; Tr ace : ,Writ e L i neCL"K oń cz en ie f unkcj i FunB. " ) ; Trace : .Uni ndent ( ) ;

voi d FunC() ( Tr ace: : I ndent O : Tr ace : :Wr iteLi neCL" P o cz ąt e k fu nkcj i FunC." ) ; i f(s w->Tr aceError ) Oebug: : Writ eL ine (L" Bł ą d w fu nkcji FunC.. . " ) ; Debug : :Asser t ( val ue < 4) ; Tr ace : : W r it e L i n e ( L " Ko ń c z e n i e fu nkcj i FunC." ) ;

U )

;

RozdziallO. • Oebugowanie

657

Trace: .Urn ndent O: }

pri vate: int value; static TraceSwit chA sw = gcnew T r a c e Sw l t c h (L " P r z eł ą cz ni k Sledzeni a. wyjS ciowe ś ledz en ia . "l:

L" Kont roluje dane

}:

int ma i n(array Aargs) i II Skierowani e danych

wyjś cio wych

do wiersza poleceń .

TextWrite rTraceL1st ener A l ist ener Debug : ;Li steners->Add(l iste ner l :

~

Debug: : Indent Size = 2: arrayA level s Trace'lest " obj

=

gcnew TextWri t erT raceL ist ener( Console' :Out ) ; II Ustawianie rozmiaru

wcięcia .

Tracel evel: :Off , Tracelevel : :Error , Tracelevel : :Warning ,Tracelevel : :Verbose}; gcnew TraceTest( O): , =

Console: : W r it e Lin e (L " P oc z ą t e k te stu debugowania i ś Iedzenia . . . ") : for each(TraceLevel level m levels ) {

obj ->Level = l eve l : II Ustawianie pozi omu dla komunikatów. Conso le: :Writ eLine(L"\nPoziom ś ledzenta t o {O}" , obj -r-l.evel l : obj ->FunA() : ret urn O' }

Uruchomienie tego programu powoduje pojawienie s ię okna asercji. Możemy w takim przypadku kontynuować bądź przerwa ć działan ie programu lub uruchomić go ponownie w trybie debugowania, wybierając odpowiedni przyci sk w oknie . W zależności od tego, co zrobimy w momencie szego programu jest następujący: P oc z ąt e k

Poziom

test u debugowania i

to Off funkcj i FunA . Poc ząte k funkcji FunB . P o c z ą t ek fun kc j i FunC . Ko ń cze n ie funkcj i FunC. Końc ze n ie funkcJ1 FunB. Ko ńcz en ie funkcj i Fu nA, ś l edz e nia

Po c z ąt e k

PoziomSledzenia to Error Poc zątek funkcj i FunA. Po c z ą t e k funkcj i FunB . P o c z ą t ek funkcj i FunC. Błą d w funk cj i FunC. . . Ko ń c z en i e funkcji FunC. Koń c z e nie funkcj i FunB. K o ń cze nie fun kcj i FunA.

ś le dze nia

.. .

wyświetlenia

asercji, rezultat

działania powyż­

658

Visual C++ 2005. 011 podstaw Poziom ś led zen i a to W arning Po c zątek funkcj i FunA. Poc z ąt e k funkcji FunB. Ost r ze że n ie w fu n~ c j i FunB. . . P oc z ąt e k fu n ~c j i FunC. B łą d w funk cj i FunC.. Ko ńcz e ni e funkcji FunC . Ko ń czeni e fun kcj i FunB . Ko ń cz en i e fu n ~ c ji FunA. Poziom ś l ed z en i a t o Verbose Po c z ą t e k funkcj i FunA. Funkcj a FunA w tra kcie d zi ał a n i a .

Poc z ą t e k funkcji FunB. O s t rz eż e n i e w funkc j i FunB. . . P oc zą t e k fun~c j i FunC. B ł ąd w funkcji FunC . . .

Jak to IJziała Klasa Tr aceTest defin iuje trzy funkcje obiektowe: FunA(), FunB( ) i FunC ( ). Każda z tych funkcji zawiera wywołanie funkcji Trace: : Indent( ) zwiększającej wcięcie danych debugowania oraz wywołanie funkcji Debug: :Wr iteLi ne() śledzącej początek i koniec każdej funkcji . Funkcja Tr ace: :Uni ndent () jest wywoływana bezpośrednio przed wyjściem z każdej funkcji w celu przywrócenia wcięcia do poziomu sprzed wywołania funkcji . Klasa Tr aceTest definiuje prywatną składową klasy TraceSwitch o nazwie sw, która kontroluje poziom wyświetlanych danych debugowania. Każda z tych trzech funkcji wywołuje również funkcję Debug: :W rit eLi ne() w celu wydania komunikatu w zależności od poziomu ustawionego w s k ła d ow ej sw. Funkcja FunA( ) zwiększa wartość będącą składową obiektu klasy za każdym razem , gdy jest wywoływana , a funkcja FunC () generuje asercję, jeżeli wartość przekracza 3. W funkcji mai n( ) tworzony jest obiekt TextWri te r Tr acel i ste ner , który kieruje dane debugowan ia i śledzen ia do wiersza poleceń:

TextWri t erTraceL i st ener A l i st ener = gcnew Text Writ erTraceL i st ener ( Console: :Out J: Nast ępnie

do kolekcji obiektów

nasłuchujących w

klasie Debug dodajemy obiekt Li stener:

Debug: :L1 st eners->Add(l i st ener ): Powoduj e to, że dane w yj ś c i o w e debugowania i śledzenia strumienia wyjś ciow ego - Consol e: :Out . Tworzymy tablicę obiektów klasy Tr aceLeve l , które danych wyj ściowych debugowania i śled zeni a :

arr ayA level s

= (



kierowane do standardowego

re pre zen tuj ą różne

poziomy kontroli

Tracel evel : :Off. Tracel evel . :Error. Tra cel evel : :W arni ng .TraceLevel : :Verbose) :

Po utworzeniu obiektu klasy TraceLeve l j ego poziom

śledzenia

ustawiany jest w pętli f or each:

Rozdział 10.

f or each (T rac et.evel l evel

l

• Debugowanie

659

n l evel s J

{ obj ->Level ~ l evel ; II Ustawianie p oziomu dla komun ikatów. Con sol e: :Writ eLi ne( L"\ nPozi om ś led ze n i a t o {D} ". obj c- Level ) : obj - >FunA() :

Poziom ustawiany je st poprzez właś ciwoś ć Level obiektu obj. Powoduje to ustawienie wła­ ś c iwoś c i Level w składowej klasy TraceSwitch sw, która jest używana w funkcjach obiektowych do kontroli danych wyj ści owych . Z danych na

wyj ś ciu

wynika,

że

ustawione

w ci ę cie

ma wpływ na dane generowane przez funk-

cj ę

WriteLi ne( ) zarówno w klasie oebug, jak i Trace. W ida ć także sposób, w j aki poziom ustawiony w s kład ow ej klasy TraceSwi t ch wpływa na dane wyjściow e . Kiedy zmienna skł adowa obiektu klasy TraceTest obj os i ąga wartoś ć 4, funkcja FunC() generuje asercję. Uruchom program kilkakrotnie, aby logowym asercji.

sprawdzić

efekt

kliknięcia każdego

z trzech przyci sków w okni e dia-

Podsumowanie Debu gowanie jest bardzo obszern ym zaga dnieniem. W Visual C++ 200 5 dostępnych j est o wiele więcej narzędzi wspomagających ten proces, ni ż przed stawiłem w tej książc e . Jeżeli udało Ci s i ę dobrze opanować zagadnienia opisane przeze mnie w tym rozdziale, to nie będz ie sz mieć problemów z poszerzeni em tej wiedzy za pom ocą dokum entacji Visual C++ 2005 . Wpisując do wyszukiwarki s łowo " debugowanie" , możn a zna leźć wiele dodatk owych informacji. Poni żej



znajduje

s ię

lista

najw ażniejs zych zagadni eń

poruszanych w tym rozdziale :

Funkcj i bibliotecznej asse rt( ) zadeklarowanej w pliku n agłówk owym do sprawdzania warunków logicznych, które zawsze powinny m ieć wartość t rue w programach w natywnym C++ . można używać



Symb ol preprocesora _NoEBUGj est automatycznie zdefiniowany w wersji testowej programu w natywn ym C++. Brak jego defini cji w wer sji ostate cznej.



Można d odać własny kod debu guj ący dla symbolu _NOE BUG, który zamy ka s i ę pomi ędzy dyrektywami #i fdef oraz #endi f . Kod tak i do łączan y j est tylko do

wersji testowej programu . •

Plik n a główkowy crtdbg.h dostarcza deklaracj e funkcj i um ożliwiający ch debu gowanie operacji na obszarze wolnej pamięci.



Odp owiednio u stawiaj ąc zna cznik _crtDbgFl ag, mo żn a włąc zyć automatyczne sprawdzanie programu w celu znalezienia wyc ieków pamięci .



W celu sk ierowania w odpowiednie mi ej sce komunikatów z funkcji debuguj ących obszar wolnej pamię ci należy wywołać funkcje _CrtSetReportMode( ) i Jrt SetReportFi l e( ).



Operacj a debu gow ania przy użyciu punktów wstr zymani a i punktów w C++/CLI przeb iega w taki sam sposób jak w natywnym C++.

ś ledzen ia

660

Visual C++ 2005. Od podstaw •

Klasy Debug i Assert zdefiniowane w przestrzeni nazw Syst em: :Di agnost i es dostarczają funkcji śledzących wykonywanie programu i generowanie danych debugowania w programach CLR.



Statyczna funkcja Assert () w klasach Debug i Traee umożliwia asercje w programach CLR.

Dysponując wiedzą dotyczącą debugowania, możemy jemniczenia: programowania dla systemu Windows!

przejść

do

najwyższ ego

stopnia wta-

11 Zaloienia programowania dla systemu Windows W tym rozdziale poznasz podstawowe koncepcje, które mają zastosowanie w każdym programie dla systemu Windows utworzonym w C++. Zaczniemy od utworzenia bardzo prostego przykładu , korzystającego bezpośrednio z API systemu operacyjnego Windows. To pozwoli Ci zrozumieć, co dzieje się "pod powierzchni ą" aplikacji dla Windowsa . Wiedza ta przyda się, gdy będziesz tworzył aplikacje za pomocą bardziej wyrafinowanych narzędzi dostępnych w Visual C++ 2005. Następnie zobaczysz, co można uzyskać, tworząc program dla Windowsa z wykorzystaniem Microsoft Foundation Classes, bardziej znanych jako MFC. Na koniec, korzystając z biblioteki Windows Forms (formularzy Windows), utworzysz podstawowy program, który będzie uruchamiany w CLR. A zatem po przeczytaniu tego rozdziału będziesz wiedział, na czym polegają te trzy podej ścia do tworzenia aplikacji dla Windowsa. W tym rozdziale nauczysz się: •

Podstawowej struktury okna.



API systemu Windows oraz sposobów jego wykorzystania.



Komunikatów Windowsa oraz sposobów ich obsługi .



Notacji stosowanej w programach dla systemu Windows.



Podstawowej struktury programu dla systemu Windows .



Jak



Microsoft Foundation Classes.



Podstawowych elementów programu opartego na MFC.



Biblioteki Windows Forms.



Podstawowych elementów aplikacji

utworzyć

prosty program, korzystając z API Windows, oraz jak on

korzystającej

z Windows Forms.

działa.

662

Visual C++ 2005. Od podstaw

Podstawy programowania dla systemu Windows W Visual C++ 2005 dostępne

są trzy

sposoby tworzenia interaktywnej aplika cji dla Wind ows a:



Korzystani e z AP! Windows. Je st to podstawowy interfejs sys temu operacyj nego Win dows, um o żli wiający komunikację mi ędzy sys temem a ap l i ka cj ą wykonywaną pod j ego ko ntro l ą.



Kor zystanie z bibl ioteki Microso ft Foundation C lasses, bard ziej znanej jako MFC. Jest to zes taw klas C++, który obej muje W indows API.



Korzystan ie z Windows Forms. Je st to op art y na formul arzach sposób tworzenia aplikacj i uruchamian ych w C LR.

Te trzy spo sob y zo stały wym ienione w kol ejn oś ci od wy magając e g o najwięks zego nakł adu prac y podczas programowania do wy m agaj ącego najmniej. Kor zystanie z Windows APT wymaga napisania całego kodu - mu sisz sa m n ap i s a ć kod dla wszystkich eleme ntów tworzą­ cych graficzny interfejs u żytkownika (GUT) . Przy tworzeniu aplikacji MF C otrzy muj emy p ewną pomoc dzięki wbud ow anemu GU l, w którym w formularzu wybieramy kontrolki, a nas tęp n i e programujemy jedynie interakcj e z użytkown ik iem ; jednak wc i ąż wyrnaga to napisania sporej ilości kodu. Przy tworzeniu aplikacj i z wyko rzystaniem Windows Forms można utworzyć cały GW, włącznie z głównym okne m aplikacji, graficzn ie gromadząc kontrolki, z których korzysta użytkownik. Nal eży j edyni e umie ś ci ć kontrolki w wybranych przez siebie miejscach formularza okna, a kod , który będzie j e tworzył , zos tan ie au tomatyc znie wyge ne rowany. Korzystanie z Window s Form s jest j ak dotąd najszy bszy m i najprostszym sposobem tworzenia aplika cji, ponieważ il o ść kodu , który n al e ży samo dzielnie napisać, jest zde cydowanie mniejsza ni ż p rzy pozostałych dwóch met odach. Kod dla aplikacji Windows Fom1S kor zy sta rów nież ze wszystkic h zalet wykonywa nia w CLR . Korzystanie. z MFC wy mag a więcej wy siłku ni ż używanie formularzy Wind ows , j edn ak daje w ięk sz ą kontrol ę nad sposo bem, w jaki tworzony je st GUI, a także tworzy program uruch amiany natywnie na Pc. P oniewa ż bezpo średnie korzystanie z Windows API j est naj bardzi ej pracochłonną met od ą two rzenia aplikacj i, nie będ ę jej o m aw i ał s zcz egóło w o . Utw orzysz jednak prostą apli ka cję k o rzy stającą z Wind ows API , ab yś miał m o żli w o ść pozn ania podstaw mechanizmu, z którego ko rzy s taj ą wszy stk ie aplikacje pracujące w sys temie operacyj nym . W tym rozdziale poznasz podstawy ws zystki ch trzech spo sobów tworzen ia aplikacj i dla Windowsa, natomi ast w dalszej częś c i k siążki poznasz s zc zegóły używan i a MfC i W indows Forms. Oczywiści e , w C++ m o żn a równ i eż tw orzy ć aplikacje n i ewyrnagając e systemu operacyjnego Windows i to podej ś cie je st stosowane na przykład w grach komputerowych , gdzie wymagana je st najwięks z a wydaj noś ć grafiki. C hoć jest to bard zo interesuj ące , wł a ś ci w e omówienie tego tematu wymagałob y napisania całej ks iążki, więc nie będę go dalej ro zwij ał. Zanim przejdziemy do przykładów dla tego rozdziału, przypomnę term in ol og ię u ż yw an ą do opisu okna apli kacji . W rozdz iale l. utworzyłeś już program dla systemu Wind ows, nie pi sząc przy tym ani jednego w iersza kodu, więc do omówienia elementów tw orząc y ch okno u żyj ę okn a utworzon ego przez tamt en kod.

Rozdziałl1.



Założenia

programowania dla systemu Windows

663

Elementy okna zna sz większo ś ć (j eżeli nie wszystkie) podstawowych elem ent ów inte rfejsu programu dla systemu Windows. Mimo to o mów ię je , aby nasze rozumieni e terminów było t ożsam e . Naj lepszym sposo bem na poznanie elementów okna jest przyjrzen ie s ię je dnemu. Rysunek 11.1 przed stawia opisaną wersj ę okna wyś w i etlanego przez przykł ad z ro zd zi ału l. Z

p e wn o ścią

użytkownika

Uchwyt rozmiaru

Rysunek 11.1 Okno macierzyste apl ikacji MDI

Krawędź

Okno pot omn e ap likacji MDC Obszar klient a okn a po to mnego Ikona ok na potomnego Obszar klient a okna macierzystego Pasek n arz ędzi

Przycisk Zamknij

Pasek men u Ikona na pasku

Przycisk Mak symalizuj

na rzędz i

Przycisk Minimali zuj

Pasek st anu

I

Tekst w pasku tytułu

Tekst w pasku t ytułu okna potomn ego

W rzeczyw i stości ten przykład utw orzył dwa okna. W i ęk s ze okn o, zawi eraj ąc e pasek menu i narzędzi , jest oknem główn ym lub macierzystym, a mniejs ze okno jes t oknem potomnym okna główne go. Okno potomn e m ożna za mknąć poprzez dwukrotne klikn ięc ie ikony paska tytułowego zn aj d uj ącej s i ę w lewym górnym rogu okna potomnego, nie z a my kając przy tym okna głów nego . Natomiast zamkn ięcie okna głó w n ego spowoduje automatyczne zam kn i ęc ie okna potomnego. Dzieje s ię tak dlatego, że okno potomne należy do okna głównego i jest od niego zależne . Jak się przekonasz, gł ówne okn o mo że p o s i adać kilka okien potomnych . Najbardziej podstawowymi elemen tami typowego okna s ą jego obramowanie i pasek tytułu , w którym wyświ e tlana jest nadana nazwa, ikona paska tytułu , poj awi ająca się w lewym rogu paska tytułu, oraz obszar klienta , który jest obszarem w ś rod ku okna, niewykorzystywanym przez pasek t ytułu i obramow anie . Wszystk ie te elementy można bez przeszkód umieścić w program ie dla systemu Windows. Jak się przekonas z, jedyn e, co musisz zrobi ć , to poda ć jak iś tekst dla paska tytułu .

664

Visual C++ 2005. Od podstaw Obramowanie okre śla wymi ary okna , którego rozmi ar może być ustawiony na stałe lub z moż­ liwo ś cią zmiany. Je żeli obramowanie umożl iwi a zm i an ę rozmiaru, mo żn a tego dokonać poprzez przeciąganie ram ki okn a. Okno może również po siadać uchwyt zmiany rozmiaru, dzięki któremu można zmieniać rozmiar okna , za chowując przy tym jego proporcje - współczyn­ nik szerokości do wysoko ś ci . Podczas definiowani a okna można określić wyg ląd i zachowanie obramowania. Większość okien będzie również po s iadała w prawym górnym rogu przyciski mak symalizacji , minimalizacji i zamkni ęci a . Pozwalają one na p owięk s zenie okn a do rozmi arów pełnego ekr anu , zmniej szenie do ikony lub na jego zamknięcie. Po klikn ięciu lewym przyciskiem myszy ikony paska tytułu pojawi si ę standardowe menu, zwane menu systemowym lub menu sterowania, umożl iwi ające zmianę lub zam kn i ęc i e okna. Menu sys temowe pojawia s i ę również po klikni ęciu prawym przyciskiem myszy paska tytułowego okna. Choć nie jest to konieczne, umieszczenie ikon y paska tytułu we wszy stkich głównych oknach programu jest dobrym pomysłem. Zam ieszczenie ikon y pask a ty tu ł u pozwala łatwo zam kn ąć program, gdy coś nie działa podcza s debugowania . Ob szar klienta j est częśc i ą okna, w której przeważnie program ma pisać tekst lub grafik ę. W tym celu obszar klienta j est adresowany tak samo jak podwórko , które widziałeś na rysunku 7.1 w rozdziale 7. Lewy górny róg obszaru klienta ma współrzędne (O, O) z wymiarem x zwięk­ szającym się od lewej do prawej , a y od gó ry do d ołu . Pas ek menu nie j est wymagany, ale prawdopodobnie jest naj częs tszym sposo bem sterowania aplikacją. Każde menu w pasku menu po klikni ęc iu wyświetla li stę rozwijan ą elementów menu. Zawa rto ść menu i wy gląd wielu obiektów wyświetlanych w oknie - takich jak ikony w pasku narzędzi , kursor i wiele innych - są defini owane przez plik zasobów. Poznasz więcej plików zasobów, gdy przejdziemy do tworzenia bardzi ej złożonych programów dla systemu Wind ows. Pasek n arzędzi za wiera zestaw ikon, które przeważnie są alternatywnym sposobem do stępu do najczęś ciej wykorzystywanych opcji menu . Ponieważ za w ie rają one gra fi cz ną w skazówkę co do ich funkcj i, często ułatwiają i przyspi e szają pracę z programem. Zanim przejdę dalej , chciałbym zastrzec jedną rzecz dotyc ząc ą term inologi i. Uży t kow n icy zwykle p ojmuj ą okno jako to, co pojawi a s i ę na ekranie i ma obramowanie, co o czyw iś ci e jest prawdą, jednak to jest tylko jeden rodzaj okna. W systemi e Windows okno jest ogólnym poję c iem obejmującym cały zbiór elementów. W rzeczywi stości w zasadzie każdy wyświe­ tlany element jest oknem - na przykład okno dialogowe lub przycisk. Odnosząc się do obiektów, będę przew ażn ie stosował terminol ogi ę , która opi suje, czy m one są, czyli na przykład przyciskami , okn ami dialogowymi itp. Mu sisz jednak pamiętać , że są one równie ż oknam i, ponieważ możes z z nimi robi ć wszy stko to, co ze zwykłym okn em na przykład możesz rysowa ć na prz ycisku.

Programy dla Windowsa i system operacyjny Gdy tw orzysz program dla sys temu Windows, jest on podporządkowan y sys temo wi operacyjn emu i to Windows ma k ontrol ę . Program nie musi sobie r adzi ć bezpośredni o ze sp rzętem , bo cała je go zewnętrzna komunikacj a przechodzi przez Windows. Gdy korzystasz z programu dla Windowsa, oddziałujesz gł ó w n i e na system Wind ows, który w Twoim imieniu komun ikuje s i ę z aplik acją. Można pow iedzieć , że program dla Wind ows a jest ogonem, system Wind ows j est psem , a program poru sza s ię jedynie wtedy, gdy W indows mu każe.

Rozdział 11.

• Zalozenia programowania dla sltstemu Windows

665

tak z wielu powodów. Po pierwsze i najważniejsze, program w zasadzie zawsze zasoby komputera z innymi programami, które mogą być uruchomione w tym samym czasie. Windows musi mieć nadrzędną kontrolę, aby zarządzać wsp ółdzieleniem zasobów komputera. Gdyby jedna aplikacja mogła mieć nadrzędną kontrolę w środowisku Window s, programowanie byłoby zdecydowanie bardziej skomplikowane, ponieważ trzeba by zadbać o możliwość działania innych programów i zapobiec utracie informacji przeznaczonych dla innych aplikacji. Drugim powodem, dla którego Windows ma kontrolę , jest fakt, że Windows dostarcza standardowy interfejs użytkownika i musi wymagać przestrzegania tego standardu. Na ekranie można wyśw ietlać informacje jedynie za pomocą narzędzi dostarczanych przez system Windows i tylko po autoryzacji.

Dzieje

się

w sp ółdzieli

Programy sterowane zdarzeniami W rozdziale l . dowiedziałeś się, że program dla systemu Windows jest sterowany zdarzen iami, program dla Windowsa przeważnie czeka, aż coś się zdarzy. Znaczna część kodu wymaganego przez aplikację dla Windowsa służy do przetwarzania zdarzeń, które są spowodowane przez zewnętrzne działania u żytkownika, ale mimo to niektóre działania niezwiązane bezpoś rednio z aplikacją mogą wymagać wykonania części kodu programu. J eżeli na przykład użytkownik przeciągnie aktywne okno innej aplikacji obok Twojego programu i poprzez to odsłoni część obszaru klienta okna Twojej aplikacji , musi ona przerysować tę część okna. więc

Komunikaty Windowsa Zdarzenia w aplikacji dla Windowsa to wystąpienia na przykład takich sytuacji jak kliknięcie myszą przez użytkownika, naciśnięcie klawisza czy osiągnięcie zera przez licznik czasu . System operacyjny Windows umieszcza każde zdarzenie w komunikacie i zamieszcza komunikat w kolejce komunikatów programu, dla którego przeznaczony jest dany komunikat. A zatem komunikat Windowsa jest po prostu zapisem danych dotyczących zdarzenia, a kolejka komunikatów aplikacji jest sekwencją takich komunikatów oczekujących na przetworzenie przez aplikację. Poprzez wysłanie komunikatu system Windows może powiedzieć programowi, że coś musi zostać zrobione, że jakieś informacje są dostępne lub że nastąpiło kliknięcie myszą. Jeżeli program jest poprawnie utworzony, w odpowiedni sposób zareaguje na komunikat. Istnieje wiele rodzajów komunikatów i mogą one pojawiać się bardzo często ­ na przykład podczas przemieszczania myszy kilka razy na sekundę. Program dla systemu Windows musi zawierać funkcję przeznac zoną do obsługi tych komunikatów. Funkcja często nazywana jest WndProc e) lub Wi ndowProc e), jednak nie musi mieć ona jakiejś szczególnej nazwy, ponieważ Windows ma dostęp do funkcji poprzez utworzony przez Ciebie wskaźnik do funkcji. A zatem wysłanie komunikatu do programu sprowadza się do wywołania przez system Windows funkcji określonej przez programistę (przeważnie W i nProc e)) i przesłania programowi wymaganych danych za pośrednictwem argumentów tej funkcji . W funkcji WinProc() musisz na podstawie dostarczonych danych samodzielnie określić typ komunikatu oraz reakcję programu. Na szczęście nie trzeba pisać kodu do obsługi każdego komunikatu. Można wybrać te, które faktycznie interesują nasz program, i obsłużyć je w dowolny sposób , a resztę przesłać z powrotem

666

VislJal C++ 2005. Od podstaw do Windowsa . Komunikat można przesłać z powrotem do Windowsa poprzez wywołanie standardowej funkcji DefWi ndowP rocO systemu Windows, która dostarcza domyślne przetwarzanie komunikatów.

Windows API Całość

komunikacji między każdą aplikacją dla Windowsa a systemem Windows wykorży­ stuje interfejs programowania zwany Windows API (ang. Application Programming fnter(ace). Składa się on dosłownie z setek funkcji, dostarczanych standardowo z systemem Windows, które umo żliwiają komunikowanie si ę aplikacji z Windowsem i odwrotnie. Windows API był utworzony, gdy głównym językiem programowania b ył C, na długo przed nadejści em C++, i z tego powodu do przesyłania niektórych rodzajów danych między systemem Windows a aplikacją stosowane są struktury, a nie klasy .

Windows API obejmuje wszystkie aspekty komunikacji między Windowsem i aplikacją. API zawiera tak wiele funkcji , bezpośrednie z nich korzystanie może być bardzo trudne - wyzwaniem jest tu nawet samo ich poznanie. Tutaj z pomocą pro gramiście przychodzi Visual C++ 2005 . W tym środowisku funkcje API są ustrukturyzowane w sposób przypominający obiekty. Dzięki niemu można również w prostszy sposób korzystać z interfejsu w C++ z bardziej rozbudowaną domyślną funkcjonalnością. Przybiera to formę Microsoft Foundation Classes - MFC. Ponadto dla aplikacji przeznaczonych dla CLR dostępna jest funkcja Windows Fonns, gdzie cały kod potrzebny do utworzenia GUl jest tworzony automatycznie . Wówczas pozostaje jedynie napisać kod potrzebny do obsługi zdarzeń w wymagany przez aplikację sposób. Aplikacj ę korzystającą z Windows Fonns utworzysz w dalszej części tego rozdziału , natomiast szczegóły korzystania z Windows Forms poznasz w rozdziale 22 . Ponieważ

Visual C++ dostarcza także kreatory aplikacji (ang. application wizard) do tworzenia podstawowych aplikacji różnego rodzaju, włącznie z aplikacjami MFC i Windows Fonns. Kreator aplik acji może wygenerować kompletną, działającą aplikację , zawierającą szablonowy kod potrzebny dla podstawowej aplikacji dla systemu Windows, pozostawiając programiście jedynie dostosowanie go do własnych potrzeb. Przykład z rozdziału l . pokazał, jak wiele funkcjonalno ści można uzyskać dzięki Visual CH, nie wpisując żadnej linijki kodu. Omówię szczegółowo ten temat, gdy przejdziemy do pisania bardziej praktycznych przykładów za pomocą kreatora MFC Application.

Typy danych wsystemie Windows Windows definiuje sporo typów danych wykorzystywanych do określania typów parametrów funkcji oraz typów zwracanych w Windows API. Te swoiste dla systemu Windows typy rozprzestrzeniają się do funkcji zdefiniowanych w MFC. Każdy z tych typów windowsowskich będzie miał odwzorowanie w którymś z typów C++, jednak ponieważ mapowanie typów Windows i CH może się zmienić, zawsze gdy potrzeba, powinien eś stosować typy Windows. Na przykład w przeszłości windowsowski typ WORD był definiowany w jednej wersji Windowsajako typ unsigned shor t , a w innej wersji jako typ unsi gned i nt. Na maszynach szes-

Rozdział 11.• Załoienia programowania

nastobitowych te typy

jednak na maszyna ch 32-b itowych s ą zdecy dowanie z typów C++ zamiast z typów systemu Wind ow s m ó gł mieć

problemy.

Kompl etn ą li stę

znajduje

s ię

typów danych w Wind owsi e znajdziesz w dokum entacji, natomi ast kilka najc zęś ciej spotykanyc h.

poni żej

BOOLlub BOO LEAN

Zm ienna logiczna przyjmując a wart ości TR UE lub FA LSE . Z a uw a ż , ż e nie j est to ten sam typ co boa l w C++, który m ó gł mi e ć wartość t rue lub fal se.

iJYTE

Bajt

CHAR

Znak

m·JORD

32-bitowa liczba

HAND LE

Uchwyt obiektu, 32-bitowa liczba

HBRUSH

Uchw yt

HCURSOR

Uchwyt kursora .

HOC

667

są r ównow a żn e,

ró żne , wi ęc ktoś korzystaj ący poważn e

dla systemu Windows

oś m iobi towy o ś m i obito wy

cał k owita

pędzla, pęd z e l

Uchwyt kon tekstu na okn ie.

bez znaku , co odp owi ada typowi un signed l ong w C++. c ałko wi ta określ ająca lok ali z ację

obiektu w pa mięci .

wy korzystywany do w ypełnieni a obs zaru ko lorem.

urz ąd z eni a

-

kontekst

urz ądzen ia jest

ob iektem, który pozwala

ry s ować

HIN STAN CE

Uchwyt instancji .

LPA RAM

Para metr komuni katu .

LPCSl R

Wsk a źn ik

do

LP HAND LE

Wska źni k

do uch wytu .

LRESULT

Wa rt o ś ć

vJO RD

16-bitowa liczba

s t ałego ł ańc u ch a

(ze znakie m)

b ęd ąc a

ca ł kow i ta

S-bitowy ch znaków.

wynik iem przetworzenia komuni katu .

bez znaku, co odpo wiad a typo wi uns 1 gned short w C++.

I

Wszelkie inne typy danych Windowsa wy stępujące w przykładach będę omaw iał na bieżąc o . Wszystkie typy wykorzystywane prze z system Windows oraz prototypy funk cji Windows API s ą przechowywan e w pliku nagłówkowym windows .h, więc musisz go dołączyć przy tworzeniu podstawowego programu dla Wind owsa.

Notacja wprogramach dla systemu Windows W wielu program ach dla Windowsa nazwy zm iennych m aj ą prefiks o kreś l ający, jakiego rodzaju warto ś ć przech owuje zmienna i jak jest ona wykor zystywana. Istnieje sporo takich prefiksów i często s ą stosowane ich kombinacje. Na przykł ad prefiks l pf n okre śl a daleki wskaźnik do funkcji (ang. tong pointer to a functiony. Przykładowe prefiksy, z którymi możes z si ę zetkn ąć są wymieni one w tabeli na następnej stronie. Te prefiksy tworzą tak zwaną notację węgierską . Zo stała ona wprow adzona, aby zminimal izować ryzyko błędneg o użyc i a zmiennej przez zinterpr etowanie jej w inny sposób, ni ż była ona zdefiniowana, lub niez godnie z jej przeznac zeniem . Tego typu błąd można b yło łatwo popełni ć w języ ku C, poprzedn iku CH . W C++, dzięki jego lepszej kontroli typów , nie trzeba

668

Visual C++ 2005. Od podstaw

b

zmienna logiczna typu BOOL,ekwiwalent i nt

by

typ unsi gned cher (skrót od angielskiego byte)

c

typ char

dw

typ DWORD, czyli unsi gned l ong

fn

funkcja (ang.fimction )

h

uchwyt (ang. handle), używan y do odwoływania

si ę

do

c zegoś

typ i nt typ l ong lp

daleki wskaźnik (ang. fong p ointer)

n

typ i nt

p

w sk a źnik

(ang. pointer)

łańcuch

(ang. string)

sz

ł ańcu ch

ASCII

w

typ WORD , czyli uns igned short wkładać tyle wysiłku w sposób zapisu, aby uniknąć tego typu błędów . Kompilator zaws ze oznaczy błąd niespójności typów w programie, więc wiele błędów , które dotykały programy nap isane w C, nie może pojawić się w C++.

Z drugiej strony notacja węgierska może pomóc z ro zu m i e ć kod programu, w szczególności gdy mamy do czynienia z dużą liczbą zmiennych różnego typu, które są argumentami funkcji Windows API. Ponieważ programy dla Windowsa są wciąż pisane w języku C i oczywiście dlatego , że parametry dla funkcj i Windows API są wciąż definiowane z użyciem notacji wę­ gierskiej, ta metoda jest wci ąż szeroko stosowana. Wi ęcej informacji na temat notacji węgier­ skiej znajd ziesz na stronie: http://web.umr.edu/- cpp/common /hungarian.html. Możesz sam zdecydować o stosowaniu notacji węgi er skiej, ponieważ nie jest ona obowiąz­ kowa . Możesz w ogóle z niej nie korzystać , jednak gdy s ię z nią dobrze zapoznasz, będzie Ci łatwiej zrozumie ć , czym są argumenty funkcji Windows API. Jest tu jednak m ałe zastrzeżenie. Wraz z rozwojem systemu Windows typy argumentów niektórych funkcji Windows API zostały nieco zmienione, natomiast nazwy zmiennych pozostały takie same. W konsekwencji prefik s może niedokładnie określać typ zmiennej .

Struktura programu dla systemu Windows W celu utworzenia małego programu korzystającego jedynie z Windows API napiszesz dwie funkcje. Będ zie to funkcja WinMain( l, od której zaczyna się wykonywanie programu i przeprowadzana jest jego podstawowa inicjalizacja, oraz funkcja WindowProc( i .wywoływan a przez system Windows w celu przetworzenia komunikatów dla aplikacji. Część Wi ndowProc() programu jest przeważnie większa, ponieważ to tutaj znajduj e s i ę większość kodu specyficznego dla aplikacji , reagującego na komunikaty powodowane działalnością użytkownika.

Rozdział 11.



Założenia programowania dla

systemu Windows

669

Pomimo że te dwie funkcje tworzą cały program, nie są one bezpo średnio połączone . Wi nMai n( l nie wywołuje Wi ndowProc ( l . Robi to system Windows. W rzeczywistości Windows wywołuj e takż e Wi nMai n( l. Przedstawia to rysun ek 11.2.

Rysunek 11.2

Windows

Windows API

6 ~----

.8"

fi----

~

---- E I I I I

,

·1[

Finrsh

II

Cancel

opcji w tej kategorii:

Opcja

Opis

Dialog based

Oknem aplikacji jest okno dialogowe, a nie okno ramek.

Mullipie top-level documents

Dokumenty są wyświetlane w oknach potomnych pulpitu, a nie jako okna potomne aplikacji , jak to ma miejsce w aplikacj i MDI.

Document/View architecture

Ta opcja jest domyślnie włączona, więc kod ob s łu g uj ąc y architekturę dokument-widok zostaj e wbudowan y. Gdy opcja ta jest nieaktywna, obsługa tej architektury nie jest dostarczana, więc możesz samodzielnie zadecydować , co zaimplementować.

support

Resource language

Lista rozwijana zawiera wybór języków dla zasobów takich jak menu i łańcuchy tekstowe w aplikacji .

Use Unieode libraries

Obsługa Unieode j est dostarczana przez biblioteki MFC w wersji Unicode . Jeżeli chcesz z nich korzystać, musisz zaznaczyć tę opcję .

Powinieneś wyłączyć opcję

Use Unieode libraries. Jeżeli jest ona aktywna, aplikacja spodziewa w formacie Unicode, a pliki są zachowywane jako znaki Unicode, wskutek czego nie są one możliwe do odczytania w programach spodziewających się tekstu ASCII. się wejścia

708

Visual C++ 2005. Od podstaw Możesz także wybrać między stylem projektu Windows Exp/orer i MFC standard. Pierwszy z nich implementuje okno aplikacji podzielone na dwie części - lewa wyświetla dane w formie drzewa, a prawa wyświetla zwykły tekst. Możesz także wybrać

sposób, w jaki kod bibliotek MFC jest wykorzystywany w programie. jako biblioteki współdzielone DLL (ang. Dynamie Link Library biblioteka dołączalna dynamiczni e), co oznacza, że program łączy się z ich procedurami, gdy są potrzebne. Dzięki temu rozmiar pliku wykonywalnego się zmniejsza, ale wymaga, aby biblioteka MFC DLL znajdowała się na komputerze, na którym program jest uruchamiany. Łączna wielkość obydwu modułów (pliku .exe aplikacji i MFC .dlł) może być większa niż w sytuacji, gdyby biblioteka została połączona statycznie. Jeżeli wybierzesz łączenie statyczne, procedury biblioteki MFC podczas kompilacji zostają włączone do modułu wykonywalnego programu. Aplikacje z łączeniem statycznym działają nieco szybciej niż te z łączeniem dynamicznym, więc należy tu wybrać rozwiązanie kompromisowe między szybkością wykonywania i użyciem pamięci . Jeżeli pozostawisz wybraną domyślną opcję, czyli korzystanie z biblioteki współdzielonej, kilka programów działających jednocześnie i korzystających z tej samej biblioteki będzie mogło współużytkować jedną kopię biblioteki w pamięci. Domyślnie są one używane

W kategorii Doeument Template Strings mamy możliwość wpisania rozszerzenia plików tworzonych przez program. Dobrym wyborem dla naszego przykładu jest rozszerzenie .t xt . W tej części w polu Filter Name można podać nazwę filtru, który zostanie użyty w oknach dialogowych Open (otwórz) i Save as (zapisz jako) do wyświetlania jedynie plików z rozszerzeniem obsługiwanym przez aplikację. Kategoria User Interface Features kreatora aplikacji daje (patrz tabela na następnej stronie).

dostęp

do kolejnych opcji programu

W kategorii Advaneed Features znajduje się kilka opcji, na które należy zwrócić uwagę. Jedną z nich jest wybrana domyślnie Printing and print preview, a inną Context-sensitive he/p, którą należy uaktywnić. Opcja Printing and print preview dodaje do menu File (plik) standardowe elementy Page Setup (ustawienia strony), Print Preview (podgląd wydruku) oraz Print (drukuj), a kreator aplikacji umieszcza kod obsługujący te funkcje . Uaktywnienie opcji Contextsensitive he/p dodaje podstawową obsługę pomocy zależnej od kontekstu. Jeżeli chcesz korzystać z tej funkcji, musisz dodać odpowiednią treść do plików pomocy. Po wybraniu kategorii Generated Classes ukaże się lista klas, które kreator aplikacji wygeneruje w kodzie aplikacji, co obrazuje rysunek 12.7. Możesz wybrać dowolną klasę, klikając ją,

a w części poniżej ukaże się jej nazwa, nazwa pliku nagłówkowego, w którym będzie przechowywana jej definicja, używana klasa bazowa oraz nazwa pliku zawierającego implementację funkcji składowych klasy. Definicja klasy jest zawsze przechowywana w pliku z rozszerzeniem .h, a kod źródłowy funkcji składowych zawsze znajduje się w plikach .cpp. W przypadku klasy CTextEditorOo c możemy zmienić wszystko oprócz klasy bazowej, natomiast gdy wybierzesz klasę CTextEdi torApp,jedyne, co będzie można zmienić, to nazwa klasy. Spróbuj wybrać inne klasy. Dla klasy CMai nFrame można zmienić wszystko poza klasą bazową, a dla CText Ed itorVie w- pokazanej na rysunku 12.7 - można również zmienić klasę bazową, Rozwiń listę innych klas, których można użyć jako klasy bazowej . Lista ta jest przedstawiona na rysunku 12.7. Możliwości w klasie widoku zależą od wybranej klasy bazowej .

Rozdział 12.

Opcja ThickFrame

• Programowanie dla sYStemu Windows zwykorzystaniem MFC

709

Opis Umożliwia zmianę

Opcja tajest

rozmiaru okna poprzez

przeciąganie jego

obramowania.

domyślnie aktywna.

Minimize box

Ta opcja jest również domyślnie aktywna i umieszcza przycisk minimalizacji w prawym górnym rogu okna .

Maximiz e box

Ta opcja jest również domyślnie aktywna i umieszcza przycisk maksymalizacji w prawym górnym rogu okna .

Minimized

Po wybraniu tej opcji aplikacja zostaje uruchomiona ze zminimalizowanym oknem .

Maximized

Po wybraniu tej opcji aplikacja zostaje uruchomiona ze zmaksymalizowanym oknem .

lnitial status bar

Opcja ta włącza pasek stanu umieszczony na dole okna aplikacji. W pasku tym znajdują się wskaźniki aktywności CAPS LOCK, NUM LOCK i SCROLL LOCK, a także wiersz komunikatów, w którym wyświetlane są łańcuchy podpowiedzi dla opcji menu i przycisków paska narzęd zi.

Sp/iI window

Ta opcja umieszcza pasek

Standard docking bar

Ta opcja dodaje do okna aplikacji pasek narzędzi , w którym znajduje s ię standardowy zestaw przycisków stanowiących odpowiedniki standardowych elementów menu. Pasek narzędzi jest umieszczany domyślnie. Pasek dokowalny może być przeciągany do bocznej lub dolnej części okna aplikacji, więc można go umieścić w najbardziej dogodnym miejscu. W rozdziale 13. dowiesz się, jak dodawać przyciski do paska nar zędzi .

Browser style toolbar

Ta opcja dodaje do okna aplikacji pasek Internet Explorer.

Rysunek 12.7

podziału

w każdym z głównych widoków aplikacji .

narzędzi

podobny do tego z programu

MFC AI'plicalion Wiza. d - Textfditor -

-

L1Jf:EJ

Generated Classes

Ov er vłe w

Application Type Compound OOC:lJTlert 5upport Document Template Strings

§enerated dasses: r-

.il CTe xtEditorApp

CTextEditorOoc

,l CMainframe

O_taba,e Support User Interface Features Advanced Features

Generat ed Classes

q~sname :

.h fll!:.,

!CTextEditorView

Bgseclass: J C Vie w

i Te xt Ed ito rVie w .h

....

~-CFormV iew

C Hl m lEdilVie w CHlmlView

CListV iew C RichEditView

C S cro llVle w CTreeview CV iew

Iv

I

. cp~ file ,..,'::-:-.ITe xtEd ilo rVie w .cp p

--,

710

Visnal C++ 2005. Od podstaw

Klasa bazowa CEdit Vi ew

Możliwości klasy widoku U m o ż li w i a prostą wielowierszową ed ycję

tekstu, a

także

funkcj e wyszukiwania,

zamieniania oraz drukowania. CFarmVi ew

Dostarcza widok b ędąc y formularzem. Formularz jest oknem dialogowym, które może zawierać kontrolki dla wyświetlania danych oraz wprowadzania danych przez użytkownika. Jest to w zasadzie taka sama funkcjonalno ść jak w przypadku aplikacji Windows Form s dla CLR, które po znasz w rozdziale 21 .

CHtml Ed itV i ew

Ta klasa rozszerza klasę CHt ml Vi ewi dodaj e

CH t ml Vie w

Dostarcza widok , w którym

CList View

Umożliwia

CRie hEdi t View

Um ożli wia wyświ etl ani e

CSera11Vi ew

Dostarcza widok, który automatycznie dodaje pa ski przewij ani a, je ż eli dane tego wymagają.

CTr eeVi ew

U m ożl i w i a

CView

Dostarcza podstawowych

możliw ość

można przegl ądać

edytowan ia stron HTML.

stron y WWW ora z lokalne pliki HTML.

korzystanie z architektury dokument-widok z kontrolkami list. i edycję tekstu sformatowanego (RTF). wyświetlan e

korzystanie z architektury dokument-widok z kontrolkami drzewa. możl iwości pr z e gl ądania

dokumentu.

Poni eważ nasza aplikacja nosi nazwę Text Editor, czyli edytor tekstu, wybierz CEdit Vi ew, aby automatycznie uzyskać podstawowe możliwości edycji tekstu .

Kliknij przycisk Finish, aby kreator aplikacji MFC utworzył pliki dla w programu bazowego, korzystając z właśnie wybranych opcji .

pełni

funkcjonalnego

Wynik działania MFC Application Wizard Wszystkie pliki utworzone przez kreator aplikacji są przechowywane w katalogu projektu TextEditor, który jest podkatalogiem katalogu rozwiązania o tej samej nazwie. W katalogu res, będącym podkatalogiem katalogu projektu, znajdują się pliki zasobów. W IDE można na kilka sposobów przeglądać informacje zwi ąza n e z projektem (patrz tabela na następnej stronie). Jeżeli

w panelu Solution Explorer klikniesz prawym przyciskiem myszy TextEdi tor, a nawybierzesz Properties z menu kontekstowego, wyświetlone zostanie okno właściwo­ takie jak na rysunku 12.8.

s tępn i e ści ,

Lewa część teg o okna zawiera grup y właściwości , których zawartość będz ie wyświetlana w prawej czę ści okna. W tej chw ili wy świetlana jest grupa General (ogólne). Wartość wła­ ściwości można zmieniać w prawej części okna, klik ając ją i wybierając nową wartość z listy rozwijanej znajdującej się po prawej stronie nazwy właściwości lub w niektórych przypadkach samodzielnie wpisując wartości.

Rozdzial12.• Programowanie dla systemu Windows zwykorzystaniem MFC Zakładka

711

Zawartość

lub panel

Solution Explorer

Tu wyświetlane sąp liki wchodzące w sk ład projektu. Są one um ieszczone w wirtualnych katalogach o nazwach Header Fi/es (pliki nagłówkowe) , Resource Fi/es (pliki zasobów) i Sourc e Fi/es (pliki źró d łow e).

Class View

W Class View wyśw iet la ne s ą klasy i ich składowe wc hodzące w skł ad proj ektu. Znaj dują s ię tu ró wn i eż wszys tkie zdefiniowa ne byty globalne . Klasy wyświetla ne są w górnym panelu, a w dolnym wyśw ie tla ne są składowe klasy wybranej w górnym panelu. Klikn i ę ci e prawym przyc iskiem myszy elementu zn aj d uj ące go się w Class View spowoduj e wyśw ietlen ie menu, za pom o cą którego m ożna przej rzeć defi nicję danego elementu lub odwolania do niego.

Resource View

Tu wy świ etl a ne są zaso by, tak ie ja k elemen ty menu i przyciski paska narzę dz i, wykorzystywane w proj ekcie. K liknięcie zasobu prawym przyciskiem myszy spowoduje wyśw i e tleni e menu um ożli wi aj ące go edycj ę istniej ącego zasobu lub dodanie nowego.

Property Pages

Tu w y św i etl a n e są moż liwe do utworzen ia we rsje proj ektu. Wersj a tes towa (ang. Debug ) zawiera dodatkowe udogodnienia uła tw i aj ące debugowanie kodu . Skompilowanie wersji ostatecznej (ang. Release) skutkuje utworzeniem mniej szego pliku wykonywalnego. Wersję tę tworzymy po przeprowadze niu wszystkich testów. Po k liknięciu prawym przyciskiem myszy wersj i (Debug albo Release) zostanie wyśw ietlone menu kontekstowe, za pomocą którego m ożesz dodać arkusz właśc iw ośc i lub wyśw i e tli ć bi eżące właśc i wo ści danej wersji. Arkusz wła ściw ośc i umożliwia ustawieni e opcji kompil atora ikonsolidatora.

Te xtf duur

Plopell y

ConfigU"ation:

Pages

,,1

I

Active(Debuo)

Pletf crm:

!iI COtMlon Properties

@MMOI'

.Gener.1 Oebugging

i±I M an~est T001 ltl ge soc rces @ XML Docereot Generator !ł1. Browse Inform6tion

i3 Build Events

1iJ··Custom euild Step ~ Web Depłov ment

Ad iv e(Win32)

~I

-

,El Gener al

(3 . Coof igur ation Prcperties

\B CjC++ l±l Linker

I

[1Jrxl ( Corłi guration M«lager. , . I

I nt ermediate Oirector y

I

$(Solut ionDir)$ (ConfigurationName)

.2;J

Extens:ions to Oelete on elean

· .ob);' .dk;' .t1b; · .t1i; ·.tlh; · .tmp; • .rsp; - ,pgC; - ,pqd;$(Targe t1

Build Log File

$(lntDir)\BuildLog .htm

Inherlted Proje ct Property Shee ts

EJ Pr o j ec t Def aults Conf lguration Type

Application (.e x e)

Use of MFC

use rtFC in a Shared Dl L

Use of ATL

Not UsingATL

Minimize CRT Use in ATL

No Use Unicode Character set No Common Languege Runtime support No Whole Program Opt imization

Character Set Comman Language Runtime SUPP Oł"t Whole Program Optimizetion

D\JtpUt OirecŁory

Specjfiesa r ełative path to tbe ouQ:n,t fle drector y; cen indude environmentvariabIes.

I Rysunek 12.8

,

$(Conf igurationName )

OK

II

Aruluj

= 11 Z.'j5!:~JI

J

712

Visual C++ 2005. Od podstaw

Przeglądanie

IJlików proieklu

w panelu Solution Explorer rozwiniesz listę poprzez kliknięcie znaku + przy nazwie TextEditor, a następnie klikniesz ten znak przy folderach Source Fi/es (pliki źródłowe) , Header Fi/es (pliki nagłówkowe) i Resource Fi/es (pliki zasobów), ujrzysz wszystkie pliki wchodzące w skład projektu. Zostało to przedstawione na rysunku 12.9. Jeżeli

Rysunek 12.9

. ~ ł ;(Sl [;;l

Solution 'TextEditor' (I project )

8· · ~imifiHtitj EJ··

c:::;. Header File. @ MainFrm,h ~ Resource.h stdef x.h

o

l!il l!il l!il l!il

.

8

TextEd,tor .h Te xtłidit n rfr o c .h TextEditorYiew .h

Resource Rles

[00

TextEdltor.k o

gJ

TextEd'to r.r c2

Jii TextEdltor.rc

-

~ TexłEditorOoc. ico

tS Toolb_r .bmp

8

t2:4 Source F~es

e

, '::3 MainFrm,cpp stdefx.cpc '::3 TextEditor .cpp o

·

~ TextEdit orDoc.cpp

g 111

TextEditorView ,cpp

ReadMe.t xt

,&!SOIutlon Explorer ~$ CI ess Y'ow

Na rysunku 12.9 panel przedstawiony jest jako pływające okno, aby była widoczna cała lista plików. Wszystkie zwinięte panele można przekształcić w pływające okna poprzez kliknięcie ikony strzałki skierowanej w dół, która znajduje się w górnej części panelu, i wybranie z listy żądanej pozycji. W skład naszego projektu wchodzi 17 plików. Zawartość każdego pliku można wyświetlić , klikając dwukrotnie jego nazwę. Zawartość wybranego pliku jest wyświetlana w oknie edycji. Otwórz w ten sposób plik Readme.txt . Jak się przekonasz, zawiera on krótki opis zawartości wszystkich plików wchodzących w skład projektu. Nie będę powtarzał tych opisów, ponieważ ich podsumowanie w pliku Readm e.txt jest bardzo dobre.

Przeglądanie klas Dostęp

do projektu za pomocą panelu Class View jest często zdecydowanie bardziej wygodny za pomocą Solution Explorer, ponieważ klasy są podstawą organizacji aplikacj i. Gdy chcemy przejrzeć kod, przeważnie chodzi nam o definicję klasy lub implementację funkcji skła­ dowej. Z panelu Class View mamy bezpośredni dostęp do każdej z nich. Czasem jednak przyniż

Rozdział 12.

daje

się

• Programowanie dla systemu Windows zwykorzystaniem MFC

713

Solution Explorer. Gdy chcemy sprawdzić dyrektywy #i ncl ude w pliku .cpp, za poExplorer możemy bezpośrednio otworzyć interesujący nas plik .

mocąSolution

W panelu Classes możesz rozwinąć element TextEditor, aby zobaczyć klasy zdefiniowane dla aplikacji. Kliknięcie nazwy klasy powoduje wyświetlenie wszystkich jej składowych w dolnej części panelu. W panelu Class View, przedstawionym na rysunku 12.10, została wybrana klasa CTextEdi t orDoc.

Rysunek 12.10

®

(lass Yiew

... l ~ .

u l.

;. . C ~.

El ~ TeKtEditor ffi = Maps .. Global FuncŁions and VariabJes

. .~ MatrosandConstants [jJ

"l$ CAboutDlg

I±I ~ CMainFrame

$ ~

CTextEditorApp

Iii ~

CTextEditorView

l±l 4tNAi iiild@

..

Finish

)

I

Cancel

Dodatkową klasą jest CChi l dFr ame, która jest wyprowadzana z klasy MFC CMD I Ch i l dWnd. Klasa ta dostarcza okno ramowe dla widoku dokumentu , który pojawia się wewnątrz okna aplikacji utworzonego przez obiekt CMai nFrame. W aplikacji typu sm istnieje jeden dokument z jednym widokiem, więc widok jest wyświetlany w obszarze klienta głównego okna ramowego. W aplikacji typu Mm może zostać otwartych wiele dokumentów, a każdy z nich może mieć wiele widoków. Aby było to możliwe, każdy widok dokumentu w programie posiada wła­ sne potomne okno ramowe utworzone przez obiekt klasy CCh i l dF rame. Jak widziałeś wcześniej, widok jest wyświetlany w osobnym oknie, które dokładnie wypełnia obszar klienta okna ramowego.

Uruchamianie programu Program możesz utworzyć tak samo jak w poprzednim przykładzie. Gdy go uruchomisz, zostanie wyświetlone okno aplikacji przedstawione na rysunku 12.13. Oprócz głównego okna aplikacji mamy osobne okno dokumentu oznaczone jako Sketcherl. Sketcherl jest domyślną nazwą początkowego dokumentu, a po zapisaniu ma rozszerzenie .ske. Możesz tworzyć dodatkowe widoki dokumentu, wybierając z menu Window/New Windowo Możesz także tworzyć nowe dokumenty poprzez wybranie z menu Fi/e/New. W takiej sytuacji w aplikacji będą dwa aktywne dokumenty. Sytuacja, w której mamy dwa aktywne dokumenty z dwoma widokami każdy, została przedstawiona na rysunku 12.14. Na razie w aplikacji nie można tworzyć żadnych danych, ponieważ nie napisaliśmy jeszcze żadnego kodu, który by to umożliwiał. Natomiast cały kod tworzący dokumenty i widoki został dołączony przez kreator aplikacji.

Rozdzial12.• Programowanie dla syStemu Windows zwykorzystaniem MFC

Rysunek 12.13 Dwa widoki dokumen t u Sketcherl To j est tworzon e jako egzemplarz CMainFrame Dwa w idoki dokume ntu Sketcher2

Menu i pasek narzędz i



t worzone przez obiekt CMainFrame, ale są wspó /dzie lone

To jest okno w obs zarze klienta okna CChi ld Frame. Jest ono t wo rzone przez obiekt CSketcherView i bę dz ie zawierało dan e dokume ntu

-T-

Rysunek 12.14

123

724

lisual C++ 2005. Od podstaw

Podsumowanie Ten rozdział dotyczył głównie sposobu działania kreatora aplikacji MFC. Poznałeś podstawowe komponenty programów MFC , generowane przez ten kreator zarówno dla aplikacji typu sm, jak i MDI. Wszystkie nasze przykładowe aplikacje MFC zostały utworzone za pomocą kreatora aplikacji MFC, więc należy pamiętać ogólną strukturę i szerokie powiązania klas. Liczba szczegółów może być nieco przytłaczająca, jednak wraz z rozwijaniem aplikacji w kolejnych rozdziałach wszystko stanie się bardziej zrozumiałe. Poniżej

przedstawiam naj istotniejsze zagadnienia omówione w tym rozdziale:



Kreator aplikacji MFC generuje kompletny, działający szkielet aplikacji dla Windowsa, który można następnie dostosowywać do własnych potrzeb.



Kreator aplikacji może utworzyć aplikacje z interfejsemjednodokumentowym (Sfrl), które pracują z pojedynczym dokumentem i pojedynczym widokiem, lub może utworzyć aplikacje z interfejsem wielodokumentowym (Mm), które mogą obsłużyć jednocześnie wiele dokumentów w wielu widokach.



Cztery najważniejsze klasy w aplikacji sm, które •

klasa aplikacji,



klasa okna ramowego,



klasa dokumentu,



klasa widoku.

są wyprowadzane

z klas MFC, to:



Program może posiadać tylko jeden obiekt aplikacji. Jest on definiowany automatycznie w globalnym zakresie przez kreator aplikacji .



Obiekt klasy dokumentu przechowuje dane specyficzne dla aplikacji, a obiekt klasy widoku wyświetla zawartość obiektu dokumentu.



Obiekt klasy szablonu dokumentu jest używany do połączenia dokumentu, widoku i okna. W aplikacji sm służy do tego klasa CS ingleDocTemplate, a w aplikacji MDI wykorzystywana jest klasa CDocTemp l ate. Obydwie te klasy pochodzą z MFC i przeważnie nie ma potrzeby wyprowadzania klas specyficznych dla aplikacji.

Ćwiczenia utworzenia ćwiczeń związanych z programowaniem, jedynie podstawowe sposoby tworzenia aplikacji MFC. Do poniż­ szych ćwiczeń nie ma rozwiązań, ponieważ odpowiedź albo pojawi się na ekranie, albo będziesz mógł ją znaleźć w treści tego rozdziału.

W tym rozdziale nie ma

możliwości

ponieważ przedstawiał on

Natomiast pod adresem: http://helion,pl/ksiaz/d/vcpppo.htm znajdziesz kod z tej książki oraz rozwiązania innych ćwiczeń.

kładów

źródłowy

do przy-

Rozdział 12.•

Programowanie dla slslemu Windows z wlkorzyslaniem MfC

725

1. Jaki jest związek miedzy widok iem a dokumentem?

2. Do czego

a.

s łu ży

szablon dokumentu w programie MFC dla Windowsa?

Dlaczego należy być ostrożnym i dokładnie przed skorzystaniem z kreatora aplikacj i?

zaplanować strukturę

lo Utwórz prosty edytor tekstu. Skompiluj zarówno wersję a na stępnie

sprawdź

testową,

pro gramu

jak i o stateczną,

typ i rozmiar utworzonych plików.

I. Kilka razy utwórz edytor tekstowy, za każdym razem zmien iając styl okna w zakł adc e Advanced Options kreatora aplikacji .

726

Visual C++ 2005. Od podstaw

13 Praca zmenu i paskami narzędzi

Z poprzedniego rozdziału dowiedziałe ś się, z czego składa się podstawowa aplikacja wygenerowana przez kreator aplikacji MFC oraz jak te części s ą ze s o bą powiązane. W tym rozdzial e rozpoczniemy rozbudowę naszej podstawowej aplikacji MOI o nazwie Sketcher w celu stworzenia z niej w pełni funkcjonującego programu . Zaczniem y od widoku. Pierw szy krok w tym procesie polega na zrozumieniu sposobu, w jaki definiowane są menu w Visual C++ 2005 oraz jak tworzone są funkcje obsługujące charakterystyczne dla aplikacji elementy menu, które dodamy do programu . Dowi esz się też, jak dodać do aplikacji paski narzędzi. Po przeczytaniu tego rozdziału będziesz wiedz iał : obsługuje



W jaki sposób program oparty na MF C



O zasobach menu i sposobach ich tworzenia oraz modyfikacji.



O



Jak utworzyć funkcję obsługującą komunikat wygenerowany po wybraniu elementu menu .



Jak

dodać

procedury



Jak

dodać

przyciski paska narzędzi i powiąz a ć j e z

właściwościach

komunikaty.

menu i sposobach ich tworzenia oraz modyfikacji.

obsługi aktualizuj ące właściwości menu. istniejąc ymi

elementami menu.

Komunikacja zsystemem Windows Jak wiesz z rozdziału 11., Windows komunikuje s i ę z programem, wysyłaj ąc do niego komunikaty. Większość pracy związanej z obsłu gą komun ikatów wykonuje MFC ; nie musisz zatem w ogóle martwić się o dostarczenie funkcji WndProc( ). Dzięki MFC możesz dostarczyć funkcje obsługujące wybrane komunikaty, a resztę zignorować . Te funkcje nazywaj ą się procedurami ob sługi komunikatów. Ponieważ nasza aplikacja je st oparta na MFC, procedura ob sług i komunikatów jest zawsze funkcją składo wą j ed n ej z klas aplikacji.

728

Visual C++ 2005. Od pOlIslaw P owiązanie międ zy

konkretnym komunikatem a o b s łu g uj ącą go funkcj ą w program ie jest dokonywane przez map ę komunikatów - musi ją mi eć k a żda klas a w pro gram ie, która obsługuje komun ikaty Windowsa . Mapa komunikatów dla klasy jest po prostu tablic ą funkcji s kład o w yc h, które obsłu gują komun ikaty Windowsa. Każdy wpis w mapie komunikatów łączy funkcję z konkretnym komunikatem. Gdy nadejdzie dany komunikat, wywoływana jest odp owiadająca mu funkcja . W mapie komunikat ów dla klasy zn ajdują się jedynie komun ikaty dla niej istotne. Mapa komunikatów dla klasy jest tworzona automatycznie przez kreator aplikacji MFC podczas tworzenia projektu lub przez ClassWi zzard, gdy dodajesz do programu klasę obsłu guj ąc ą komunikaty . Dodawaniem i usuwaniem wpisów z mapy komunikatów przeważnie zajmuje się ClassWi zard, jednak istnieją wyjątki i wtedy musisz samodzielnie zmienić mapę komun ikatów. W kodzie programu początek mapy komunikatów jest określony makrem BEGIN_MESSAGE_ MAP O , ajej koniec makrem END_MESSAGE_MAPO. Przyjrzyjmy się teraz, jak działa mapa komunikatów, na podstawie naszego przykładowego programu Sketcher.

Zrozumieć mapy komunikatów Kreator aplikacj i MF C tworzy mapę komunikatów dla każdej głównej klasy programu. W przypadku programu typu MDI, takiego jak Sketcher, mapa komunikatów jest definiowana dla następujących klas: CSket cherApp, CSket cherDoc, CSket cherVi ew, CMai nFrame i CChi ld Frame. Mapa komunikatów dla klasy znajduje się w pliku .cpp zawierającym implementację danej klasy. Oczywiście funkcje umieszczone w mapie komunikatów muszą być także zadeklarowane w definicji klasy, są one jednak identyfikowane w specy ficzny sposób. Przyjrzyj się poniższej definicji klasy CSk etc herApp:

cl ass CSketcherApp

publ ic CWinApp

{

publ ic: CSketcherApp(): II Przes lonięcia.

publ tc :

vir tual BDDL InitI nst ance(): II Implementacj a.

afx_msg void DnAppAbout () ; DECLARE MESSAGE MAP( ) }:

W klasie CSketc herApp zadeklarowana jest tylko jedna procedura obsługi komunikatów OnAppAbout ( l. Słowo af x_msg, znajdujące s i ę na początku wiersza z deklaracją funkcji OnAppAbout( l, służy jedynie do wyróżn ienia procedury ob sługi komunikatów spo śród innych funkcji składowych klasy. Preprocesor zmienia je na białe znaki, więc nie ma ono znaczenia podczas kompilacji. Makro DECLARE_MESSAGE_MAP( ) wskazuje, że klasa może zawi erać funkcje składowe służące do komunikatów. W rzeczywi sto ści wszystkie klasy wyprowadzane z klasy MFC CCmdTarget mogą potencjalnie zawierać procedury obsługi komunik atów, więc tego typu klasy będą obsługi

Rozdział 13.•

Praca zmenu i paskami narzędzi

729

zawierały to makro w swojej defin icj i. Umie ści j e tam kreator aplikacji MFC lub kreator Add Class, w zależnoś ci od narzędzia, z którego skorzystasz do utworzenia danej klasy. Rysunek 13.1 przedstawia klasy MFC wyprowadzone z klasy CCmdTarget , które zostały użyte w naszych dotych czasowych przykładach.

CCmdTarget

CWinThread

CWnd

CDocument

CFrameWnd

Rysunek 13.1 Klasy , które zostały wykorzystane bezpośrednio lub jako bezpośrednia podstawa naszych własnych klas, są zacieniowane. A zatem klasa CCmdTarget jest p o średnio klasą bazową dla klasy CSketc herApp, wi ęc ta ostatnia będzie zawsze zawierała makro DECLARE_ME SSAGE_MAP ( ). Wszystkie klasy widoku (i inne) wyprowadzane z CWnd również będąje zawierały . Jeżeli samodzielnie dodajesz własne składowe do klasy , najlepiej będzie , jeżeli pozostawisz makro DECLARE_MESSAGE_MAP O jako ostatni wiersz definicji klasy. Jeżeli dodasz składowe za makrem , będziesz musiał umieści ć dla nich specyfikatory do stępu: publ t e, pr ot ecte d lub

pri vat e.

Definicje procedur obslugi komunikatów Jeżeli

definicja klasy zawiera makro DECLARE_MESSAGE_MAP, implementacja klasy musi zawierać makra BEG IN_MESSAGE_MAPO i END_MESSAGE_MAP O . Jeżeli zajrzysz do pliku Sketcher.cpp, znajdziesz tam poniższy kod będący częś cią implementacji CSket cherApp:

BEGI N_MESSAGE_M AP(CSketcherApp, CWi nApp) ON_COM M AND(ID_APP_ABOUT. &CSketche rApp: :OnAppAbout) II Standardowe polecenia do pr acy z plikami.

ON_COM M AND(ID_FILE_NEW, &CW inApp: :OnFileNew) ON_COMM AND( ID_FIL E_OPEN . &CWinApp: :OnFi leOpen) II Standardowe polecenie ustawień drukowania.

ON_COM M AND(ID_F ILE_PRINT_SETUP. &CWinApp: :OnF il ePri nt Setup) ENDME SSAGE M AP( )

730

Visnal C++ 2005. Od podstaw Tak wygląda mapa komunikatów. Makra BEGIN_MESSAGE_MAP( ) i END_MESSAGE_MAP( ) są granicami map y komunikatów, a każda procedura obsługi komunikatów klasy znajduje się między nimi . W tym przypadku kod obsługuj e tylko jedną kategorię komunikatów - rodzaj komunikatu WMJ OMMAND, zwany komunikatem polecenia, który jest generowany, gdy użytkownik wybierze opcję menu lub użyje skrótu klawiaturowego (może się to wydawać nieco niezgrabne, a jest tak dlatego, że - jak przekonasz się w dalszej części rozdziału - istnieje jeszcze jeden rodzaj komunikatu WM_COMMAND, zwany komunikatem powiadamiającym z kontrolki). Mapa komunikatów rozpoznaje naciśnięty klawisz lub wybrany element menu na podstawie identyfikatora przesłanego w komunikacie. W przedstawionym powyżej kodzie znajdują s ię cztery makra ON_COMMAND, po jednym dla każdego obsługiwanego komunikatu poleceń. Pierwszy argument przesyłany do makra stanowi identyfikator pow iązany z konkretnym poleceniem, natomiast makro ON_COMMAN D wiąże nazwę funkcji z poleceniem określonym przez identyfikator. J eżeli zatem zostanie otrzymany komunikat odpowiadający identyfikatorowi l F_APP_ABOUT, zostanie wywołana funkcja OnAppAbout ( l . Podobnie dla komunikatu z identyfikatorem IDJILE_NEWzostanie wywołana funkcja OnFi leNew( l . Ta proc edura, podobnie jak pozostałe dwie, jest definiowana w klasie bazowej. Makro BEG IN_MESSAGE_MAP ( l przyjmuje dwa argumenty. Pierwszy z nich identyfikuje nazwę klasy, dla której definiowana jest mapa komunikatów, a drugi dostarcza połączenie z klasą bazową w celu odszukania procedury obsługi komunikatów. Jeżeli procedura nie zostanie odnaleziona w klasie, dla której definiowana j est mapa komunikatów, przeszukiwana jest mapa komunikatów dla klasy bazowej . Należy pamięta ć , że

identyfikatory poleceń, takie j ak ID_APP_ABOUT, są standardowymi identyfikatorami definiowanymi w MFC. Odpowiadają one komunikatom ze standardowych elementów menu i przycisków pasków narzędzi . Prefiks ID_używany jest do identyfikowania polecenia powiązanego z elementem menu lub przyciskiem paska narzędzi, o czym przekonasz s ię , gdy b ędę omawiał zasoby. Na przykład IDJ ILE_NEWjest identyfikatorem odpowiadającym wybraniu z menu FileINew, a ID_AP P_ABOUT odpowiada wybraniu z menu Help/About. Do identyfikowania standardowych komunikatów Windows wykorzystuje nie tylko symbol WM_COMMAND. Każdy z nich ma prefiks WM_ ozn aczaj ący komunikat Windowsa (ang. Windows Message). Te symbole są definiowane w pliku Winuser.h, który jest włączany w Windows.h. Jeżeli chcesz je poznać , plik Winuser.h znaj duje si ę w podfolderze lnclude folderu VC zawierającego Visual C++ .

Istni eje skrót do przeglądania pliku .h. Jeżeli j ego nazwa pojawia s ię w oknie edycj i, możesz ją po prostu kliknąć prawym przyciskiem myszy i wybrać z menu podręczn ego Open document "Nazwa_pliku.h". Komunikaty Windowsa często mają dodatkowe dane wykorzystywane do dokładnej identyfikacji danego komunikatu określonego identyfikator em. Na przykład komunikat WM_COMMAND jest przesyłany dla szeregu poleceń, włączając w to te pochodzące z wybrania elementu menu lub przycisku paska narzędzi. Podczas samodzielnego dodawania procedur obsługi komunikatów należy pamiętać, aby nie mapować komunikatu (lub - w przypadku komunikatów poleceń - identyfikatora polecenia) do więcej niż jednej procedury obsługi w klasi e. Robiąc w ten sposób, niczego nie zepsujes z,

Rozdzial13. • Praca zmenu i paskami narzędzi

731

jednak druga procedura nigdy nie zostanie wywołana . Procedury obsługi komunikatów dodaje się zwykle za pomocą okna właściwości i w takim przypadku nie ma możliwości mapowania komunikatu do więcej niż jednej procedury. Okno właściwości klasy można wyświetlić, klikając prawym przyciskiem myszy nazwę klasy w panelu Class View i wybierając Prop erties z menu kontekstowego. Procedurę obsługi komunikatów dodaje się poprzez kliknięcie przy cisku Messages w oknie Prop erties (rysunek 13.2). O tym, który przycisk to Messages, dowiesz się, przytrzymując kursor nad każdym z przycisków do momentu pojawienia się wskazówki.

Rysunek 13.2

Prooerttes

'r

J) X

CMainFrame VCCodeClass

~ ~ ! b ill -f ~. J;-m Btf~~.~ ~.E~~.;?~E.J

J-

----~-__,il

....

WM_ACTlVATE WM_ACTlVATEAPP WM_A5KCBFORMATNAM WM_CANCELMODE WM_CAPTURECHANGED WM_CHANGECBCHAIN WM_CHANGEUI 5TATE WM_CHAR WM_CHARTOlTEM WM_CHILDACTlVATE WM_CL05E WM_COMPACTlNG WM_COMPAREITEM WM_CONTEXTME NU WM_COP VDATA WM_CREATE

OnCreate

WM_CTLCOLOR WM_DEADCHAR WM_DELETEITEM WM_DE5TROY WM_DE5TROYCLlPBOAR WM_DEVMODECHANGE WM_DRAWCLlPBOARD

CMainFrame

Kliknięcie przycisku Messages spowoduje otwarcie listy identyfikatorów komunikatów. Zanim jednak powiem , co będziemy dalej robić, muszę omówić dokładniej typy komunikatów, które będziemy obsługiwać.

Kategorie komunikatów Istn ieją trzy kategorie komunikatów obsługiwanych przez program , a kategoria, do której komunikat należy , determinuje sposób jego obsługi . Istniejące kategorie komunikatów są pokazane w tabeli na następnej stronie.

Standardowe komunikaty Windowsa należące do pierwszej kategorii są identyfikowane przez definiowany przez system Windows identyfikator poprzedzony prefiksem WM_. W kolejnym rozdziale napiszesz procedury obsługi kilku takich komunikatów. Komunikaty z drugiej kategorii są szczególnym rodzajem komunikatów WM_COMMAN D. Poznasz je w rozdziale 16. podczas pracy z oknami dialogowymi. W tym rozdziale zajmiemy się ostatnią kategorią - komunikatami pochodzącymi z menu i pasków narzędzi. Oprócz identyfikatorów komunikatów

732

Visual C++ 2005. Od podstaw

Kategoria komunikatu

Opis

Komunikat Windowsa

Opr ócz ko munikatów WM_COMMAND, które omówię za mom ent, są to standa rdowe komunikaty Wind owsa roz p o c zy n aj ąc e się prefiksem WM_. Przykł adem może być tu komunikat W M_PAINT wskazujący k oniec zn o ś ć przerysowania obszaru klienta okna i WM_LBUTTONUP ws ka zuj ący na zwolnienie lewego przyci sku myszy.

Komuni katy z kontrolek

powiadamiając e

S ą to komunikaty W M_COMMAND prz e s y ła ne z kontrolek (takich jak lista wyb oru) do okna, które utworzyło k ontrol kę , lub z okna potomnego do macierzystego. Parametry p owiązan e z komunikatem WM_COMMAND p ozwalaj ą w programie odróżni ć komun ikaty pochodzące z kontrolek.

Komun ikaty

poleceń



to komuni katy WMJOMMAND p och od z ąc e z elementów inter fejsu takich jak elementy menu i przyciski pasków narzędzi . MFC defin iuje unikalne identyfikatory dla standardowych komunikatów pol ec eń menu i pasków narzęd zi . u żyt k own ika,

dostarczanych przez MFC dla standardowych menu i pask ów narzędzi, możesz defin iować własne identyfikatory komun ikatów dla menu i pasków n arzędz i , które dodasz do programu. Jeżeli nie podasz identyfikatora dla tych nowych elementów , MFC automatycznie wygeneruje go za Ciebie na podstawie tekstu menu .

Obsluga komunikatów wprogramie Procedury obsługi komunikatów nie można umieścić w dowolnym miejscu . Jej umiejscowienie zależy od rodzaju komunikatu , jaki ma obsługiwać. Pierwsze dwie kategorie komunikatów z przedstawionej powyżej listy, czyli standardowe komunik aty Windowsa i komunikaty powiad amiające z kontrolek, s ą zaw sze obsługiwane przez obiekty klas wyprowadzanych z CWnd. Z CWnd są wyprowadzane na przykład klasy okien ramowych i widoków, więc mogą one zawi erać funkcje składowe do obsługi komunikatów Windowsa i komunikatów powiadamiających . Klasy aplikacji dokumentu czy szablonu dokumentu nie s ą wyprowadzane z CWnd, więc nie mogą one obsługiwać tego typu komunikatów . U życi e

okna właściwości klasy do dodania procedury obsługi komunikatu zwalnia nas od procedur, ponieważ zawiera ono jedynie identyfikatory komunikatów, które dana klasa może obsłużyć. Je żeli na przykład jako klasę wybierzesz CSketcherDoc, w oknie jej wła ściwości nie pojawi si ę żaden komunikat WM_. koniecznośc i pamiętania możliwych um iejscowień

Klasa CWnd dostarcza domyślną obsługę standardowych komunikatów Windowsa . Jeżeli zatem wyprowadzona klasa nie zawiera proc edury ob słu gi standardowego komunikatu Windowsa , jest on przetwarzany przez domyślną procedurę obsługi umieszczon ą w klasie bazowej . Jeżeli w klasie umieścisz własną procedurę obsługi , do poprawnego ob słu żenia komunikatu może być czasem potrzebne odwołanie do procedury znajdującej się w klasie bazowej. Gdy tworzysz własną procedurę obsługi, po wybraniu procedury w oknie właściwości klasy tworzona jest jej ramowa implementacja zawierająca w razie potrzeby odwołanie do procedury bazowej. Obsługa

komunikatów poleceń jest znacznie bardziej elastyczna. Procedury ich obsługi można w klasie aplikacji, klasach dokumentu i szablonu dokumentu , w klasach okna oraz widoku. Co s ię zatem dzieje po przesłaniu komunikatu polecenia do aplikacji, biorąc pod uwagę liczbę miejsc , w których mogą się zn ajdować ich procedury ob sługi? umieszczać

Rozdzial13. • Praca zmenll ipaskami narzędzi

733

Przetwarzanie komunikatów poleceń

Wszystkie komunikaty poleceń są przesyłane do głównego okna ramowego aplikacji. Następ­ nie główne okno ramowe rozsyła je w określonej kolejności do klas programu w celu ich obsłu­ żenia. Jeżeli klasa nie może przetworzyć danego komun ikatu, jest on przesyłany do następnej. Oto

kolejność

1

klas, do których

przesyłany jest

komunikat w aplikacji typu SD! :

Obiekt widoku.

2. Obiekt dokumentu.

a.

Obiekt szablonu dokumentu .

.. Obiekt głównego okna ramowego.

I. Obiekt aplikacji. Jako pierwszy możliwość obsłużenia komunikatu otrzymuje obiekt widoku, ajeżeli nie została zdefiniowana procedura obsługi, taką możliwość ma kolejny obiekt klasy. Jeżeli żadna z klas nie ma zdefiniowanej procedury obsługi danego komunikatu, zajmuje się tym domyślna procedura , przeważnie wyrzucając komunikat. W programie typu l'vlDI sytuacja się w niewielkim stopniu komplikuje. Mimo że mamy dostęp­ nych wiele dokumentów z wieloma widokami każdy, jedynie aktywny widok i powiązany z nim dokument są zaangażowane w przesyłanie komunikatów. Kolejność przesyłania komunikatu polecenia w programie MDI przedstawia się następująco: 1

Aktywny obiekt widoku .

2. Obiekt dokumentu powiązany z aktywnym widokiem.

a.

Obiekt szablonu dokumentu dla aktywnego dokumentu.

.. Obiekt okna ramowego dla aktywnego widoku.

I. Obiekt głównego okna ramowego.

8. Obiekt aplikacji. Kolejno ść przesyłania

gana, że nie

będę

jej

komunikatów można zmienić, jednak jest to sytuacja tak rzadko wymaw niniejszej książce.

omawiał

Rozwijanie programu Sketcher W tej części dodamy do programu Sketcher kod, aby zaimplementować funkcje niezbędne do tworzenia szkiców. Umożliwi on rysowanie różnokolorowych linii, kół, prostokątów i krzywych o różnej grubości linii, a także dodawanie komentarzy do szkiców. Dane szkicu będą przechowywane w dokumencie, zapewnimy także różnorodne widoki tego samego dokumentu w różnych skalach.

734

Visual C++ 2005. Od podstaw Poznanie wszystkich elementów, jakie zamierzamy dodać, zajmie nam kilka rozdziałów, natomiast dobrym punktem wyjścia będzie dodanie elementów menu obsługujących typy rysowanych kształtów i pozwalających wybrać kolor, w którym będziemy rysować . Zarówno wybrany kształt, jak i kolor będą trwałe w programie, co oznacza, że wybór będzie aktualny do momentu wybrania innego kształtu lub koloru . Dod anie menu do programu Sketcher będzie przebiegać w

poniższych

pojawiały się



Zdefiniowanie elementów menu, aby oraz w każdym menu .



Zdecydowanie, które klasy naszej aplikacji powinny dla poszczególnych elementów menu.



Dodanie do klas funkcji



Dodanie do klas funkcji on aktywny wybór.



Dodanie dla

każdego

obsługujących

w

etapach:

głównym

pasku menu

obsługiwać komunikat

komunikaty menu.

uaktualniających wygląd

elementu przycisku paska

menu tak, aby

zadań

odzwierciedlał

razem z podpowiedziami .

Elementy menu dwoma aspektami pracy z menu w MFC : tworzeniem i modyfikacją menu w aplikacji oraz przetwarzaniem niezbędnym po wybraniu danego elementu menu, czyli definicją procedury obsługi komunikatu . Zacznijmy od tworzenia nowych elementów menu .

Zajmiemy

się

pojawiającego się

Tworzenie iedycja zasobów menu Menu są definiowane poza kodem programu - w pliku zasobów, a specyfikacja menu jest nazywana zasobem . Istnieje jeszcze kilka innych rodzajów zasobów, które można dołączyć do aplikacji. Typowymi przykładami mogą być tu okna dialogowe, paski narzędzi i przycisk i pasków narzędzi. W trakcie dalszego rozwijania programu Sketcher poznasz kolejne z nich. Przechowywanie definicji menu jako zasobu pozwala na zmianę wyglądu menu bez modyfikowania kodu obsługującego zdarzenia menu. Możesz na przykład zmienić elementy menu z angielskich na francuskie lub norweskie bez konieczności modyfikowania i rekompilowania kodu programu. Kod obsługujący komunikat generowany po wybraniu elementu menu nie musi być związany z wyglądem menu, a jedynie z faktem, że element został wybrany. Oczywiście , jeżeli dodasz jakieś elementy do menu, będziesz musiał dla każdego z nich dodać kod, aby te elementy do czegokolwiek słu żyły! Program Sketcher posiada już menu , co oznacza, że istnieje dla niego plik zasobów. Dostęp do zawartości pliku zasobów dla programu Sketcher możemy uzyskać, wybierając panel Resource View lub klikając dwukrotnie Sketcher.rc w panelu Solution Explorer. Przełącza to widok na Resource View (widok zasobów) , w którym wyświetlane są zasoby. Po rozwinię­ ciu zasobu menu (po kliknięciu symbolu +) okaże się, że mamy zdefiniowane dwa menu

Rozdział 13.•

Praca zmenu i paskami narzędzi

135

oznaczone identyfikatorami IDR_MAI NFRAME oraz IDR_Sket cher TYP E. Pierwsze z nich dotyczy sytua cji, gdy żaden dokument nie jest otwarty, natomiast drugie sytuacj i, gdy otwarty jest jeden lub więcej dokum entów. MFC wykorzystuje prefiks IDR_ do oznaczania zasobów definiujących kompl etne menu okna. Będziemy zm i en ia ć

jedynie menu o identyfikatorze IDR_Sket cher TYPE. Nie musimy zajmoIDR_MA INFRAME, ponieważ dodamy elementy menu mające zastosowanie jedynie w sytuacj i, gdy b ędzie otwarty jaki ś dokumen t. Może sz uruchomić edytor zasobów dla menu poprzez dwukrotne kliknięci e identyfikatora zasobu w Resource View. Jeżeli otworzysz w ten sposób IDR_Sket cher TY PE, pojawi się panel Editor , który został pokazany na rysunku 13.3. wać s ię

.J

• x ł Śkd(h~; . t c(i:'~ ';T-YPf -~·Menu'-·~ ,1 -----;===--~------------"'-------"---~~---'-'-

~d l t

!:iew

!:!ew

Ctrl+N

Qpen.. .

Ctr\+O

~ind ow

ttelp

l_T}p"H..., ..

I

~.Iose ~e

ct rl+S

~rtlt. . .

arl+p

Print Preyiew

ptirt SetIJP•.•

Recent File

Rvsunek 13.3

Dodawanie elementu menu do paska menu Aby dodać nowy element menu, wystarczy kliknąć element menu z etykietą Type Here i wpis a ć nazwę noweg o elementu. Jeżeli w nazwie elementu umieścisz przed literą znak &, litera ta będzie skrótem klawiaturowym danego elementu. Jako nazwę pierwszego elementu wpisz E&l ement. Dzięki temu L będzie skrótem klawiaturowym tego elementu , a do jego wywołania będzie wystarczało wciśnię cie Alt +L. Nie możemy wykorzystać E, ponieważ jest już ono używane dla Edit. Gdy skończysz wpisywać nazwę , kliknij dwukrotnie nowy element, aby wyświetlić jego właściwości (rysunek 13.4).

736

Visual C++ 2005. Od podstaw

Rysunek 13.4

_!,lg

" I Men;;Edilor IMenuEd

~

I ~ 1 ~ 1::1 B Appe arance I

I

Checked

EBd.m.nt F.Is.

E~~i~d

True

G,. ~ed

F. lse

Popup

True

-

--

-

8 Behavlor Break Right Ju~ly RlQht Ordor

Non
AddElement Cm_pTempElement ); II Przerysuj bie ź qce okno. Inval idat eRect CO) ; m_pTem pE lement = O: II Zeruj wskaźn ik do elementu.

O czywi ście ,

przed dod aniem elementu do dokumentu mu sim y sprawd zi ć , czy faktycz nie on istnieje. Użytkownik m ógł po prostu kliknąć lewym przyciskiem, nie przesuw aj ąc przy tym my szy. Po dod aniu eleme ntu do listy w dokumencie wyw o ł uj e my Inval i dateRecU), aby obszar klienta bie żąceg o widoku z ostał przerysowany. Argument o w artośc i O oznacza cały obszar klienta w widoku jako niepoprawny. W związku ze sposobem, w jaki działa mechani zm rubber-band, niektóre elementy mogą być niepoprawnie wyśw ietl ane . Jeżeli na przykład narysujesz poziomą lini ę, a nast ępnie będ z ie sz rysow ał prostokąt tego samego koloru, to w momenci e, gdy jego dolna lub górna krawęd ź nałoży się na lini ę, ten fragment linii zniknie. Dzieje s i ę tak dlatego , że krawędź j est rysowana w kolorze przeci wn ym do koloru elementu l eżąceg o poni żej, więc ponownie uzyskujemy kolor tła . Zerujemy także wskaźnik m_pTernpE l ement , aby uniknąć błędów, gdy utworzony zostanie kolejny element.

Testowanie dokumentu Po zapisaniu wszystkich zmienionych plików możes z utworzyć nową wersję programu Sketcher i ją uruchomi ć . Będziesz w stanie wykonać w nim rysunek taki jak ,,Zadowolony programista", prz edstawiony na rysunku 15.8. Dział an i e

programu lepiej teraz odpowiada rzeczywi stości. Przechowuj e w skaźnik do każdego elementu w dokumencie obiektu, wię c elementy te są przerysowywane automatycznie, gdy zajdzie taka potrz eba. Program dokonuje także poprawnego czyszczenia danych dokum entu, gdy zostanie usunięty. Możnajednak wc iąż wskazać



Można otworzyć

szereg

o g ran i cze ń

programu. Na

przykład:

kolejne okno widoku, wybierając z menu programu Windo w/N ew Window oTa możliwo ś ć jest wbud owana w ap l i k ację MDl i otwiera nowy widok i stniejąceg o dokumentu, a nie no wy dokument. Jednak w trakcie rysowania

Rozdział 15.•

Tworzenie dokumentu i poprawianie widoku

837

Rysunek 15.8 w jednym oknie elem enty nie są rysowane w drugim oknie. Elementy nigdy nie pojawi ają się w oknie innym niż tym , w którym zostały utworzone , chyba że zajmowany przez nie obszar musi zost a ć zj akie go ś powodu przerysowany. •

Możes z rysować

tylko w wido cznym obszarze klienta . Byłoby miło, gdyby wid oku i rysowania na wi ęks zym obszarze.

istniała

mo żliwość przewinięcia



W żaden spos ób nie musisz albo to jako ś

To są całkiem

można usunąć z n i e ś ć,

albo

elementu, więc je żeli p opełnis z b ł ąd , od nowa , tworząc nowy dokument.

ro zp ocząć

poważne

mało użyteczny.

braki, które zebrane razem sprawiają, że program w takiej postaci jest Na prawimy j e wszystkie jeszcze w tym rozdziale.

Poprawianie widoku Pierwszym problemem, z ja kim si ę zmierzymy, będzie uaktualnianie wszystkich okien dokumentu podczas rys owania elementu. Problem powstaje, ponieważ jedynie widok, w którym element je st rysow any , wie o nowym elem encie . Każdy widok d ziała niezależnie od inny ch i nie ma między nimi komunikacji. Musimy sprawić , aby k ażdy widok, który doda element do dokumentu, informował o tym p ozostałe widoki , tak aby i one mogły podjąć odpowiednie d z iałan i a.

Uaktualnianie wielokrotnych widoków Klasa dokumentu zawiera funkcję Upda t eA 11Vi ews O, co stanowi nieocenioną pomoc pr zy rozwiązywaniu tego problemu. D z ięki tej funkc ji dokument m o że przesł a ć komunikat do wszystki ch swoi ch widoków. Mus imy jąjedynie wywołać z funkcji OnLBut to nUp() w klasie CSketc her Vi ew po każdym dodaniu nowe go elementu do dokumentu :

838

Visual C++ 2005. Od podstaw void CSket cherView: :OnLButt onUp(UINT nFlags. CPoint poi nt ) {

if( this == GetCapture() ) Re leaseCapture() ;

II Przerwij przechwytywanie komunikatów myszy.

II Jeżeli istnieje element, dodaj go do dokumentu.

if (m_pTempElement ) {

GetOocument( )->AddE lement (m Tem El ement ); GetOocument ( )->Upd at eA11 Vi ews (O .O.mpTempE lement) ; II Powiadom wszystkie widoki . m_pTempEl eme nt = O; II Zeruj wskaźn ik do elementu.

Jeżeli

m_pTempEl ement nie jest NULL, działanie funkcji jest rozszerzane, aby wywoływana była funkcja składowa klasy dokumentu IJpdat eA11 Views(). Funkcja ta komunikuje się z widokami, powodując wywołanie funkcji składowej OnUpdat e( ) każdego widoku . Trzy argumenty funkcji Upd ateA11 Views ( ) zostały opisane na rysunku 15.9.

Ten argum ent j est do bieżącego widoku . Pow strzymuje on wywołanie funkcji składowej OnUpd ateOwido ku. wskażnikiem

LPARAMjest 32-bitowym typem w systemie Windows i może zostać użyty do przesIania informacji o obszarze, który ma zo stać uaktualni ony w obszarze klient a

void UpdateAlIView( CView* pSender, LPARAM IHint

Ten argum ent jest w s kaźnikiem

do ob iektu mogącego do starczyć informacji o fragm encie obszaru, który ma zostać zaktualizowany w obszarze klienta

=OL, CObject* pHint =NULL) ;

\"

j

Wartości tych dwóch argumentów są przesyłane do funkcji OnUpdateO

w widokach

Rysunek 15.9 Pierwszym argumentem funkcj i UpdateA11 Vi ews() jest często wskaźnik th i s dla bieżącego widoku. Zapobiega to wywołaniu funkcji OnUpdateC) dla bieżącego widoku i jest przydatne , gdy bieżący widok jest już aktualny . W przypadku programu Sketcher, ponieważ korzystamy z metody rubber-banding, chcemy, aby bieżący widok również został przerysowany. Podając jako pierwszy argument 0, sprawiamy, że funkcja OnUpdate() zostanie wywołana dla wszystkich widoków, włącznie z bieżącym. To sprawia, że wywołanie Inva1 id ateRectC ) staje się niepotrzebne.

Rozdzial15. • Tworzenie dokumentu ipoprawianie widoku

839

Nie używamy tu drugiego argumentu Updat eA11Vi ews (l , jednak przesyłamy wskaźnik do nowego elementu poprzez trzeci argument. Przesłanie wskaźnika do nowego elementu pozwala widokom określi ć, który fragment ich obszaru klienta wymaga przerysowania. Aby przechwyci ć informacje przesłane do funkcji UpdateA11Views( l, dodamy funkcję składową On Update ( l do klasy widoku. Możesz to zrobić za pomocą kreatora klas, korzystając z właści­ wości klas CSketc herView. Jestem pewien, że pamiętasz , i ż właściwości klasy można wyświe­ tlić , klikając prawym przyciskiem myszy nazwę klasy i wybierając Properties z menu kontekstowego. Jeżeli klikn iesz przycisk Overrides w oknie Properties, będziesz mógł wyszukać OnUpdate na liście funkcji możliwych do nadpisania. Kliknij nazwę funkcji, a następnie wybierz z menu rozwijanego w s ąsi edniej kolumnie OnUpdate. Gdy zamkniesz okno Prop er-

ties, będziesz mógł funkcji:

edytować

kod. Musisz jedynie dodać zaznaczony

poniżej

kod do definicji

void CSketcherView: :OnUpdate (CView* pSender. LPARAMlHint , CObject* pHi nt)

i II Jeżeli laki istnieje , unieważn ij obszar odpo wiadają cy wskazywanemu ob iektowi. II w p rzeciwnym razie unieważn ij cały obszar klienta . i HpHi nt)

InvalidateRect ( (CElement*) pHint)->Get BoundRect () ) ; el se Inval idat eRect (O ) ; Zauważ, że

musisz usunąć komentarze przy nazwach parametrów w wygenerowanej wersji funkcji. W przeciwnym przypadku nie skompiluje się ona z dodatkowym kodem. Trzy argumenty przesyłane do funkcji OnUpdat e( l w klasie widoku odpowiadają argumentom przesyła­ nym w wywołan iu funkcji UpdateAll Views( ). A zatem pHin t zawiera adres nowego elementu. Nie możemy jedn ak zakładać , że tak jest zawsze. Funkcja OnUpdate ( l jest wywoływana także przy pierwszym utworzeniu widoku, jednak ze wskaźnikiem NULL jako trzecim argumentem. A więc funk cja sprawdza, czy wskaźnik pHi nt nie ma wartości NULL i tylko wtedy pobiera opisujący prostokąt dla elementu przesłanego jako trzeci argument. Unieważnia ten obszar w obszarze klienta widoku poprzez przesłanie pro stokąta do funkcji I nval i dateRect () . Ten obszar jest przerysowywany przez funkcję OnDraw w tym widoku, gdy zostanie do tego widoku przesłany kol ejny komunikat WM_PAI NT. Jeżeli wskaźnik pHi nt ma wartość NULL, cały obszar klienta jest unieważniany. Przerysowywanie nowego elementu przez funkcję OnUpdat e( l może być kuszące, jednak nie jest to dobry pomysł. W odpowiedzi na komunikat WM_PAI NT powinny powstawać jed ynie trwałe rysunki. Oznacza to, że funkcja OnDraw( l w widoku powinna być jedynie miejscem rozpoczynającym jaki ekolwiek operacje rysowania dla danych dokumentu. To zapewnia, że widok zostanie narysowany poprawnie za każdym razem, gdy Windows uzna, że trzeba rysować. Jeżeli przebudujesz i uruchomisz teraz program Sketcher z nowymi modyfikacjami, wszystkie widoki pow inny zostać uaktualnione.

840

Visual C++ 2005. Od podstaw

Przewijanie widoków Na pierwszy rzut oka dodawanie przewijania do widoków wygląda prosto. C h o c i aż woda jest w rzeczywi stości głę bsza i bardziej mętna, niżby s ię to wydawało , to jednak do niej wskoczymy. Pierwszym krokiem je st zmiana klasy bazowej eSket eherView z Cvt ew na eSera1lView. Ta nowa klasa bazowa posiada wbudowan ą funkcję przewijania, więc możemy zmieni ć definicję klasy na:

class CSket cherView

pub ll C CSc rol lV iew

II Reszta defi nicji bez zmi an.

Musimy także zmie n i ć dwa wiersze kodu na początku pliku SketcherView.cpp, które odwołują się do klasy bazowej eSketeherView. Jako klasę bazową musimy podać eSe ra 11Vi ew zamiast eView:

IMPLEMENT_DYNCREATE(CSket cherView , CScrol lView) BEGINMESSAGE MAP (CSket cherVi ew, CSc rollView) Jednak to wciąż nie wystarczy. Nowa wersja kla sy widoku musi coś wiedzieć o obszarze, w którym rysujemy, na przykład znać jego rozmiar i to, jak daleko widok został przewin ięty za pomocą paska przewijania . Te informacje muszą zostać dostarczone przed pierwszym narysowaniem widoku . Możemy wstawić odpowiedni kod do funkcji On l nitia 1UpdateC) w klasie widoku. Wymagane informacje możemy dostarczyć , korzystając z funkcji SetSerallSizesC) dziedziczonej z klasy eSerall Vi ew. Argumenty tej funkcj i s ą objaśnione na rysunku 15.10. Przewinięcie o jeden wiersz następuje po kliknięciu strzałki w górę lub w dół na pasku przewijania. Przew in ięcie o jedną stronę ma miejsc e po kliknięciu samego paska przew ijania. Mamy tu możliwo ść zmiany trybu mapowania. MM_LOENGLI SH będzie dobrym wyborem dla aplikacji Sketch er, jednak najpierw uruchomimy przewijanie dla trybu mapowania MM_TEXT, pon ieważ wciąż czekają nas pewne problemy.

Aby dodać kod wywołujący funkcję SetScr a11Sizes( ), musimy nadpisać domyślną wersję funkcji Onlniti alupdat et ) w widoku. Możemy to zrobić w ten sam sposób jak przy nadpisywaniu funkcji OnUpda t e() - za pomocą okna Properties klasy eS kete her Vi ew. Po dodaniu nadpi sania po prostu dopisz kod do funkcji w miejscu oznaczonym komentarzem:

void CSket cherV iew : :Onlnit ialUpdate( ) {

CScrol lView : .Onl ntt ta l tlpdat et ): II Definiuj e rozmiar dokum entu.

CS ize OocSize(20000.20000): II Ustawia tryb mapowania i rozmi ar dokum entu.

SetScrol lSizes(MMTEXT , DocS ize):

Rozdzial15. • Tworzenie dokumentu ipoprawianie widoku

To d efiniuje odległo ść w poziomie (ex) i w pio nie (cy), o j ak ą przewinąć stronę . Może być zdefi niowane jako : CSize Page(cx. cy); Do m yś l n ą wart o ścią jest 1/10 całego ob szaru

841

To definiuje odległ o ść w poziomie (ex) i w pionie (cy), o j a k ą przew inąć l in ię .

M oże być zdefiniow ane jako: CSize Line(cx, cy); Domyś Iną wa rtośc i ą j est 1/10 calego obszaru

void SetScrollSizes( int MapMode, SilE Total, const SllE& Page = sizeDefault, const SllE& line = sizeDefault );

M oże być jedn ą z p on iż szych : MM_TWIPS MM_Text MM_HEINGLlSH MM_LOENGLlSH MM_HIMETRIC MM _LOMETRIC

To jes t całkow ity obszar ry sowan ia i m oż e być zdefi niow ane jako : CSize Total(cx,cy); gdzie ex jest wym iarem w pozi omie , a cy w pionie wyrażonym w jednostkach log icznyc h

Rysunek 15.10 To pozostawia tryb mapowania MM_TEXT i umożl iw ia rysowanie na obszarze 20 000 na 20 000 pikseli . Te zm iany s ą wystarczające do zadziałania mechanizmu przewijania. Przebuduj program i uruchom go. Narysuj kilka elementów, a następnie przewiń widok. Mimo że okno przewija się poprawnie, gdy spróbujesz nary sować więcej elementów w przewin iętym obszarze, nie wszystko będzie dobrze działało. Elementy pojawiają się w innym miejscu, ni ż były rysowane, i nie są wyświetlane poprawnie. Co się dzieje?

Współrzędne logiczne i współrzędne klienta Problem jest związany z używanymi układami współrzędnych - a liczba mnoga jest tu zastosowana celowo. Do tej pory we wszystkich przykładach używaliśmy dwóch układów współ­ rzędnych, choć fakt ten mógł być n iezauważony . Jak widziałeś w poprzednim rozdziale, przy wywołaniu funkcji takiej jak Li neTo( ) zakłada ona, że prze słane argumenty s ą współrzędnymi logicznymi. Funkcja ta jest składową klasy COC, która definiuje kontekst urządzenia, a kontekst urządzenia ma własny układ współrzędnych logicznych. Tryb mapowania, będący właściwością kontekstu urządzenia , określa jednostki układu współrzędnych używane podczas rysowania. Z drugiej strony współrzędne otrzymywane wraz z komunikatami myszy nie mają nic wspólnego z kontekstem urządzenia lub obiektem CDC, a poza kontekstem urządzenia współrzędne

842

Visual C++ 2005. Od podslaw do OnLButtonDown () i OnMouseMove( ) mają zawsze w jednostkach urządzenia, czyli pikselach, i są mierzone względem lewego górnego rogu obszaru klienta. Są one nazywane współrzędnymi klienta. Podobnie, gdy wywołujes z I nva l i dat eRect( ) , zakłada się , że prostokąt jest zdefiniowany z użyciem współrzędnych klienta. logiczne nie

mają zastosowania. Punkty przesyłane

współrzędne wyrażone

W trybie MM_TE XT w s p ó ł rzę d ne klienta i współrzędn e logiczne w kontekście urządzenia są w pikselach, więc po zostają te same, dopóki nie przewiniesz okna. We wszystkich poprzednich przykładach nie przewijaliśmy okna, więc wszystko działało bezproblemowo. W ostatniej wersji programu Sketcher wszystko działa poprawnie do momentu przewini ęc ia widoku, kiedy to logiczne współrzędne początku układu współrzędnych (punkt O, O) zostają przeniesione przez mechanizm przewij ania, w związku z czym nie pokrywają si ę już one ze współrzędnymi początku układu współrzędnych klienta. Jednostki dla współrzędnych logicznych i współrzędnych klienta są takie same, ale początki dwóch układów współrzędnych już nie. Ta sytuacja została przedstawiona na rysunku 15.11. wyrażo n e

Rysowanie w nieprzewiniętym oknie W jednostkach logicznych

0,0

~-J.P_~

~

Zwolnienie Naciśnięcie lewegoprzycisku lewegoprzycisku .... - - - - - - .

... - - - - - - e

~A;;

< 1~

We współrzędnych klienta linia rysowana jest tutaj

We współrzędnych logicznych linia pojawia się tutaj

Rysowanie w przewiniętym oknie W jednostkach logicznych

0,0 x 4

Widok zastal przewinięty, więc początek układu znajduje się teraz tutaj _____ ~ SuttlWl" N aciśnięcie

lewego przycisku

~

Zwolnienie lewegoprzycisku

.' JJ We współrzędnych klienta linia rysowana jest tutaj

l_~L:tEJ

----..: ..~

We współrzędnych logicznych linia pojawia się tutaj

Rysunek 15.11 Lewa strona pokazuje pozycj ę w obszarze klienta , w której rysujesz, i punkty będące pozycjami myszy podczas definiowania linii. Są one zapisywane z użyciem współrzędnych klienta. Prawa strona pokazuje, gdzie linia jest naprawdę rysowana . Rysowanie jest dokonywane we współrzędn ych logicznych, ale z użyciem wartości współrzędnych klienta. W przypadku

Rozdzial15. • Tworzenie dokumentu ipoprawianie widoku przewiniętego

okna linia pojawia

się

w innym miejscu,

ponieważ zmieniła si ę

843

pozycja po-

czątku układu współrzędnych.

Oznacza to, że używamy złych wartości do definiowania elementów w programie Sketcher, a gdy unieważniamy fragmenty obszaru klienta, aby je przerysować , przesyłane prostokąty także są nieprawidłowe - stąd bierze się dziwne zachowanie programu . Przy innych trybach mapowania jest jeszcze gorzej, ponieważ nie tylko jednostki dla współrzędnych są różne , ale też osie y mają przeciwne kierunki!

Jak poradzić sobie ze wspólrzędnymi klienta Zastanówmy się , co musimy musimy się zająć :

zrobić,

aby

rozwiązać

ten problem.



dwie sprawy, którymi



Musimy przekształcić współrzędne klienta otrzymane z komunikatami myszy na logiczne współrzędne, zanim użyjemy ich do tworzenia elementów.



Musimy

przekształcić opisujący prostokąt utworzony

współrzędnych z wywołania

powrotem na współrzędne klienta, Inval i dat eReet( ).

z użyciem logicznych chcemy go użyć do

jeżeli

to do dopilnowania, aby korzystając z funkcji kontekstu urządzenia, zawsze logiczne, a dla innej komunikacji z oknem używać współrzędnych klienta. Funkcje, dzięki którym uzyskamy przekształcenia, są powiązane z kontekstem urzą­ dzenia, więc musimy uzyskać kontekst urządzenia za każdym razem , gdy będziemy chcieli przekształcać logiczne współrzędne na współrzędne klienta lub odwrotnie . Możemy do tego użyć funkcji konwersji współrzędnych klasy CDC, które są dziedziczone przez CCl ientDC. Sprowadza

się

stosować współrzędne

Nowa wer sja procedury OnLButtonDown()

będzie wyglądała następująco:

vO ld CSket cherV iew: :OnLBut tonOown(UINT nF lags. CPoi nt poi nt ) I CC li entOC aOC (t his ) : II Tworzy kontekst urządzenia .

On Prepa reOC( &aOC): II Popra wia początek układu wsp ółrzędnych.

aOC.OPtoLP (&point) ; II Konw ertuje punki na układ logiczny. m_Fl rst Po lnt ~ pOlnt : II Zapisz pozycję kursora. II Prz echwytuj kolejne komunikaty z mys zy. SetCa pt ure() ; }

Poprzez utworzenie obiektu CCl i entDC i przesłanie wskaźnika thi s do konstruktora uzyskujemy kontekst urządzenia dla bieżącego widoku . Zaletą CCl i entOCjest fakt, że automatycznie zwalnia kontekst urządzenia, jeżeli obiekt wyjdzie poza zakres. Ważne jest, aby nie przetrzymywać kontekstów urządzeń, ponieważ w systemie Windows jest ich ograniczona liczba i może ich po prostu zabraknąć . Jeżeli korzystasz z CCl i entDC, nie ma takiego zagrożenia. Ponieważ wykorzystujemy CSero11 Vi ew, funkcja składowa OnPr epareOCO dziedziczona z tej klasy musi zostać wywołana do ustawienia początku logicznego układu współrzędnych w kontekście urządzenia zgodnie z przewiniętą pozycją. Po ustawieniu początku układu korzystamy z funkcji OPtoLP( ), która zamienia punkty urządzenia na punkty logiczne (ang. Device Points to Logical Points), aby przekształcić wartość point przesyłaną do procedury na współrzędne logiczne. Następnie zachowujemy przekształcony punkt jako gotowy do tworzenia elementu w procedurze OnMouseMove().

844

Visual C++ 2005. Od podstaw Nowy kod dla procedury OnMou seMove ()

wyg ląd a n astęp ująco :

void CSketcherView: :OnMouseMove( Ul NT nFlags. CPoi nt point) ( II Definiuje obiekt kontekstu urządzenia dla widoku . CCl ientOC aDC(this): II Kontekst urządz e nia dla te o widoku. OnP repareDC( &aDC): II Popraw początek układu współrzędnych.

if( (nF lags&MK_L BUTTON) && (t his==GetCapt ure()) ) aDC.DPtoL P(&point ): m_Second Point ~ point:

II Konwer tuj pu nkt na układ log iczny . II Zapisz bieżącą pozycję kursora.

II Reszta funkcji bez zmian...

Kod dla konwersji w artości punktu przesyła nego do procedury jest taki sam jak w poprzedniej procedurze i w tej chwili tyle nam wystarczy . Os tatn i ą funk cj ą, którą musimy zm i e n i ć , a co łatwo przeoczyć , jest funkcja On Update( ) w klasie widoku. Musi by ć ona zmieniona w nast ę­ puj ący sposó b:

void CSket cherView : :OnUpdat e(CView* pSender . LPARAMlHi nt . CObject* pHint) ( II Jeżeli taki istnieje. un ieważnij obszar odpo wiadający wskazywa nemu obiektowi. II w prz eciwnym razie unieważn ij cały obszar klien ta.

l f( oHint ) CCl lent DC aDC(t hi s): OnPrepa reDC( &aDC) ;

II Utwórz kontekst urządzenia . II Popraw początek układu współrzędnych.

II Pob ierz pros toką t opis ujący i konwertuj go na

wspó łrzędne

klien ta.

CRect aRect =((CElement *)pHint )->Get BoundRect () ; aDC .LPtoDP(aRect ): II Przerysuj obszar. Inval idateRect (aRectl : else Inval idat eRect (Q):

II Un ieważn ij obszar klienta.

Wprow adzona tu zmiana tworzy obiekt CC l i entOC i u żywa funkcji składowej LPt oO P( ) do konwersji pro st okąta obszaru, który ma zos tać przerysowany na współrzędn e klienta. Je żeli

teraz skompilujesz oraz uruchomisz program Sketcher z wprowadzonymi modyfikacjami i udało Ci s ię nie p op ełni ć żadnych literówek, będzi e on dział ał poprawnie nieza leż nie od przewini ę c i a widoku.

Korzys1anie ZIrybu mapowania MM_LDENGLl8H Zastanówmy s ię teraz, co musimy zrobić, by zacząć korzystać z trybu mapowania MM_L OENGLI SH. Umożliwia on rysowanie w jednostkach logiczn ych 0,01 cala, a także zapewnia, że rozmiar rysowania jest spójny na wyśw ietlaczach o różnych rozdzi elczości ach . Z punktu widzenia użyt­ kownikajest to bardzo użyte czne .

Rozdział 15.•

Tworzenie dokumentu ipoprawianie widoku

845

Tryb mapowania możemy ustawić w wywołaniu SetScro l l Si zes ( l znajdującym się w funkcji Onl nitia lUpdat e () w klasie widoku. Musimy także określić całkow i ty rozmiar obszaru rysowania, wi ęc j eżeli okre ślimy go jako 3000 na 3000, będz i emy m ie li do dy spozycji obszar o rozm iarze 30 na 30 cali, co wydaje się odpowiednie. Domyślne od l egłośc i przewijania dla wiersza i strony są zadowalające, więc nie będzi emy ich dodatkow o określać. Możemy użyć panelu Class View, aby przejść do funkcji On l nit i al Updat e ( l , a n astępn ie zmienić ją w nastę­ pujący sposób:

void CSket eherVi ew : :OnInit 1alUpdat e() (

CSero11 Vlew: :OnInit ialUpdat e() : II Zdefiniuj rozmiar doku mentu jako 30 na 30 cali w trybie MM_LOENGLlSH.

CS ize OoeSize(3000.3000 ): II Ustaw tryb mapowania i rozmi ar dokumentu.

SetSerol lSizes(MMLOENGLISH. OoeS ize): Zmieniamy jedynie argumenty w wywołaniu SetScrollS i zes() na żądany tryb mapowania i rozmi ar dokumentu . To wszystko, co nale ży zrobić , aby um o żliwić pracę w trybie MM_LOENG LISH, j edna k wciąż musimy poprawić sposób obsługi prostokątów . Zauważ, że

wyboru trybu mapowania nie ustala się raz na zawsze . M oże sz w dowolnym momencie zmienić tryb mapowania w kontekście urządzenia i rys ować różne czę ści wyświe­ tlanego obrazu, korzyst ając z różnych trybów mapowania. Służy do tego funkcja Set MapMode ( l , jednak ni e będę jej tu sz erzej o m aw i ał. Nasza aplikacja może d zi ałać , korzystając jedynie z trybu MM_LOENGLI SH. Za każdym razem, gdy tworzony jest obie k t CC l ientOC dla widoku i wywoływana jest funk cj a OnPrepareOC() , kontekst urządzenia, który go posiada, ma tryb mapowania ustawi ony przez funkcję Onl rr i t i al i zeUpdate ( ). Problem, który mamy z prostokątami , polega na tym, że klasy elementów zakładają, iż trybem mapowania jest WM_TEXT, a w MM_LOENGLI SH prostokąty są odwró cone , ponieważ odwrócona jest również oś y. Gdy dla prostokąta zostanie zastosowana LPt oOP( l , zakł ad a s ię , że jest on poprawnie położony, z uwzględnieniem os i trybu MM_LOENGLI SH. A jako że tak nie jest, funkcja odwraca pro stokąty w pionie. Stwarza to problem, gdy w celu uni eważni eni a obszaru widoku zostaje wywołana funkcja Inva l i dateRect O, ponieważ odwrócony prostokąt we współrzęd­ nych urządzeni a nie jest rozpoznawany przez system Windows jako leżący wewnątrz widocznego obszaru klienta. Możemy

to

rozwiązać

na dwa sposoby: poprzez zmodyfikowanie klasy elementów , aby prood razu wyrażane w MM_LOENGLISH, lub poprz ez re n o rm a lizacj ę prostokąta, który zamierzamy prze słać do funkcj i Inval i dateRect ( l . Drugi sposób jest najłatwiej szy , ponieważ należy tylko zmienić jedną składową klasy widoku - Onupdate t l: stokąty opisujące były

void CSk eteherVi ew: :OnUpdat e(CView* pSender . LPARAMlHint. CObJeet * pHint ) { II Jeż eli taki istn iej e, un ie ważn ij obszar odpowiadający wskazywanemu obiektowi, II w p rzeciwnym razie un ieważnij cały obszar klienta.

if (pHintl (

CC l ientOC aOC (t hi s): OnPrepareOC(&a DC ) :

II Utwórz kontekst urzqdzeni a. II Popraw początek układu współrzędnych.

846

Visual C++ 2005. 0(1 podstaw

II Pobierz prostokąt

op isujący

i kon wertuj go na współrzędne klienta.

CRect a Rect~ « C E l emen t *) pH in t ) - >G et Bo u n d Re c t( ) : aDC.LPt oDP(aRect) : aRect .No rmal izeRect () : II Przerysuj obszar. inval idat eRect (aRect ): }

else invalid at eRect (O );

II

Unieważnij

obszar klienta .

Takie rozwiązanie wystarczy dla naszego programu. Jeżeli przebudujesz pro gram Sketcher, przewijanie będzie działało dla wielokrotnych widok ów. Musisz pamiętać, aby renonnalizować wszystkie prostokąty konwertowane do współrzędnych urządzeni a dla użytku z funkcją Inva l i dateRect ( ). Ma to również wpływ na wszystkie konwersje w drugą stronę .

Usuwanie iprzesuwanie kształtów Możliwość

usuwania kształtów jest jedną z podstawowych cech programu do rysowania. Wi ą­ z tym pytani e, w jaki sposób będziemy wybi era ć element do u sunię cia . Oczywi ście , gdy zdecydujemy się na sposób wyb ierania elementu, będzie on również doty czył przesuwania elementów, wię c usuwanie i przesuwanie elementów możemy uznać za tematy powi ą­ zane . Najpierw j ednak zastanówmy się , w jaki sposó b dodamy operacje przesuwania i usuwani a elementów. że się

Zgrabn ym sposobem udostępnienia funkcji przemieszczania i usuwania byłoby dodan ie menu kontekstow ego wyśw i etlającego się przy kursorze po kliknięciu prawym przyciskiem myszy . Możemy wówczas do niego dodać Move (przenieś) i De/ele (u suń) . Takie menu kontek stowe jest bardzo przydatnym udog odnien iem i mo żesz je w ykorzystać w wielu różnych innych sytu acjach. Jak powinn o być u żywan e menu kontekstowe? Standardowo użytkownik przesuwa kursor nad wybrany obiekt i klika go prawym przyciskiem myszy. Obiekt zostaje zaznac zony i zostaj e wyświ etlone menu zaw i erające li st ę działań do stępnych dla tego obiektu . Oznacza to, że ró ż­ ne ob iekty mo gą mi eć różne menu . Może s z z ob aczyć to r ozwiązan i e w Developer Studio. Gdy kliknie sz prawym przyci skiem myszy ikon ę klasy w panelu C/ass View, pojawi s i ę menu inne od tego , które pojawi s i ę po klikni ęciu prawym przyciskiem ikony dla funkcj i składowej . Pojaw i aj ąc e s i ę menu j est zależne od kontekstu kurs ora i stąd właśnie wziął się termin "menu kontekstowe". W program ie Sketcher mamy dwa możli we menu kontekstowe. Mo żna kl ikn ąć prawym przyci skiem albo wtedy , gdy kursor znajduje s ię nad elementem, albo kiedy s ię tam nie znajduj e. w i ęc sposób można za i m p le me n to wać tak i zestaw funkcj i w pro gr amie Sketcher? to zro b ić , two rz ąc po prostu dwa menu : jedno dla sytuacji, gdy pod kur sorem znajduje się element, i drugie w przeciwnym przypadku. Gdy użytkow n ik wc iś n i e prawy przycisk myszy, m ożem y sp raw dzać , czy pod kursorem znajduje s i ę element. J e żeli tak, możemy podśw ie t l ić element, aby użytkownik dokładnie wiedz i ał , do którego elementu odnosi się menu kont ekstowe.

W j aki

Można

Rozdział 15.•

TWll'Zellie 110kllmenlll i poprawianie widoku

847

poznaj sposób, w jaki można utworzyć menu kontekstowe dla kursora, a gdy ono działało , powrócimy, aby zaimplementować szczegóły operacji przemieszczania i usuwania elementów.

Na

początek

będzie

Implementacja menu kontekstowego Pierwszym etapem jest utworzenie menu zawierającego dwie karty - jednej z elementami Move i Delete, a drugiej stanowiącej połączenie elementów z menu Element i Color . Przejdź do Resource View i rozwiń listę zasobów. Kliknij prawym przyciskiem folder Menu, aby otworzyć menu kontekstowe - to kolejny przykład tego , co staramy się utworzyć dla programu Sketcher. Wybierz Jnsert Menu, aby utworzyć nowe menu. Ma ono przypisany domyślny identyfikator IDR_MENU1, jednak możesz go zmienić. Wybierz nazwę nowego menu w panelu Resource View i wyświetl jego okno właściwości - w tym celu naciśnij Alt+Enter (to skrót do elementu menu View/Other Windows/Properties). Możesz teraz zmienić identyfikator zasobu w oknie Prop erties. Zmień go na bardziej odpowiedni, np. IDR_CURSOR_MENU. Zauważ, że nazwa zasobu menu musi rozpoczynać się od IDR. Naciśnij klawisz Enter, aby zapisać nową nazwę .

Utwórz teraz dwa nowe elementy paska menu w panelu Editor. Mogą one mieć dowolne stare podpisy, ponieważ i tak będą niewidoczne dla użytkownika. Reprezentują one dwa menu kontekstowe, które będą dostępne w programie Sketcher, więc nazwij je el ement i no el ement, zgodnie z sytuacją, w której będą używane. Teraz dodaj elementy Move i Delete do karty el ement. Domyślne identyfikatory, lO_ELEMENT~OVE i lO_ELEMENT_DELETE, są odpowiednie, lecz jeżeli chccszje zmienić, możesz to zrobić w oknie właściwości każdego z nich. Rysunek 15.12 obrazuje, jak wygląda nowe menu .

Rysunek 15.12

Sketcher.rc (.:.~MENU ~Meiluj·tL. no element ~ h';::'

element

L,!..,.,

J

~_~~_ .~

~~~· .2L

Move Delete

Drugie menu zawiera listę dostępnych typów elementów i kolorów, czyli elementów menu Element i Color z głównego paska menu, ale oddzielonych separatorem. Identyfikatory użyte dla tych elementów muszą być takie same jak w menu IDR_Sketch erTY PE. Jest tak dlatego, ponieważ procedura obsługi menu jest powiązana z identyfikatorem menu . Elementy menu z tym samym identyfikatorem używają tej samej procedury, więc ta sama procedura będzie użyta dla elementu Line, niezależnie od tego, czy zostanie wywołana z menu głównego, czy też z menu kontekstowego. Dostępny

jest skrót, dzięki któremu nie musimy po kolei tworzyć tych elementów menu. menu IDR_SketcherTYPE i rozwiń Element, a następnie wybierz wszystkie elementy menu poprzez kliknięcie pierwszego elementu, a następnie ostatniego, trzymając wciśnięty klawisz Shift . Potem kliknij zaznaczenie prawym przyciskiem myszy i wybierz Capy z menu Wyświetl

848

Vlsual C++ 2005. Od podstaw kontekstowego lub po prostu n a ci śnij Ctrl-sC. Przejdź do !DRJUR SOR_ME NU i kliknij prawym przyciskiem myszy pierwszy element w menu no element. Jeżeli wy bierzesz Paste z menu kontekstowego lub naciśniesz Ctr/ + V, zost ani e wstaw iona c ał a za w a rt ość menu Element. Skopiowane elementy menu b ędą miały takie same identyfikatory jak oryginały. Aby wstawić separator, kliknij prawym przyciskiem pusty element menu i wyb ierz Insert Separator z menu kontekstowego. Powtórz ten proces dla elementów menu Color . Umieszczenie razem elementów menu Element i Color spowodowało konflikt, ponieważ elementy Rectangl e i Red mają ten sam skr ót. Zmiana &Red na Re&d ro zw iąże ten problem, a dla spójności dobrze byłoby to zrobi ć również w menu IORSketcher TYPE. W tym celu należy zmienić właś ciwo ś ć Cap tion elementu menu . Ukończone menu powinno wyg l ąd ać j ak na rysunku 15.13 .

Rysunek 15.13

IV5ketch~r :rc (:, ::",~,~;.:;u=--~M:::=~ enU).·te,..........-.='--'-"_ _-"-'-'-'--"-'_-'--'.........~'-='-"- =- I element

:~? ~~ I

TYr'"

riGetElement Type( ); menu.CheckMenultem (ID_ELEMENT_LINE . (LINE==ElementType?MF_CHECKED:MF_UNCHECKED) IMF_BYCOMMANO): menu .CheckMenultem( ID_ELEMENT_RECTANGLE. (RECTANGLE==El ementType?MF CHECKED'MF UNCHECKED)IMF BYCOMMAND ):

Rozdział 15.

• Tworzenie dokumentu i poprawianie widoku

855

menu .CheckMenuIt emCI D ELEME NT CIRC LE.

C CIRCLE~~E lemen t Ty pe? M F _CH E C K E O : MF_UN C HE C KE D ) I M F_BYCOM M A NO) :

menu .CheckMenuIt emC IO_ELEMENT_CURVE. CCURVE==Element Type?MF_CHECKEO: MF_UNCHECKEO) IMF_BYCOMMAND): }

CMenu* pPopup = menu. GetSubMenuC m_pSelected == o ? l : O); ASSERTC pPopup ł= NULL) ; pPopup- >TrackPopupMenuCTPM_LEF TALIGN I TPM_RIGHT8UTTON. po i nt .x. point. y. t hlS) . Dzięki

tej zmianie opcj e menu kont ekstow ego powinny przebudujesz i uruchomi sz program Sketcher.

by ć

poprawn ie zaznaczone, gdy

Podświellanie elementów Najlep iej był oby, gdyby użytko wn i k w i edz iał , który element znajduje si ę pod kursorem, zanim kliknie prawym przyciskiem w celu wy świetlenia menu kontekstowego. Gdy usuwasz element, chcesz wiedz i eć, z którym elementem pracujesz. Podobnie, jeżeli chcesz użyć drugieg o menu kontekstowego - na przykład w celu zmiany koloru - musisz mi eć pewno ść , że pod kursorem nie ma ż a dnego elementu. Aby do kładni e pokazać , który element znaj duje s i ę pod kursorem, musisz go w jakiś sposób w yróżn i ć przed klik nię ci em prawym przyci skiem myszy. Mo żemy

to zro bić w funkcji s kłado wej Draw( ) dla elementu . Musimy jedynie przesła ć argument do funkcji Draw(), aby w skazać, kiedy element powinien zo stać p odś w i etlony. Jeżeli prześlemy do funkcji Oraw( ) adres aktualn ie wybranego elementu , który jest przechowywany w składowej m_pSel ect ed widoku, będz iemy mogli poró wn a ć go ze w skaźniki em t hi s, aby sp rawdz ić, czy je st to bi e ż ący element. Wszystkie podświetlenia działają w ten sam sposób , więc pr zedstawię je na przykładzi e skła ­ dowej CLi ne. Będz ie sz mógł d odać podobn y kod do wszystkich klas dla innych typów elementów. Zanim zaczniesz zmieniać CLi ne, musisz najpierw z m i e n i ć definicję klasy bazowej CEle ment :

class CElement : public COb ject {

prot ected : COLORREF m_Color ; CRect m_Enclosi ngRect : int m_Pen:

II Kolor elementu. II Prost o kąt opisujacy element. II Szerokos ć p ióra.

publi c: virt ua l -CElement () :

II Wirtualny destruktor.

II Wirtualna o eracia rysowa nia.

vi rt ua l vOid Oraw(COC* pOC.CElement* pElement =O) {} virtual voto Move(CSize&aS ize){} II Przenieś element. CRect Get8oundRect() ; prot ected: CElenent O: };

II Pob ierz prostoką t ograniczający element.

II Umieszczamy tu/aj , aby zapo biec wywa laniu .

856

VisualC++ 2005. 0[1 podstaw Zmiana polega na dodaniu drugiego parametru do funkcji wirtualnej Draw() . Jest to wskaźnik do elementu. Drugi argument inicjalizujemy z warto ścią O, pon ieważ chcem y p ozwolić na uży­ cie tej funkcji tylko z jednym argumentem; jako drugi argument domyślnie będzie podane O. W ten sam sposób musimy zmien i ć deklaracj ę funkcji Draw() w każd ej klasie wyprowadzanej z CEl ement. Na przykład definicję klas y CL i ne nale ży zmieni ć w następujący sposób:

cl ass Cli ne : publ ic CE lement publ ic:

-u.tner voi d) II Funkcj a

:

wyświetlająca lin ię .

vi rtual void Oraw(COC* pOC , CElement* pElement=O ) ; II Konstru ktor obiektu linii.

Cl lne(CPoint Start, CPoint End, COlORREF aColor): II Funk cja przen o sząca element.

vir tual vo id Move (CS ize&'a Size); protected: CPo int m_St artPoint ; CPoint m_End Point :

II Początek linii. II Koniec linii.

Cl ine(vo id):

II Domyślny konstruktor - nie powinien

być używany.

}:

Implementacje wszystkich funkcji Draw () dla klas wyprowadzanych z CE l ement rozbudowane w ten sam sposób. Funkcja dla klasy CLi ne wy gl ąda następująco :

void Cl ine: :Oraw( COC* pOC . CElement* pE lement) { II Tworzy pi óro dla tego elementu i II inicj alizuje je z obiektem koloru i

linią

o gru b oś ci l p iksela.

CPen aPen; COlORREF aColor = m_Color : II l nicj alizuj z kolorem elementu. if (t hi s == pElement) II Czy ten element jest wybrany? II Ustaw kolor wyróż nienia.

aColor = SElECT_COlOR: i f( faPen.CreatePen(PS SOLID. m Pen. aCo lor)) II Tworz enie pióra nie po wiodlo się.

zako ńcz

program.

AfxMessageBox( _T(" Pen creat ion fail ed drawing a l ine"). MB_OK): AfxAbo rt( ): CPen* pO ldPen II Teraz narysuj

~

oOC- >SelectObject(&aPen) :

II Wy bierz pi óro.

linię.

pOC ->MoveTo( m_St art Pol nt) ; pOC ->L ineTo(m_EndPoint) : pOC->Select Object (pOl dPen);

II Przywró ć star e p ióro.

mu szą być

Rozdział 15.

• Tworzenie dokumentu i poprawianie widoku

857

Jest to bardzo prosta zmiana. Ustawiamy nową zm ienną lokalną aCo l or na bie ż ący kolor przechowywany w m_Co lo r , natomiast instrukcja if usta wia ponownie warto ś ć aCo lor na SELECT_COLOR, gdy pEl ement jest równy th i s, czyli w przyp adku, gdy bi eżący element i wybran y element są tym samym . Musi sz także dodać definicj ę SELECT_COLORdo pliku Our tlonstants. h: // Definicje stalych.

#pragma ance // Defin icje typów elementów. // Wa rtość każ dego typu musi być

canst canst canst canst

unsi gned unsigned un signed unsigned

i nt i nt int int

różna .

LINE = lOlU ; RECTANGLE = l02U; CIRCLE = l 03U; CURVE ~ l04U ;

/////////////////////////////////// //

War tośc i

canst canst canst canst canst

kolorów rysow ania.

COLORREF BLACK ~ RGB (O .O.O ); COLORREF RED = RGB(255,O. O); COLORREF GREEN = RGB( O.255.0) ; COLORREF BLUE = RGB(O .O,255) ; COLORREF SELECT COLOR = RGB(255.0,180) ;

///////////////////////////////////

Musimy także dodać dyrektywę #i ncl ude dla pliku Ourtlon stants.h do pliku CE l ements .cpp, aby ud ostępnić definicję SELECT_COLOR. Prawie zaimplementowaliśmy podświetl anie . Klasy wyprowadz ane z klasy CEl ement mogą się teraz same rysować , gdy zostaną wybrane - potrzebujemy jedynie mechanizmu po wodującego wybran ie elementu. Gdzie powinniśmy go umieści ć? W procedurze OnMouseMove ( l klasy CSketch erVi ew okre ślamy, który i czy w ogóle ja kiś element znajduje się pod kursorem, jest to w i ę c z pewnością odpowiednie miejsce dla mech anizmu pod świetleni a . Do procedury Onr1ouseMove ()

należy wprowad zić następując e

zmiany;

vaid CSket cherView; ;OnMauseMave(UI NT nFlags . CPai nt paint ) ( // Definiuje ob iekt kontekst u

urządzen ia

CCl ient DC aDC( th i s); OnPrepareDC(&aDC) ;

d la widoku. // Kont ekst urządz enia dla tego widoku. // Ustaw tryb rysowania .

aOC.SetROP2 (R2_NOTXORPEN ); // Ustaw tryb rysowa nia. i f«nFlags&MK_LBUTTON) &&(t his==Get Capt ure())) (

aDC.OPt oLP(&point ); m_SeCandPal nt = paint ;

// Konwertuj punkt na uklad logi czny. // Zapisz bieżącą pozycję kursora .

i f(rn_pTempElement l (

if( CURVE {

~=

GetOocument ( )->Get El ementType() )

// Ry sujemy krzywą, // więc dodaj odcinek do

istn iejącej

// Czy j est to krzywa?

krzy wej.

st at lc_cast (m_pTempE lement )->AddSegment (m_SecandPo int ) ; m_pTempEl ement- >Oravl(&aOC) . // Terazją narysuj. retur n; // Skończyliśmy.

858

Visnal C++ 2005. Od podstaw aOC.SetROP2( R2_NOTXORPEN ) : II Przerysuj stary elem ent, aby

zniknął z

m_pTempElement->Oraw( &aOC) : del ete m_pTempElement : m_pTempE lement = O:

II Ustaw tryb rysowania. widoku