Przedmowa
C++ należy do grupy obiektowych języków programowania. Korzenie tych języków sięgają końca lat sześćdziesiątych; rok 1967 uważa się za datę powstania pierwszego języka obiektowego, Simula-67. Język Simula-67 bazował na wcześniejszej pracy nad specjalizowanym językiem Simula-1, przeznaczonym do symulacji zdarzeń dyskretnych, zachodzących w reaktorach jądrowych. Twórcami Simuli byli dwaj badacze norwescy, Kristen Nygaard i Ole-Johan Dahl, pracujący w Norweskim Centrum Obliczeniowym. W języku tym wprowadzono szereg pojęć i koncepcji, które z niewielkimi zmianami przejęły współczesne języki obiektowe. W szczególności, obok typów wbudowanych (integer, real, Boolean), podobnych do stosowanych w najbardziej wówczas znanym języku proceduralnym ALGOL, wprowadzono w Simuli pojęcie klasy. Wprowadzono również mechanizm dziedziczenia, który przekazuje klasie potomnej cechy klasy rodzicielskiej.
Kolejnym znaczącym krokiem w rozwoju języków obiektowych było opracowanie przez Alana Kay z Xerox PARC języka Smalltalk. Język ten powstał w latach 1970-1972 i posłużył jako pierwowzór dla współczesnej jego wersji, opracowanej przez Adele Goldberg w roku 1983; wersja ta znana jest obecnie pod nazwą Smalltalk-80.
Okresem szczególnej aktywności w badaniach nad językami programowania były lata siedemdziesiąte. Powstało wtedy prawdopodobnie kilka tysięcy różnych języków i ich dialektów, ale przetrwało w szerszym obiegu zaledwie kilka. Tak więc na rynku utrzymały się języki: Smalltalk, Ada (sukcesor języków ALGOL 68 i Pascal, z pewnym wkładem od języków Simula, Alphard i CLU), C++ (pochodzący z mariażu języków C i Simula), Eiffel (oryginalne dzieło Bertranda Meyera, bazujący po części na Simuli) oraz obiektowe wersje języków Pascal (Object Pascal, Turbo Pascal) i Modula-2 (Modula-3).
Powstało również szereg języków obiektowych dla konstruowania systemów ekspertowych oraz tzw. sztucznej inteligencji. Wśród nich także nastąpiła ostra selekcja i powszechnie stosowanych języków pozostało niewiele; można tu wymienić dwa szerzej znane: CLOS (akronim od Common Lisp Object System) oraz CLIPS (akronim od C Language Integrated Production System), który po rozszerzeniu o mechanizm dziedziczenia znany jest obecnie pod nazwą COOL (akronim od. CLIPS Object-Oriented Language).
Spróbujmy teraz określić, jakie wspólne własności mają wszystkie wymienione języki. Obecnie uważa się, że język programowania można uznać za obiektowy, jeżeli spełnia następujące wymagania:
• Pozwala definiować klasy i ich wystąpienia, nazywane obiektami. Definicja klasy w języku obiektowym jest podobna do definicji abstrakcyjnego typu danych w językach proceduralnych (typu, który nie jest wbudowany, lecz może być zdefiniowany przez programistę). W językach Eiffel, Smalltalk i Simula klasa jest niepodzielną jednostką syntaktyczną, zawierającą definicje struktur danych i definicje operacji wykonywanych na tych strukturach. Natomiast w językach hybrydowych, jak np. Object Pascal, Turbo Pascal, CLOS i C++ klasa jest traktowana jako “deklaracja typu” umieszczana w jednym pliku, a definicje operacji (nazywane metodami, funkcjami składowymi, lub funkcjami pierwotnymi) umieszcza się zwykle w innym pliku. Konkretna operacja może być implementowana za pomocą jednej tylko metody lub wielu metod. W pierwszym przypadku nazywamy ją monomorficzną, zaś w drugim Ä polimorficzną albo wirtualną. Wystąpienia obiektów danej klasy są tworzone według tego samego wzorca, zawartego w definicji klasy. W rezultacie wszystkie obiekty danej klasy mają takie same struktury danych (atrybuty) i operacje. Tym niemniej każdy obiekt ma własną “tożsamość”, a więc, po utworzeniu, istnieje niezależnie od innych obiektów tej samej klasy.
• Zapewnia ukrywanie informacji (hermetyzację, ang. encapsulation), co można rozumieć jako zamknięcie obiektu w swego rodzaju “czarnej skrzynce” lub “kapsułce”. W większości języków obiektowych hermetyzacja nie zawsze jest pełna, ponieważ zawierają one mechanizmy kontroli dostępu do elementów klasy. Jako zasadę przyjmuje się ukrywanie przed użytkownikiem definicji struktur danych i definicji operacji, przy czym same operacje (lub tylko część z nich) są publicznie dostępne. Zbiór publicznie dostępnych operacji dla obiektu danej klasy nazywa się często publicznym interfejsem obiektu. Taka konstrukcja klas jest logiczna, ponieważ dla użytkownika klasy istotne jest tylko to, jak się nazywa dana operacja, do czego służy i jak się ją uaktywnia (wywołuje). Hermetyzację realizuje się zwykle w ten sposób, że użytkownik nie ma dostępu do kodu źródłowego z definicjami klas i operacji, a jedynie do publicznych interfejsów. Dzięki temu zapewnia się ochronę klas (przede wszystkim predefiniowanych klas bibliotecznych) przed nieuprawnionym dostępem.
• Posiada wbudowany mechanizm dziedziczenia, dzięki któremu można tworzyć klasy potomne (podklasy, klasy pochodne) od jednej lub kilku klas rodzicielskich, nazywanych superklasami lub klasami bazowymi. Klasy potomne mogą z kolei być klasami rodzicielskimi dla swoich klas pochodnych, co pozwala tworzyć “drzewa” (hierarchie) klas. W praktyce klasa bazowa jest na ogół prostą konstrukcją językową, zaś klasa pochodna jest jej specjalizacją, co zwykle prowadzi do rozszerzenia definicji klasy bazowej o nowe elementy. Elementami tymi mogą być nowe struktury danych i nowe metody, bądź też operacje o tych samych nazwach co w klasie bazowej, ale o innych definicjach.
Mechanizm dziedziczenia jest niezwykle efektywny: nie wymaga kopiowania kodu źródłowego klasy bazowej, ponieważ klasa pochodna automatycznie dziedziczy wszystkie lub wybrane cechy klasy bazowej. W rezultacie mamy lepszą, bardziej przejrzystą organizację programu.
• Dysponuje cechami polimorfizmu. Polimorfizm jest terminem zapożyczonym z biologii i oznacza dosłownie wielopostaciowość. W odniesieniu do programów obiektowych polimorfizm można określić krótko: jeden interfejs (operacja), wiele metod. Najprostszą postacią (wbudowany polimorfizm ad hoc) polimorfizmu jest wykorzystanie tego samego symbolu dla semantycznie nie związanych operacji. Polimorfizm tego rodzaju jest charakterystyczny dla większości współczesnych języków programowania wysokiego poziomu, nie tylko obiektowych. Przykładem może być używanie tych samych symboli operacji arytmetycznych, np. symbolu mnożenia “*”, przy mnożeniu liczb całkowitych i rzeczywistych, chociaż w każdym konkretnym przypadku będzie wywoływana inna metoda mnożenia. Realizacja takiego wywołania wymaga wyznaczenia adresu danej metody; jeżeli adresy odpowiednich metod są przekazywane w fazie kompilacji, to proces ten nazywany jest wiązaniem wczesnym lub statycznym. W językach obiektowych mechanizm wiązania wczesnego wykorzystuje się również przy przeciążaniu operatorów, tj. nadawaniu innego znaczenia operatorom wbudowanym w język oraz (co jest specyfiką języka C++) przy przeciążaniu funkcji. Jednak o sile języka obiektowego decyduje możliwość wykorzystania wiązania późnego (dynamicznego), gdy adres wywoływanej metody staje się znany dopiero w fazie wykonania programu. Wiązanie późne występuje dla hierarchii klas zaprojektowanej w taki sposób, że zdefiniowane w pierwotnej klasie bazowej (“korzeniu” drzewa klas) metody są redefiniowane w klasach pochodnych z zachowaniem tej samej nazwy, typu, liczby i typów argumentów. Tego rodzaju metody nazywa się wirtualnymi. Zauważmy, że konstrukcja taka nie pozwala na związanie wywołania operacji z jej metodą w fazie kompilacji, ponieważ postać wywołania jest dokładnie taka sama dla każdej operacji w całej hierarchii klas. Dopiero w fazie wykonania, gdy zostanie utworzony obiekt odpowiedniej klasy, można wywołać metodę wirtualną dla tego obiektu.
Język C++ zawiera wszystkie wymienione cechy, a ponadto takie, które czynią go bardzo efektywnym; dzięki temu staje się de facto standardem przemysłowym. De facto, ponieważ dotychczasowe prace komitetów normalizacyjnych ANSI X3J16 oraz ISO WG-21 doprowadziły jedynie do opublikowania w roku 1994 zarysu standardu. Tekstem źródłowym dla obu komitetów była wykładnia języka, opublikowana w książce Margaret Ellis i twórcy C++, Bjarne Stroustrupa, The Annotated C++ Reference Manual [2].
Pierwotnym zamysłem Stroustrupa było wyposażenie bardzo efektywnego języka C w klasy, wzorowane na klasach Simuli. Zrealizował go w roku 1980, konstruując preprocesor rozszerzonego w ten sposób języka C. Nowy język, nazwany “C with classes”, był wtedy traktowany raczej jako dialekt języka C, chociaż zawierał już większość cech języka C++ (dziedziczenie, kontrola dostępu, konstruktory i destruktory, klasy zaprzyjaźnione, silna typizacja). W latach 80-tych Stroustrup wraz z grupą współpracowników wprowadzał dalsze mechanizmy i konstrukcje językowe (funkcje rozwijalne, argumenty domyślne, przeciążanie operatorów i funkcji, referencje, stałe symboliczne, zarządzanie pamięcią, dziedziczenie mnogie, funkcje wirtualne, szablony klas i funkcji, obsługa wyjątków), co wraz ze ściślejszą kontrolą typów doprowadziło język C++ do obecnego kształtu.
C++ jest językiem wysoce modularnym; każdy program można zdekomponować na oddzielnie kompilowane moduły z publicznym interfejsem i ukrytą implementacją. I odwrotnie: do każdego programu można dołączać wcześniej opracowane moduły, przechowywane na dysku w postaci tzw. plików nagłówkowych.
Kluczowym pojęciem w C++ jest klasa, traktowana jako typ definiowany przez użytkownika. W definicji języka nie przewidziano standardowej biblioteki klas jako części środowiska programowego, chociaż opracowanie [4], sygnowane przez AT&T, zawiera opis i sposób korzystania z bibliotek wejścia/wyjścia. W praktyce można więc spotkać wiele bibliotek klas, opracowanych niezależnie od standardu AT&T, jak np.
* bibliotekę NIH (USA, National Institutes of Health), wzorowaną na bibliotece języka Smalltalk;
* bibliotekę Interviews, która pozwala na dogodne używanie systemu X Window z poziomu C++;
* bibliotekę GNU C++ (g++), opracowaną w ramach projektu GNU;
* biblioteki dla tworzenia obiektów trwałych (POET, ObjectStore, ONTOS, Versant).
* biblioteki specjalizowane, jak np. RHALE++ (dla obliczeń matematycznych w fizyce), SIMLIB (dla symulacji sieci przełączanych).
Z bieżących informacji wynika, że komitety ANSI/ISO przyjęły już standardy dla następujących klas bibliotecznych: array (szablon tablic), dynarray (szablon tablic dynamicznych), string (szablon łańcuchów), wstring (szablon łańcuchów z rozszerzonym kodem znaków), bits<N> (szablon zbioru bitowego o ustalonej liczności), bitstring (szablon zbioru o zmiennej liczności) i complex (liczby zespolone). W najbliższym czasie można się spodziewać przyjęcia standardów dla szablonów klas: vector, list i associative array (map).
Ze względu na brak niektórych standardów, biblioteki klas są używane w niniejszej książce raczej oszczędnie; prawie wyłącznie będą to biblioteki wejścia/wyjścia z bardzo nielicznymi odstępstwami. Dzięki temu zamieszczone w tekście przykłady (a jest ich ponad 160) były kompilowane i wykonywane zarówno w środowisku Windows'95 (kompilator Borland C++, wersja 5.01, kompilator Visual C++ v.4.0), jak i w środowisku Unix (kompilatory CC, GNU gcc, GNU g++, v.2.8.1).
Prezentowany tekst należy traktować raczej jako wstęp do programowania w języku C++, a nie jako wyczerpujący podręcznik (zarówno w sensie kompletności wykładu, jak i znużenia potencjalnego czytelnika). W stwierdzeniu tym nie należy upatrywać samokrytyki; w książce o rozsądnej objętości można dokładnie opisać składnię i semantykę języka C++, ale pragmatyka może mieć potencjalnie (i ma) tak wiele kontekstów, że nie jest praktycznie możliwe opisanie wszystkich możliwych wariantów i niuansów.
Chociaż język C++ został “nadbudowany” nad językiem C, zrozumienie prezentowanego tekstu, przykładów i programów, nie wymaga umiejętności programowania w języku C. Tym niemniej założono, że czytelnik ma pewne doświadczenia programistyczne w którymś z języków wysokiego poziomu, jak np. Pascal, Modula-2, czy wreszcie wspomniany język C. Bardzo polecam uważne przestudiowanie zamieszczonych przykładów; ponieważ zdecydowana większość z nich to kompletne programy, warto je skompilować i wykonać w dostępnym środowisku programowym. Sądzę, że towarzyszące przykładom dyskusje i analizy programów okażą się interesujące nie tylko dla początkujących, ale i dla zaawansowanych programistów, których uwadze polecam rozdziały 10 i 11, poświęcone obsłudze wyjątków i dynamicznej kontroli typów. Zawarte w tych rozdziałach opisy i dyskusje oparto na ostatnich ustaleniach wspomnianych komitetów ANSI/ISO, a kompilacja i wykonanie programów wymagają dostępu do najnowszych kompilatorów, np. Borland C++ w wersji 5.01 lub CC w wersji 4.0.
Gdańsk, w sierpniu 1998 W. M. Porębski
4
Język C++
3
Wprowadzenie