E:\Moje dokumenty\HELION\Linux Unleashed\Indeks\27.DOC 459
Rozdzia³ 27. ¨ Programowanie w C++ 459
Rick McMullin
W tym rozdziale:
u Język C++
u Klasy i metody
u Opcje kompilatora GCC
u Opcje współpracy z debugerem i programem profilującym
u Wyszukiwanie błędów w aplikacjach C++
u Wyszukiwanie błędów w funkcjach wirtualnych
u Wyszukiwanie błędów w funkcjach obsługi wyjątków
u Polecenia gdb specyficzne dla C++
u Biblioteki klas GNU C++
C++ to wersja języka C rozszerzona o obsługę obiektów. Język ten opracowany został w Bell Labs na początku lat osiemdziesiątych i szybko zyskał sobie uznanie i popularność wśród programistów. Na rynku istnieje obecnie kilkanaście różnych kompilatorów tego języka. Najpopularniejsze z kompilatorów przeznaczonych dla platformy PC to Borland C++, Microsoft Visual C++, Zortech C++ i Watcom C++. Przeznaczone są one do tworzenia programów działających w systemach DOS i Windows, choć niektóre z nich potrafią również tworzyć programy pracujące pod kontrolą Windows NT i OS/2. Oczywiście istnieje również wiele kompilatorów języka C++ przeznaczonych dla innych platform sprzętowych.
W przypadku większości systemów UNIX-owych kompilatory języka C++ rozprowadzane są przez dystrybutorów systemu. Podobnie jest w przypadku Linuxa. Kompilator C++, który kiedyś nazywał się g++, jest bardzo blisko spokrewniony z GCC. W wersji 2.0 oba te kompilatory zostały scalone w jeden program.
Obecnie GCC to połączenie kompilatorów języków C, C++ oraz C z obiektami. W systemie nadal znajduje się plik o nazwie g++, ale jest on tylko skryptem wywołującym kompilator GCC z odpowiednimi opcjami.
C++ i programowanie obiektowe (ang. object-oriented programming, OOP) nie powstały od razu. Wiele lat temu, gdy na świecie panowały komputery ośmiobitowe, programiści zamiast języka maszynowego zaczęli używać asemblera, wykorzystując nieco większe możliwości obliczeniowe następców komputerów czterobitowych. Umożliwiło to przerzucenie części pracy związanej z tworzeniem programu na komputer.
Z upływem czasu moc obliczeniowa komputerów rosła i możliwe stało się pisanie coraz bardziej skomplikowanych programów. Było to jednak coraz trudniejsze. Powstawać zaczęły kompilatory języków wyższego poziomu, które brały ogrom „czarnej roboty” na siebie. Pierwsze języki były językami strukturalnymi – na przykład FORTRAN, COBOL, Pascal czy C. Strukturalizacja programów pozwalała na ich uproszczenie, zwiększenie przejrzystości, a przede wszystkim zmniejszenie liczby błędów dzięki podzieleniu zadania na mniejsze problemy (funkcje i procedury), łatwiejsze do rozwiązania.
Programowanie strukturalne dobrze spełniało swoją funkcję, ale znów tylko do pewnego czasu. W miarę wzrostu rozmiarów aplikacji zaczęło zawodzić. Programowanie zorientowane obiektowo powstało jako odpowiedź na problemy stwarzane przez programowanie strukturalne.
Główne nowe pojęcia wprowadzane przez OOP to:
u hermetyzacja danych,
u dziedziczenie,
u polimorfizm.
Przy programowaniu strukturalnym problemy pojawiały się najczęściej wtedy, gdy różne funkcje czy moduły programu korzystały ze wspólnych danych. Dowolny fragment programu mógł odwoływać się do danych bez poinformowania o tym innego fragmentu.
Hermetyzacja danych ma na celu zabezpieczenie przed tego typu problemami. Polega ona na zgrupowaniu wspólnych danych, zapisaniu ich w odpowiednim typie i zapewnieniu spójnego interfejsu dostępu do nich. Dzięki temu nikt nie może skorzystać z danych, pomijając ten interfejs.
Najważniejszą zaletą takiego rozwiązania jest zabezpieczenie danych przed ich nieautoryzowaną bezpośrednią modyfikacją. Często bowiem zmiana danych musi wiązać się z jakimiś ściśle określonymi czynnościami. Poza tym zmiana struktury danych nie jest widziana na zewnątrz samej struktury, o ile tylko interfejs dostępu do danych pozostanie niezmieniony. Znacząco upraszcza to tworzenie i poprawianie bardziej złożonych programów.
W języku C++ hermetyzację danych zapewnia mechanizm klas.
Dziedziczenie pozwala na ponowne użycie części kodu. Zwykle stosowane jest w przypadku, gdy jakaś część programu posiada zasadniczo wszystkie cechy posiadane przez inną część, plus kilka innych, na przykład gdy jeden obiekt jest szczególnym przypadkiem drugiego.
Dziedziczenie jest w języku C++ zaimplementowane jako dziedziczenie klas.
Polimorfizm umożliwia zdefiniowanie funkcji wykonujących różne działania w zależności od typu danych, na których operują. Moc tego mechanizmu ujawnia się, gdy informacje przesyłane są poprzez klasę bazową do klas pochodnych, i dla każdej z nich mogą one oznaczać coś innego.
Polimorfizm w języku C++ zaimplementowany jest poprzez funkcje wirtualne.
Klasy w języku C++ można w zasadzie wyobrażać sobie podobnie jak struktury w języku C, z tym, że oprócz danych mogą one zawierać również funkcje, które na tych danych mogą być wykonywane. Rozpatrzmy na przykład typ danych reprezentujący bryłę geometryczną. Bryły mogą mieć najróżniejsze kształty, ale wszystkie mają kilka wspólnych atrybutów, na przykład pole powierzchni czy objętość. Można zatem zdefiniować w języku C strukturę o nazwie bryla:
struct bryla { float pole; float obj;}
Jeśli do tej definicji dołączymy jeszcze kilka funkcji, otrzymamy odpowiednik klasy:
struct bryla { float pole; float obj;
float licz_pole();
float licz_obj();};
W ten sposób zadeklarowaliśmy klasę w języku C++. Funkcje wchodzące w skład klasy nazywa się metodami. Zmienną typu bryla nazywa się obiektem. Można ją zadeklarować w następujący sposób:
bryla kula1;
Obiekt jest fizyczną realizacją klasy, podobnie jak zmienna była realizacją typu.
W tym podrozdziale opiszemy najczęściej używane opcje kompilatora GCC. Najpierw przyjrzymy się opcjom wspólnym dla języków C i C++, a następnie przejdziemy do omówienia opcji specyficznych tylko dla języka C++. Każda opcja dostępna dla języka C może być użyta dla języka C++ (choć nie zawsze ma to sens – w takim przypadku zostanie ona zignorowana).
Do kompilowania programów napisanych w języku C++ najłatwiej użyć skryptu g++, ponieważ ustawia on automatycznie wszystkie wymagane opcje.
Do kompilatora GCC przekazać można bardzo dużo różnego rodzaju opcji. Większość z nich uzależniona jest od konkretnej platformy sprzętowej albo służy do dokładnego „dostrajania” wygenerowanego kodu i prawdopodobnie nigdy nie będziesz musiał ich użyć. Poniżej omówimy opcje, które przydadzą Ci się w codziennej pracy.
Wiele opcji programu GCC wymaga podania więcej niż jednej litery. Z tego powodu nie jest możliwe grupowanie ich po wspólnym myślniku, jak ma to miejsce w przypadku większości programów systemu Linux. Przed każdą opcją znaleźć się musi osobny myślnik.
Skompilowanie programu bez podania żadnych opcji powoduje utworzenie pliku wykonywalnego o nazwie a.out. Jeśli chcesz, aby program wynikowy miał inną nazwę, powinieneś użyć opcji –o, na przykład jeśli chcesz, aby program wygenerowany na podstawie kodu źródłowego zapisanego w pliku o nazwie licznik.C (rozszerzenie .C oznacza, że plik zawiera program w języku C++, w przeciwieństwie do rozszerzenia .c, oznaczającego kod w języku C) miał nazwę licznik, powinieneś wydać polecenie:
gcc –olicznik licznik.C
Po opcji –o w wierszu poleceń nie powinno być spacji! Nazwa pliku wykonywalnego musi pojawić się bezpośrednio po opcji –o.
Inne opcje pozwalają decydować, na jakim etapie proces kompilacji ma zostać zakończony. Opcja –c powoduje pominięcie konsolidacji (konwersji plików pośrednich na plik wykonywalny), tworząc tylko skompilowane pliki pośrednie (z rozszerzeniem .o). Jest ona szczególnie przydatna przy tworzeniu większych programów, ponieważ pozwala uniknąć wielokrotnego kompilowania plików, które nie zostały zmodyfikowane.
Opcja –S powoduje zatrzymanie kompilacji po wygenerowaniu plików asemblera (z rozszerzeniem .s). Opcja –E powoduje tylko wstępne przetworzenie plików źródłowych i wykonanie dyrektyw preprocesora, wyniki przesyłając do standardowego urządzenia wyjściowego.
Program GCC posiada również kilka opcji umożliwiających współpracę z debugerem i programem profilującym. Najczęściej używane z nich to –gstabs+ oraz –pg.
Opcja –gstabs+ powoduje dołączenie do kodu programu wykonywalnego dodatkowych informacji dla debugera gdb, co znacznie ułatwia wyszukiwanie usterek w programach. Proces wyszukiwania błędów w programach napisanych w języku C++ omówiony jest dokładniej w podrozdziale „Wyszukiwanie błędów w programach C++” w dalszej części tego rozdziału.
Opcja –pg powoduje dołączenie kodu generującego informacje o ilości czasu poświęconego na wywołanie każdej z funkcji programu, które mogą być następnie przetworzone przez program gprof. Jeśli chcesz dowiedzieć się czegoś więcej o tym programie, zajrzyj do podrozdziału „gprof” w rozdziale 26. „Programowanie w języku C”.
Opcje pozwalające na modyfikację sposobu kompilacji programów w języku C++ zebrane zostały w tabeli 27.1.
Tabela 27.1. Opcje kompilacji specyficzne dla C++
Opcja
Znaczenie
-fall-virtual
Traktowanie wszystkich możliwych funkcji składowych jako wirtualnych (wszystkich prócz konstruktorów oraz przeciążonych operatorów new i delete)
-fdollars-in-identifiers
Zezwolenie na używanie znaku $ w identyfikatorach; opcja zabraniająca jego użycia: -fnodollars-in-identifiers
-felide-constructors
Pomijanie konstruktorów tam, gdzie to możliwe
-fenum-int-equiv
Zezwolenie na niejawną konwersję z typu wyliczeniowego do int
-fexternal-templates
Generowanie mniejszego kodu dla szablonów – kompilator generuje tylko jedną kopię żądanej funkcji tam, gdzie jest ona zdefiniowana
cd. na następnej stronie
Tabela 27.1. cd. Opcje kompilacji specyficzne dla C++
-fmemorize-lookups
Użycie technik heurystycznych dla przyspieszenia kompilacji; opcja ta jest domyślnie wyłączona, ale różnice są widoczne tylko przy określonym typie danych wejściowych
-fno-script-prototype
Traktowanie deklaracji funkcji bez argumentów w ten sam sposób, jak w języku C (czyli akceptowanie dowolnej liczby argumentów)
-fno-null-objects
Założenie, że obiekty, do których odnoszą się referencje, są zainicjalizowane
-fsave-memorized
To samo co –fmemorize-lookups
-fthis-is-variable
Zezwolenie na przypisanie do wskaźnika this
-nostdinc++
Pominięcie wyszukiwania plików nagłówkowych w standardowych katalogach C++
-traditional
To samo co -fthis-is-variable
-fno-default-inline
Funkcje zdefiniowane w ciele klasy nie są traktowane jako inline
-wenum-clash
Generowanie ostrzeżeń przy konwersji pomiędzy typami wyliczeniowymi
-woverloaded-virtual
Generowanie ostrzeżeń, gdy klasa pochodna może być zdefiniowana błędnie przez zadeklarowanie funkcji wirtualnych; funkcja wirtualna w klasie pochodnej musi być wirtualna również w klasie bazowej
-wtemplate-debugging
...
Wolf-1