R02-03, ## Documents ##, C++Builder 5


Programowanie w C++Builderze

Tematyka tego rozdziału koncentruje się na czynności tworzenia kodu w języku C++.

Na początku zajmiemy się problematyką czytelności tworzonego kodu i zaprezentujemy środki prowadzące do jej optymalizacji. Kod czytelny to kod łatwiejszy do zrozumienia i konserwacji, a to przekłada się wprost na niższe koszty zarządzania projektem. Jednym z najważniejszych czynników, wpływających na czytelność kodu, jest wybór i konsekwentne stosowanie odpowiedniego stylu kodowania; w rozdziale tym przedstawimy kilka propozycji stylistycznych i wyjaśnimy, dlaczego niektóre style zapisu kodu lepsze są od innych.

W dalszej kolejności przedstawimy kilka wskazówek dotyczących wybranych konstrukcji języka C++ i ich stosowania w kodzie aplikacji tworzonej z użyciem C++Buildera. Niektóre z tych konstrukcji okazują się być niezrozumiałe dla programistów stawiających pierwsze kroki w C++, niektóre zaś bywają przez nich rozumiane opacznie; programiści ci znajdą tutaj kilka wskazówek, pozwalających im uporządkować wiedzę. Niektóre zaawansowane zagadnienia, jak np. pascalowe dziedzictwo biblioteki VCL C++Buildera, z pewnością zainteresują również programistów bardziej zaawansowanych.

Style kodowania a czytelność programu

W podrozdziale tym zajmiemy się znaczeniem czytelności kodu i zaprezentujemy kilka metod przyczyniających się do jej poprawy. Trzeba stwierdzić, iż niezależnie od wyboru konkretnego stylu kodowania istotne jest jego konsekwentne stosowanie - zamieszczone dalej przykładowe fragmenty programów celowo wykorzystują różne style kodowania, a to pozwala lepiej zrozumieć, jak bardzo niepożądana jest wszelka niespójność w tej materii.

Proste i zwięzłe kodowanie

Jest niemal oczywiste, iż prostota z reguły przyczynia się do - szeroko rozumianej - efektywności, tak więc przystępując do kodowania programu, należy dążyć do tworzenia kodu krótkiego i prostego w interpretacji. Przynosi to dwojakiego rodzaju korzyści.

Po pierwsze, złożony problem zostaje niejako automatycznie rozbity na mniejsze fragmenty, z których każdy jest łatwy do zrozumienia i wykonuje dobrze określone zadanie. Złożoność kodu przekłada się wówczas jedynie na wyższy stopień jego abstrakcji, nie zaś na monstrualne niekiedy rozmiary.

Przyjrzyjmy się bliżej funkcjom na poniższym wydruku:

Wydruk 2.1. Złożoność kodu a stopień abstrakcji

#include <vector>

double GetMaximumValue(const std::vector<double>& Vector)

throw(std::out_of_range)

{

double Maximum = Vector.at(0);

for(int i=0; i<Vector.size(); ++i)

{

if(Vector[i] > Maximum)

{

Maximum = Vector[i];

}

}

return Maximum;

}

void NormalizeVector(std::vector<double>& Vector)

{

if(!Vector.empty())

{

double Maximum = GetMaximumValue(Vector);

for(int i=0; i<Vector.size(); ++i)

{

Vector[i] -= Maximum;

}

}

}

Obydwie funkcje zbliżone są do siebie pod względem złożoności kodu, tymczasem druga z nich wykonuje zagadnienie bardziej skomplikowane niż pierwsza; gdy jednak przyjrzeć się dokładniej ich treści, różnica w stopniu abstrakcyjności kodu staje się natychmiast widoczna - obliczenie maksymalnej wartości wśród elementów wektora wykonywane jest w drugiej funkcji jako operacja elementarna, podczas gdy operacje elementarne w ramach pierwszej funkcji ograniczają się do porównań i podstawień.

Pomijam tutaj TIP z górnej części strony 58 oryginału, bo jest kompletnie bez sensu

Druga ze wspomnianych korzyści wiąże się bezpośrednio z rozmiarem kodu - w kodzie o niewielkich rozmiarach deklaracje zmiennych lokalnych i parametrów funkcji dostępne są niemal natychmiast, bez konieczności nieustannego wertowania wielu nieraz stronic wydruku czy przewijania znacznych rozmiarów wydruku.

Akapitowanie kodu

O czytelności kodu decyduje również jego przejrzysty układ graficzny. W odniesieniu do kodu programu w języku C++ przejrzystość taką osiągnąć można przede wszystkim dzięki przestrzeganiu następującej zasady: każdy z nawiasów {} ograniczających zawartość bloku powinien być jedynym znakiem w swym wierszu, para odpowiadających sobie nawiasów powinna znajdować się w tej samej kolumnie, zaś zawartość bloku powinna być wcięta o kilka kolumn w stosunku do ograniczających go nawiasów (zazwyczaj stosuje się wcięcia o dwie lub cztery kolumny).

Upewnij się, że w edytorze kodu wyłączona jest funkcja automatycznej konwersji ciągów spacji na znaki tabulacji (decyduje o tym opcja Use tab character na karcie General opcji edytora - Tools|Editor Options). Ze względu na potencjalnie odmienne traktowanie znaku tabulacji w różnych edytorach wykorzystywanie go do akapitowania kodu może spowodować, iż układ kodu poprawny w jednym środowisku stanie się karykaturalny w innym.

Układ taki ogromnie ułatwia identyfikację poszczególnych bloków i strukturę ich zagnieżdżania. Widoczne stają się zakresy obowiązywania deklaracji zmiennych lokalnych i ewentualne przypadki błędnego ich użycia poza zakresem. Nie należy zapominać, iż z każdym blokiem związania jest pewna „instrukcja nagłówkowa”, na przykład instrukcja for; jest zrozumiałe, iż instrukcja ta powinna rozpoczynać się w tej samej kolumnie, w której znajdują się nawiasy {} ograniczające podporządkowany jej blok, gdyż w ten sposób łatwo widoczny staje się jej koniec.

Do przesuwania w poziomie zaznaczonych bloków tekstu służą w IDE kombinacje klawiszy Ctrl+Shift+I oraz Ctrl+Shift+U. Wielkość pojedynczego „skoku” (jako liczba kolumn) określona jest za pomocą opcji Block indent na wspomnianej karcie General opcji edytora; ustawienie jej na 1 zapewnia oczywiście największą elastyczność.

Największe jednak korzyści z opisanego sposobu akapitowania kodu widoczne są w przypadku zagnieżdżonych instrukcji if-else. Oto fragment programu, badający, czy trzy liczby całkowite A, B i C mogą być długościami boków trójkąta i jeżeli tak, to jakiego rodzaju trójkąt utworzą:

#include <ostream>

// zakładamy, że A <= B <= C

if (A + B > C)

if ((A==B) || (B==C))

if ((A==B) && (B==C)) std::cout << "Trójkąt równoboczny";

else

if ((A*A + B*B) == C*C)

std::cout << "Równoramienny trójkąt prostokątny";

else std::cout << "Trójkąt równoramienny";

else

if ( (A*A + B*B) == C*C )std::cout << "Trójkąt prostokątny";

else std::cout << "Taki sobie trójkąt";

else std::cout << "Odcinki o podanych długościach nie tworzą trójkąta";

Poza chaotycznym sposobem zapisu bezsensowne namiastki akapitowania mogą tu tylko wprowadzić w błąd nieuważnego czytelnika. Oto ten sam fragment kodu zapisany zgodnie ze wspomnianymi zasadami:

#include <ostream>

// zakładamy, że A <= B <= C

if (A + B > C)

{

if ((A==B) || (B==C))

{

if ((A==B) && (B==C))

{

std::cout << "Trójkąt równoboczny";

}

else if ((A*A + B*B) == C*C)

{

std::cout << "Równoramienny trójkąt prostokątny";

}

else std::cout << "Trójkąt równoramienny";

{

else if ( (A*A + B*B) == C*C )

{

std::cout << "Trójkąt prostokątny";

}

else

{

std::cout << "Taki sobie trójkąt";

}

}

else

{

std::cout << "Odcinki o podanych długościach nie tworzą trójkąta";

}

Jeżeli już mowa o instrukcjach if-else, przy znacznym ich zagnieżdżeniu korzystne może się okazać przekształcenie całej instrukcji if w instrukcję switch (o ile oczywiście jest to możliwe).

Pewną barierą ograniczającą swobodne akapitowanie jest ograniczona długość wiersza. Zbyt długie wiersze skutecznie utrudniają przeglądanie tekstu, zmuszając do ciągłego przewijania ekranu w lewo i prawo, stając się prawdziwym problemem w sytuacji drukowania tekstu. Wiersze przekraczające szerokość strony wydruku mogą być obcięte albo zawinięte - w obydwu przypadkach czytelność wydruku ulega niepotrzebnemu pogorszeniu. Edytor kodu zawiera orientacyjny wskaźnik prawego marginesu, którego przekroczenie może zwiastować problemy z wydrukiem. Położenie tego wskaźnika ustalić można za pomocą opcji edytora (Tools|Editor Options|Display|Right margin); dla wydruku na kartce formatu A4 przy braku prawego marginesu (opcja File|Print|Left margin ustawiona na 0) maksymalna bezpieczna wartość dla prawego marginesu to 94. Wiersze wykraczające poza tak ustawiony margines mogą być obcinane lub zawijane (zależnie od opcji File|Print|Wrap lines).

Najpewniejszym sposobem radzenia sobie ze zbyt długimi wierszami jest oczywiście ich unikanie. Sprowadza się ono do umiejętnego dzielenia (w sensie przejścia do nowego wiersza) długich list parametrów funkcji, dzielenia długich łańcuchów na dwa (lub więcej) łańcuchy konkatenowane za pomocą operatora + itp. Niekiedy wystarczająca staje się drobna zmiana stylu kodowania - poniższą instrukcję:

switch(Key)

{

case 'a' : // tutaj długa linia kodu...

break;

case 'b' : // tutaj długa linia kodu...

break;

default : // tutaj długa linia kodu...

break;

}

można przepisać do następującej postaci:

switch(Key)

{

case 'a'

: // tutaj długa linia kodu...

break;

case 'b'

: // tutaj długa linia kodu...

break;

default

: // tutaj długa linia kodu...

break;

}

przez co oszczędza się kilka znaków na szerokości wiersza.

Dla instrukcji for i if identyczny efekt osiągnąć można, lokując jej blok wykonawczy w osobnym wierszu, czyli np. zamiast:

for(int i=0; i<10; ++i) // tutaj długa linia kodu...

...

if( Key == 'a' || Key == 'A') // inna długa linia kodu...

napisać można tak:

for(int i=0; i<10; ++i)

{

// tutaj długa linia kodu...

}

...

if( Key == 'a' || Key == 'A')

{

// inna długa linia kodu...

}

Takie łamanie wierszy daje jeszcze jedną dodatkową korzyść: śledzenie programu, zorientowane jak wiadomo na poszczególne wiersze kodu, staje się dzięki temu bardziej selektywne - w jednym wierszu znajduje się teraz bowiem mniej kodu.

Długie wyrażenia warunkowe w instrukcjach if również powinny być łamane pomiędzy kilka wierszy, zawierających być może po jednym z warunków składowych, na przykład:

if( Key == VK_UP

|| Key == VK_DOWN

|| Key == VK_LEFT

|| Key == VK_RIGHT

|| Key == VK_HOME

|| Key == VK_END

{

instrukcje uwarunkowane

}

To nie przypadek, iż operatory || znajdują się na początku poszczególnych wierszy: po prostu czytając tekst od strony lewej do prawej możemy od razu stwierdzić, iż mamy do czynienia z alternatywą warunków. Umieszczenie operatora || na końcu wiersza informuje co prawda, iż wiersz ten nie jest kompletny i posiada kontynuację, jednakże informacja taka nie jest raczej przydatna dla ludzi, którzy (w przeciwieństwie do kompilatorów) zwyczajowo postrzegają całe bloki tekstu, nie zaś jego poszczególne wiersze.

Podobnie do złożonych wyrażeń warunkowych powinny być dzielone długie listy parametrów funkcji, na przykład:

Wydruk 2.2. Łamanie długiej listy parametrów funkcji

void DrawBoxWithMessage(const AnsiString &Message,

int Top,

int Left,

int Height,

int Width);

W przeciwieństwie jednak do łamania długich wyrażeń logicznych, przecinki oddzielające poszczególne parametry znajdują się na końcu poszczególnych wierszy. Mają one znaczenie wyłącznie dla kompilatora i nie niosą poza tym żadnej użytecznej informacji; umieszczenie przecinka na początku wiersza - czyli tam, gdzie zwyczajowo spodziewane jest słowo kluczowe lub identyfikator - powodowałoby tylko zbędne utrudnienia dla użytkownika czytającego tekst.

Tak samo podzielić można długi łańcuch na konkatenowane podłańcuchy:

Wydruk 2.3. Łamanie długiego łańcucha na konkatenowane podłańcuchy

AnsiString FilePath = "";

AnsiString FileName = "TestFile";

FilePath = "C:\\RootDirectory"

+ "\\"

+ "Branch\\Leaf"

+ FileName

+ ".txt";

Znak +, jako operator, znajduje się na początku każdego wiersza.

Dwa ostatnie przykłady wiążą się z wcięciami tekstu, co przez niektórych programistów może być traktowane jako zbędna fatyga; można wówczas zrezygnować z tak rygorystycznego wyrównywania poszczególnych wierszy, pisząc po prostu:

void DrawBoxWithMessage(const AnsiString &Message,

int Top,

int Left,

int Height,

int Width);

czy też:

FilePath = "C:\\RootDirectory"

+ "\\"

+ "Branch\\Leaf"

+ FileName

+ ".txt";

co - jakkolwiek nieco mniej czytelne - stanowi bądź co bądź niezły kompromis.

Znaczącą pomoc w kompletowaniu kodu, oszczędzającą wysiłek programisty, stanowią szablony kodu (code templates). Po wpisaniu (w edytorze kodu) charakterystycznej dla danego szablonu frazy początkowej i naciśnięciu Ctrl+J spowodujemy kompletne wygenerowanie tegoż szablonu. Szablony dostarczane standardowo przez C++Buildera niosą ze sobą duży ładunek funkcjonalności, niemniej jednak dla utrzymania spójności kodowania własnych programów konieczne bywa zazwyczaj uzupełnienie tego repertuaru o własne szablony - można to zrobić, przechodząc na kartę Code Insight opcji edytora (Tools|Editor Options) i dokonując niezbędnych uzupełnień; edytując szablon należy zwrócić uwagę na to, iż znak pionowej kreski (|) w jego treści oznacza pozycję, w której ma się ustawić kursor bezpośrednio po wygenerowaniu kodu.
Edycję i uzupełnianie dostępnych szablonów kodu można też przeprowadzić w sposób bardziej bezpośredni - ich definicje znajdują się w postaci tekstowej w pliku bcb.dci zlokalizowanym w podkatalogu Bin katalogu, w którym zainstalowano C++Buildera.

Sugestywne nazewnictwo elementów programu

Kolejnym czynnikiem wpływającym na czytelność kodu programów jest tworzenie nazw elementów tegoż programu - typów, zmiennych i funkcji - w sposób odzwierciedlający przeznaczenie tych elementów. Analizując np. standardowe typy C++Buildera nie mamy przecież wątpliwości, iż typ int reprezentuje liczby całkowite (ang. integers), zaś pod typem TFont ukrywają się elementy definicji czcionki. Podobnie przykładowa definicja użytkownika

int LiczbaStron;

sugeruje wyraźnie, iż pod deklarowaną zmienną ukrywa się liczba stron jakiegoś tekstu, będąca z natury liczbą całkowitą.

Nazewnictwo zmiennych odzwierciedlające ich przeznaczenie

Zmienne o sugestywnych nazwach łatwiejsze są do zidentyfikowania w kodzie programu niż beznamiętne skrótowce: użycie do przechowywania nazwiska pracownika zmiennej NazwiskoPrac jest z pewnością bardziej celowe niż użycie w tej roli zmiennej o nazwie (na przykład) S, nic nie mówiącej o typie ani o przeznaczeniu zmiennej.

Wobec oczywistego faktu, iż oryginalne nazwy reprezentowanych zjawisk i obiektów ze świata rzeczywistego (np. „kwota podlegająca odrębnemu opodatkowaniu”) są zwykle zbyt długie, by można je było (sensownie) użyć w roli nazw zmiennych, istotne stają się sugestywne zasady skracania nazw. Istnieje kilka przyjętych praktyk w tym względzie - rozpoczynanie każdego członu nazwy wielką literą, oddzielanie poszczególnych członów znakami podkreślenia itp., tak, że wspomnianą „kwotę podlegającą odrębnemu opodatkowaniu” można by przechowywać pod zmienną opatrzoną np. jedną z następujących nazw:

KwotaOdrOpod

Kwota_Odr_Opod

kwotaOdrOpod

Kwota_odr_opod

Wszystko zależy od przyjętego stylu; używanie znaków podkreśleń ma przy tym tę słabą stronę, iż dodatkowo wydłuża nazwy zmiennych. Niektórzy programiści dążą ponadto do uwzględnienia w nazwach zmiennych pewnych dodatkowych cech, na przykład rozpoczynając z wielkiej litery nazwy zmiennych zawierających kluczowe informacje, zaś z małej litery - nazwy zmiennych roboczych, np. temp. Nie przeszkadza to niczemu pod warunkiem, iż przeznaczenie zmiennej pozostaje czynnikiem decydującym w doborze jej nazwy.

Nazewnictwo zmiennych odzwierciedlające ich typy

Mimo iż generalnie informacja o przeznaczeniu zmiennej jest dla czytelności programu ważniejsza niż informacja o jej typie, to jednak zdarzają się wyjątki od tej reguły, zwłaszcza w tych przypadkach, gdzie konkretny typ wartości decyduje o szczególnym postępowaniu. Spójrzmy na poniższy fragment kodu:

Wydruk 2.4. Szczególne znaczenie typu zmiennej

int Sum = 0;

int* Numbers = new int[20];

for(int i=0; i<20; ++i)

{

Numbers[i] = i*i;

Sum += Numbers[i];

}

double Average = Sum/20;

Jak łatwo zauważyć, powyższy fragment kodu oblicza średnią arytmetyczną kwadratów kolejnych liczb całkowitych od 0 do 19. Oczekiwanym wynikiem jest 123,5, lecz pod zmienną Average podstawiona zostanie wartość 123. Przyczyną tego jest fakt, iż dzielenie Sum/20 wykonuje się na liczbach całkowitych, a więc jego wynik ulega obcięciu. Aby otrzymać poprawny wynik, należałoby dokonać rzutowania zmiennej Sum na typ double:

double Average = static_cast<double>(Sum)/20;

Stosując rzutowanie typów, upewnij się, iż wystarczająco znasz wszystkie cztery jego typy w C++. Unikaj też rzutowania w stylu klasycznego języka C na rzecz nowszych konstrukcji C++ - te ostatnie są lepiej widoczne w kodzie i bardziej komunikatywne co do natury wykonywanych operacji.
Co prawda przedstawiony przykład jest niezwykle trywialny i przypomina trochę użycie młota pneumatycznego do rozłupania orzecha - o wiele prościej byłoby przecież zadeklarować zmienną Sum jako double - uwidacznia on jednak w prosty sposób różnice wynikające z różnych typów zmiennych.

Staraj się odkładać deklaracje zmiennych do czasu, gdy okażą się one rzeczywiście potrzebne. Zmienne deklarowane „na zapas” mogą okazać się niepotrzebne w przypadku pominięcia danego fragmentu kodu (na przykład w rezultacie wykonania instrukcji if) lub w przypadku wystąpienia wyjątku. Unikaj deklaracji zmiennych w ramach pętli (chyba, że masz w tym jakiś szczególny cel), unikaj również „mieszania” zmiennych wskaźnikowych i niewskaźnikowych w tej samej deklaracji. Dobrym zwyczajem jest także nadawanie wartości początkowych zmiennym podczas ich deklarowania.
Pożądaną praktyką jest deklarowanie zmiennych w postaci

<typ> <nazwa> ;

Dlaczego? Oto prosty przykład:

int* PointerToInt;

Zmienna PointerToInt jest typu wskaźnikowego i służy do przechowywania wskaźnika do wartości typu int. Jeżeli jednak napisalibyśmy:

int* PointerToInt, AnotherPointerToInt;

to zmienna AnotherPointerToInt nie byłaby bynajmniej zmienną wskaźnikową, lecz zmienną typu int, bowiem deklaracja ta zinterpretowana zostałaby przez kompilator jako:

int *PointerToInt, AnotherPointerToInt;

Aby uniknąć takich niejednoznaczności, należy deklarację tę rozbić na dwie niezależne deklaracje:

int* PointerToInt;
int AnotherPointerToInt;

obydwie w zalecanej postaci <typ> <zmienna>;

Upewnij się również, iż wszystkie zmienne wskaźnikowe inicjowane są wartością NULL (0) lub jakimś poprawnym adresem; unikniesz w ten sposób wyjątków spowodowanych użyciem „wiszących” wskaźników.

W niektórych przypadkach informacja o typie zmiennej jest na tyle istotna, iż konieczne staje się odzwierciedlenie jej w nazwie. Najprostszym sposobem na to jest związanie określonego typu zmiennej z pierwszą literą jej nazwy - na przykład „b” dla zmiennych boolowskich, „s” dla łańcuchów itp. Podejście takie ma jednak dwie zasadnicze wady. Po pierwsze, generalizuje ono opisaną rolę pierwszej litery nazwy na wszystkie zmienne, typy i funkcje programu - również te, w których nazwach nie zamierzamy odzwierciedlać typu; po drugie - mnogość rozróżnianych w programie typów może sprawić, iż w alfabecie zabraknie po prostu liter.

Sam pomysł jest jednak ciekawy; jego praktyczna realizacja polega na przypisywaniu określonego znaczenia nie pierwszym literom nazw, lecz ściśle określonym przedrostkom. Realizacja ta jest dziełem Charlesa Simonyi, z pochodzenia Węgra, dlatego też nazwana została notacją węgierską; zastosowano ją po raz pierwszy w związku z konstrukcją biblioteki Win API.

Notacja węgierska określa rygorystycznie przedrostki nazw w zależności od ich typu i charakteru - i tak np. nazwy zmiennych boolowskich rozpoczynają się od litery f (ang. flags), nazwy łańcuchów od sz (ang. string with zero delimiter), wskaźników od p (ang. pointers) itp. Konwencja ta ma tyluż zwolenników, co przeciwników - ci ostatni uważają, iż stwarza ona więcej problemów, niż w rzeczywistości rozwiązuje. Nazwy tworzone według jej zasad niewątpliwie trudne są do wymówienia i zapamiętania, co wcale nie ułatwia pracy czytelnikowi studiującemu złożony kod; bogatą kolekcję takich nazw znaleźć można chociażby w dowolnym pliku pomocy związanym z Win32 API. Najpoważniejszym zarzutem wydaje się jednak ten, iż notacja węgierska faworyzuje znaczenie jednego tylko czynnika - typu zmiennej w kategoriach języka C, ignorując zupełnie inne względy, jak np. właśnie znaczenie zmiennej w programie.

W samym kodzie Win API również można dopatrzyć się odstępstw od wymogów notacji węgierskiej, na przykład zmienna wParam jest nie 16-bitową (jak wskazywałby przedrostek w - od „word”), lecz 32-bitową liczbą całkowitą bez znaku.

Modyfikacja nazwy zmiennej w celu odzwierciedlenia jej charakterystyki lub ograniczeń

Każdy, kto zdaje sobie sprawę z różnicy pomiędzy np. dodaniem 5 do zmiennej typu int a dodaniem jej do wskaźnika wskazującego tę zmienną, doskonale rozumie, iż informacja o charakterze zmiennej (zmienna statyczna, wskaźnik, pole obiektu itp.) może mieć niekiedy szczególne znaczenie dla czytelnika studiującego i starającego się zrozumieć kod źródłowy. Jednym ze sposobów uwidocznienia tej informacji jest poprzedzenie nazwy właściwej specyficznym prefiksem - p_ dla wskaźnika (ang. pointer), s_ dla zmiennej statycznej (ang. static), d_ lub m_ dla pola (ang. data member) lub funkcji składowej (ang. member function) klasy oraz a_ dla parametru funkcji (ang. argument). Użycie znaku podkreślenia pozwala łatwo odseparować owe przedrostki od nazwy właściwej - i tak np. pod nazwą s_LiczbaStron łatwo rozpoznać „zmienną statyczną zawierającą liczbę stron”. Nawiasem mówiąc, opisane prefiksowanie można by z powodzeniem zastąpić postfiksowaniem, dołączając stosowny przyrostek na końcu nazwy zmiennej - wówczas nazwa Liczba Stron_s mogłaby być interpretowana przez czytającego kod jako „zmienna zawierająca liczbę stron, będąca zmienną statyczną”. Wybór konkretnej konwencji - przedrostków lub przyrostków - jest sprawą osobistego wyboru, w każdym razie użytecznym okazać się może połączenie obydwu tych konwencji, a konkretnie prefiksowanie zmiennych wskaźnikowych i postfiksowanie dla uwydatnienia pozostałych cech; wówczas np. pod nazwą p_ObszarKomunikacji_s ukrywać się będzie prawdopodobnie statyczny (_s) wskaźnik (p_) do obszaru komunikacji pomiędzy kilkoma fragmentami kodu.

Innym szczególnym rodzajem zmiennych są te, których wartość nie ulega zmianie, nazywane z tego względu stałymi. Ugruntowaną już praktyką wyróżniania takich zmiennych jest pisanie ich nazw w całości wielkimi literami.

Funkcjonalność niektórych klas - z punktu widzenia pełnionej przez nie roli - jest na tyle charakterystyczna, iż sensowne staje się zrobienie kompletnej nazwy klasy (bez ew. poprzedzającego T lub C - o tym za chwilę) częścią nazwy zmiennej. Praktykę tę stosuje ostentacyjnie sam C++Builder, opatrując np. kolejne komponenty typu TLabel nazwami: Label1, Label2 itd.; my możemy postąpić nieco bardziej inteligentnie, używając zamiast numerka sugestywnej części słowotwórczej, określającej przeznaczenie danego obiektu, np.:

TComboBox* CountryComboBox;

TLabel* NameLabel;

String BookTitleString;

W ostatnim przykładzie fraza Title jest na tyle oczywista - w tym znaczeniu, iż tytuł książki jest łańcuchem znaków - iż fraza String mogłaby zostać opuszczona bez szkody dla komunikatywności, a z korzyścią dla samej nazwy, która przez to stałaby się nieco zgrabniejsza:

String BookTitle;

Ostatnim szczególnym rodzajem zmiennych, o którym tu wspomnimy, są pola klasy związane z jej właściwościami (tj. uczestniczące w deklaracji __property). Nazwy takich pól - zazwyczaj prywatnych - tworzone są najczęściej przez poprzedzenie nazwy odnośnej właściwości literą F (ang. Field), na przykład:

__property AnsiString RemoteMachineName= { read = FRemoteMachineName,

write = FRemoteMachineName };

Stanowi to wystarczający powód do tego, by unikać wielkiej litery F w charakterze prefiksu innego rodzaju.

Nazewnictwo typów danych

Zasady nadawania nazw typom danych nie odbiegają zbytnio od analogicznych zasad dotyczących nazw zmiennych, kilka różnic wartych jest jednak szczególnego podkreślenia.

Wśród klas wykorzystywanych przez użytkowników C++Buildera szczególną grupę stanowią oczywiście klasy wywodzące się z typu TObject. Zgodnie z konwencją przyjętą przy budowie biblioteki VCL nazwy tych klas rozpoczynają się od wielkiej litery T, będącej de facto przedrostkiem. Umożliwia to łatwe ich odróżnienie od nazw pozostałych klas, stwarza ponadto jedną uboczną korzyść - możliwe jest użycie nazwy klasy (bez przedrostka T) jako nazwy zmiennej, na przykład zmienna klasy TListBox może nosić nazwę (po prostu) ListBox; nazwa taka świadczy jednoznacznie o charakterze zmiennej. Nazwy pozostałych, „normalnych” klas C++Buildera powinny być tworzone z udziałem innego przedrostka, na przykład wielkiej litery C. Na uwagę zasługuje fakt, iż odróżnienie klas biblioteki VCL od pozostałych klas używanych w programie nie jest bynajmniej tylko sprawą konwencji nazewniczej: obiekty klas VCL muszą być bowiem tworzone dynamicznie, w przeciwieństwie do obiektów C++, nie podlegających generalnie takiemu ograniczeniu.

W podobny sposób tworzone są nazwy typów strukturalnych (struct), zbiorowych (Set) oraz wyliczeniowych (enum). C++Builder standardowo opatruje nazwy typów zbiorowych i wyliczeniowych przedrostkiem T, choć oczywiście można tu użyć innego przedrostka. Jako że typ zbiorowy związany może być z pewnym typem wyliczeniowym, intuicja dyktować może literę E jako odpowiedni przedrostek jego nazwy (od Enumeration); wybór taki zdecydowanie jednak nie jest polecany, gdyż C++Builder rezerwuje ten przedrostek do tworzenia nazw wyjątków (exceptions).

Definicja klasy-szablonu Set (jako podstawowego typu zbiorowego) znajduje się w pliku sysset.h położonym w podkatalogu Include\Vcl lokalnej instalacji C++Buildera:

template<class T, unsigned char minEl, unsigned char maxEl> class RTL_DELPHIRETURN Set;

Szablon ten posiada trzy parametry: typ bazowy i dwa ograniczenia (słowo RTL_DELPHIRETURN wynika ze zgodności z biblioteką VCL), tak więc na przykład zmienna zdolna pomieścić zbiór wszystkich wielkich liter alfabetu łacińskiego (lub dowolny jego podzbiór) może być zadeklarowana następująco:

Set<char, 'A', 'Z'> MaskaWielkieLitery

Jeżeli typ MaskaWielkieLitery używany ma być wielokrotnie, warto utworzyć dla niego synonim za pomocą dyrektywy typedef:

typedef Set<char, 'A', 'Z'> TWielkieLitery;

....

TWielkieLitery MaskaWielkieLitery;

....

TWielkieLitery MaskaWielkieSamogloski;

Jak sugerować mogą użyte nazwy zmiennych, typy zbiorowe idealnie nadają się do tworzenia wszelkiego rodzaju masek.

Jak już przed chwilą wspomnieliśmy, typ zbiorowy często bywa definiowany w oparciu o pewien typ wyliczeniowy. Plik graphics.hpp, znajdujący się we wspomnianym podkatalogu Include\Vcl, zawiera taką oto definicję podstawowych cech czcionki - pogrubienia, pochylenia, podkreślenia i przekreślenia:

enum TFontStyle { fsBold, fsItalic, fsUnderline, fsStrikeOut };

Cechy te mogą być użyte niezależnie od siebie, a sposób ich użycia reprezentowany jest przez odpowiedni typ zbiorowy:

typedef Set<TfontStyle, fsBold, fsStrikeOut> TfontStyles;

Nietrudno zauważyć, iż elementy typu wyliczeniowego prefiksowane są tutaj w jednolity sposób, związany z nazwą samego typu (fs jako skrót od font style). Prefiksowanie takie, całkowicie zgodne z prezentowanymi w tym rozdziale kanonami estetyki i czytelności programu, ma ponadto wymiar znacznie bardziej praktyczny: w sytuacji, gdy do kodu źródłowego włączane są niezliczone pliki nagłówkowe, zawierające łącznie (być może) dziesiątki czy setki definicji typów wyliczeniowych, nietrudno jest o kolizję nazw elementów składowych poszczególnych typów; umiejętnie stosowane prefiksowanie zmniejsza znacznie prawdopodobieństwo takich kolizji.

Inną metodą unikania kolizji nazw typów wyliczeniowych jest zamknięcie ich definicji w ramach definicji używającej je klasy; użycie takich typów poza definicją klasy wymaga jawnego kwalifikowania odwołań do nich za pomocą odpowiedniej przestrzeni nazw.

Nazewnictwo funkcji

Nazwy funkcji powinny być dobierane szczególnie starannie; powinny one odzwierciedlać czynności przez nie spełniane. Jeżeli stworzona w taki sposób nazwa funkcji wydaje się być nienaturalnie długa i nie widać sensownych sposobów jej skrócenia, być może sama funkcja jest nadmiernie złożona i korzystne byłoby jej rozbicie na funkcje bardziej elementarne.

Różne rodzaje funkcji powinny być nazywane w różny sposób. Funkcja nie zwracająca wartości (wynik typu void) lub zwracająca jedynie informację o powodzeniu lub niepowodzeniu operacji - czyli nie zwracająca żadnego „konkretnego” wyniku - ma charakter czysto „operacyjny” i powinna posiadać nazwę w postaci „czynność-obiekt”, na przykład CreateForm(), DisplayBitmap(), OpenComPort() itp. Funkcja o charakterze czysto „obliczeniowym” - czyli zwracająca istotną wartość, będącą ostatecznym rezultatem dokonywanych w jej treści obliczeń - powinna w swej nazwie odzwierciedlać charakter zwracanej wartości, czego przykładem mogą być nazwy GetAverage(), log10() czy ReadIntervalTimeOut().

Niektórzy programiści praktykują zapisywanie pierwszego „słowa” w nazwie funkcji w całości małymi literami (getDescription(), calcTimerLoops(), normalizeCollection() itp.). Trudno odmówić temu sensu, trudno też preferować takie postępowanie; niezależnie jednak od punktu widzenia najważniejsze są tu wymogi spójności stylu - jeżeli programista zdecyduje się na taką konwencję nazewniczą, powinien stosować ją konsekwentnie.

Generalnie rzecz biorąc, nazwy funkcji powinny przewyższać swą długością nazwy zmiennych i typów. Długość nazwy nie może być jednak celem samym w sobie - jak już przed chwilą napisaliśmy, długa i mało czytelna nazwa funkcji może być świadectwem nieudolnego zaprojektowania samej funkcji.

Konwencje nazewnicze - konkluzje

Podstawowym zadaniem wszelkich reguł dotyczących stylu kodowania programu - w szczególności stosowanych w nim nazw - jest polepszenie czytelności programu i ułatwienie jego przyszłej konserwacji, czyli w konsekwencji - ułatwienie życia samym programistom. Każda konwencja nazewnicza, czyniąca zadość takiemu punktowi widzenia, warta jest zastosowania i przedstawione tu zalecenia traktować należy jedynie jako przykłady, bądź też często przejawiające się tendencje. Niezależnie jednak od wyboru szczegółowych zasad nadawania nazw zmiennym, typom i funkcjom programu, przestrzeganie poniższych zaleceń wydaje się być warunkiem koniecznym osiągnięcia oczekiwanych rezultatów:

Właściwe konstruowanie kodu

Warunkiem powodzenia wszelkich przedsięwzięć jest użycie właściwych narzędzi, we właściwym czasie, do właściwych celów. Zgodnie z tą zasadą jednym z podstawowych czynników, mających zasadniczy wpływ na czytelność konstruowanego kodu, jest użycie odpowiednich konstrukcji używanego języka programowania. Używając C++Buildera, należy więc zdawać sobie sprawę np. z tego, gdzie należy użyć deklaracji const, a gdzie jest to niepożądane; kiedy używać referencji zamiast wskaźników; kiedy zrezygnować z rozbudowanej instrukcji if na rzecz instrukcji switch; kiedy reprezentować daną wielkość w postaci klasy, a kiedy lepiej z tego zrezygnować; kiedy obsłużyć wyjątek, a kiedy pozostawić go nie obsłużonym; której odmiany pętli użyć - itd., itp.; tego rodzaju decyzje składają się na postępowanie, które nazwaliśmy tu lakonicznie „wyborem właściwych konstrukcji językowych”. Jeżeli decyzje te podejmowane będą we właściwy sposób, z myślą o właściwych celach, kod stanie się łatwiejszy do opanowania i utrzymywania, bowiem jego znaczenie zbieżne będzie w dużym stopniu z intuicją studiującego go czytelnika czy programisty; w przeciwnym razie czytanie kodu może stać się prawdziwą udręką - a tego większość z nas chciałaby przecież uniknąć.

Używanie komentarzy

Podstawowym zadaniem komentarzy jest wyjaśnienie czytelnikowi tych aspektów funkcjonowania programu, które nie wynikają w oczywisty sposób z samego kodu źródłowego. Zarówno rozmieszczenie komentarzy, jak i ich treść muszą być starannie przemyślane - porozrzucane bezładnie komentarze o niejasnej treści mogą tylko zaciemnić strukturę programu.

Komentując kod w języku C++, należy ograniczyć się do komentarzy jednolinijkowych, rozpoczynających się od znaków „//”; komentarze rozpoczynające się od „/*” mogą być - wbrew intencji programisty - nieoczekiwanie zakończone przez przypadkową sekwencję „*/”.

Komentarze dokumentujące kod programu

Komentarze nie są częścią kodu programu i są kompletnie ignorowane przez kompilator; służą one jedynie czytelnikowi. Powinny być więc oddzielone od „właściwego” kodu poziomymi separatorami, a jeszcze lepiej - zapisane w odróżniającym się stylu, np. pochyloną czcionką lub innym kolorem.

Ponieważ jednak komentarze mają logiczny związek z określonymi fragmentami programu, powinny stosować identyczne z nimi akapitowanie - jeżeli wcięty jest fragment kodu źródłowego, związany z tym fragmentem komentarz powinien być wcięty na taką samą głębokość.

Za pomocą karty Colors opcji edytora (Tools|Editor Options) możliwe jest ustawienie żądanej czcionki i koloru poszczególnych elementów składniowych kodu, między innymi komentarzy. Dzięki temu można uczynić komentarze bardziej wyrazistymi, a nawet (jeżeli ktoś koniecznie chce) niewidocznymi (ustawiając identyczny kolor tła i pierwszego planu).

Komentarz związany z określonym wierszem kodu powinien być umieszczany w tymże, o ile pozwala na to szerokość tekstu; w przeciwnym przypadku można dokonać łamania komentarza, przy czym początki jego poszczególnych wierszy powinny być wyrównane w pionie. Oto przykład prawidłowo skomentowanego kodu:

double GetMaximumValue(const std::vector<double>& Vector)

throw(std::out_of_range)

{

// inicjalizacja zmiennej Maximum; jeżeli wektor będzie pusty,

// wygenerowany zostanie wyjątek std::out_of_range

double Maximum = Vector.at(0);

for(int i=0; i<Vector.size(); ++i)

{

if(Vector[i] > Maximum) // jeżeli bieżący element jest większy

{ // od bieżącej wartości zmiennej Maximum

Maximum = Vector[i]; // uaktualnij wartość tej zmiennej

}

}

return Maximum;

}

Aby ułatwić identyfikację poszczególnych par nawiasów { }, można umieścić po nawiasie zamykającym komentarz informujący o tym, jaka instrukcja kończy się w tym miejscu - na przykład:

}//end if

}//end for

}// ! for

}//end for(int i=0; i<Vector.size(); ++i)

Ostatni komentarz może wydawać się nieco przydługi, jednak o jego użyteczności szybko można się przekonać w przypadku zagnieżdżonych pętli for, rozciągających się na kilka stron.

Komentarze okazują się niezwykle przydatne w procesie dokumentowania kodu, szczególnie w przypadku opisu działania poszczególnych funkcji. Wraz z deklaracją funkcji powinny pojawić się informacje o charakterze operacji lub obliczeń realizowanych przez tę funkcję, uwarunkowaniach koniecznych do prawidłowego jej działania oraz zwracanej wartości - stanowią one coś na kształt kontraktu gwarantującego, iż w przypadku spełnienia określonych wymogów otrzymamy określone wyniki. Oto przykład deklaracji w pliku nagłówkowym:

double GetMaximumValue(const std::vector<double>& Vector)

throw(std::out_of_range)

//PRZEZNACZENIE: zwraca wartość największego elementu wektora

// o elementach typu double

//WYMAGANIA: wektor przekazany jako parametr nie może być pusty

//WYNIK: wynikiem funkcji jest wartość największego elementu

// wektora

Ważne jest, by komentarz o identycznej treści pojawił się również w pliku implementacyjnym - po jego przeczytaniu nie trzeba już bowiem będzie sięgać do pliku nagłówkowego:

//-------------------------------------------------------------------//

//

// PRZEZNACZENIE: zwraca wartość największego elementu wektora

// o elementach typu double

//

// WYMAGANIA: wektor przekazany jako parametr nie może być pusty

//

//

// WYNIK: wynikiem funkcji jest wartość największego elementu

// wektora

//

//-------------------------------------------------------------------//

double GetMaximumValue(const std::vector<double>& Vector)

throw(std::out_of_range)

{

// inicjalizacja zmiennej Maximum; jeżeli wektor będzie pusty,

// wygenerowany zostanie wyjątek std::out_of_range

double Maximum = Vector.at(0);

for(int i=0; i<Vector.size(); ++i)

{

if(Vector[i] > Maximum) // jeżeli bieżący element jest większy

{ // od bieżącej wartości zmiennej Maximum

Maximum = Vector[i]; // uaktualnij wartość tej zmiennej

}

}

return Maximum;

}

Należy pamiętać o tym, iż pliki nagłówkowe stanowią często jedyną aktualną dokumentację programu, należy więc weryfikować zawarte w nich komentarze po każdorazowej zmianie w ramach odpowiednich plików implementacyjnych (czy tym bardziej - nagłówkowych). Wszelkie zaniedbania na tym polu mogą sprawić, iż komentarze staną się po prostu bezużyteczne, zaś kod programu - znacznie mniej zrozumiały.

Komentarze „zaślepiające”

Zasadniczo komentarze nie ingerują w kod źródłowy programu, stanowiąc jedynie jego uzupełnienie. W przypadku jednak potrzeby wyeliminowania fragmentu kodu przekształcenie go w komentarz bywa bardziej użyteczne od jego fizycznego usunięcia, bowiem w każdej chwili fragment ten można (całkowicie lub częściowo) przywrócić. W przypadku kodu generowanego automatycznie przez C++Buildera jest to szczególnie przydatne w przypadku nieużywanych parametrów funkcji, na przykład:

void __fastcall TmainForm::Button1MouseUp(TObject* /*Sender*/,

TMouseButton /*Button*/,

TShiftState /*Shift*/,

int X,

int Y)

{

// wyświetla pozycję kursora myszy w ramach Button1

Label1->Caption.sprintf("%d,%d”, X, Y);

}

Zapis „TObject* /*Sender*/, ” odczytać można mniej więcej jako „w tym miejscu oryginalnie deklarowany był parametr o nazwie Sender, która to nazwa została usunięta jako nieistotna wobec nieużywania tegoż parametru przez funkcję”. Ten sposób „wykomentowania” nazw nieużywanych parametrów nie wygląda jednak zbyt czytelnie - informacją o pominięciu nazwy parametru jest przecież przecinek następujący bezpośrednio po typie parametru, a w tym przypadku specyfikacja typu oddzielona jest od tegoż przecinka komentarzem. O wiele rozsądniejszą propozycją wydaje się następująca:

void __fastcall TmainForm::Button1MouseUp(TObject*, //Sender

TMouseButton, //Button

TShiftState, //Shift

int X,

int Y)

{

// wyświetla pozycję kursora myszy w ramach Button1

Label1->Caption.sprintf("%d,%d”, X, Y);

}

W tej wersji zapis „TObject*, //Sender” oznacza mniej więcej tyle, co „ten parametr nie jest używany przez funkcję; w oryginalnej wersji deklarowany był tu parametr o nazwie Sender”. Poddajmy ten zapis drobnej modyfikacji:

void __fastcall TmainForm::Button1MouseUp(TObject*, //Sender,

TMouseButton, //Button,

TShiftState, //Shift,

int X,

int Y)

{

// wyświetla pozycję kursora myszy w ramach Button1

Label1->Caption.sprintf("%d,%d”, X, Y);

}

Dodatkowe przecinki na końcu komentarzy nie wpływają raczej na czytelność, za to ułatwiają przywrócenie danego parametru, gdy okaże się on potrzebny - wystarczy w tym celu usunąć odpowiedni znacznik komentarza (//) i poprzedzający go przecinek.

Uaktualnienie tytułu etykiety Label1->Caption w powyższym przykładzie dokonywane jest w sposób cokolwiek trickowy - metoda sprintf() klasy AnsiString zwraca bowiem referencję do instancji (AnsiString(*this)) zaktualizowanej przez łańcuch przekazany jako argument. Pozwala to co prawda na zwięzły zapis, generalnie jednak utrudnia zrozumienie kodu - z tego powodu aktualizowanie właściwości powinno odbywać się raczej przy użyciu operatora przypisania.

Komentarze - ozdobniki

Wszelkie ozdobniki w rodzaju ramek otaczających nagłówki i tytuły modułów muszą być oczywiście ukryte przed kompilatorem, mają więc najczęściej postać komentarzy. Konstruując tego rodzaju upiększenia należy uważać, by ich wymyślna forma plastyczna nie zaciemniła tego, co najistotniejsze - zasadniczej treści kodu źródłowego.

Do kategorii komentarzy - ozdobników zaliczają się również separatory oddzielające od siebie fragmenty kodu automatycznie generowanego przez C++Buildera, także generowane automatycznie i godne polecenia również w „ręcznie” tworzonym kodzie. Postać tych separatorów zdefiniowana jest w pliku BCB.BCF zlokalizowanym w podkatalogu BIN lokalnej instalacji C++Buildera 5. Zawartość tego pliku podzielona jest na sekcje w sposób charakterystyczny dla plików typu *.ini; za wygląd wspomnianego separatora odpowiada dyrektywa Didiver Line w sekcji Code Formatting:

[Code Formatting]

Divider Line=// ...... mój separator ......... //

Jeżeli brak jest wspomnianego zapisu, C++Builder generuje separatory w standardowej postaci. Niestandardowy separator musi rozpoczynać się od znaków //; dla lepszej orientacji co do wyglądu niestandardowego separatora w kontekście otaczającego go kodu wskazane jest skonstruowanie go w oknie edytora kodu, a następnie przeniesienie do pliku BCB.BCF za pomocą schowka.

Na zakończenie dyskusji o stylu kodowania jeszcze jedna uwaga: niezależnie od wszelkich komentarzy zawartych w programie najlepszą dokumentacją kodu powinien być… on sam. Stosowanie czytelnego stylu kodowania, używanie sugestywnych nazw i przejrzystych konstrukcji językowych automatycznie przyczynia się do zwiększenia przejrzystości kodu źródłowego.

Zalecane praktyki programistyczne

Tematem tego podrozdziału jest usprawnienie programowania w C++ z użyciem C++Buildera. O teorii, praktyce, zwyczajach i błędach programowania napisano już mnóstwo książek o różnym stopniu zaawansowania. W ramach krótkiego rozdziału nie jest możliwe całościowe przedstawienie problematyki efektywnego programowania nawet w zarysie, ograniczymy się więc do tych jego aspektów, które charakterystyczne są dla C++ i sprawiają najwięcej kłopotów niedoświadczonym programistom.

Rezygnacja z używania typu char*

Typ char*, tradycyjnie używany do manipulowania ciągami znaków, został w praktyce wyparty przez klasę String z biblioteki standardowej C++, a także przez klasę AnsiString charakterystyczną dla biblioteki VCL (dla wygody klasie tej przypisywany jest - za pomocą dyrektywy typedef - synonim String). Klasa String z biblioteki standardowej wygodna jest wówczas, gdy ważne są względy przenośności programu; dla jej wykorzystywania konieczne jest dołączenie definiującego ją pliku nagłówkowego:

#include <string>

Z kolei manipulowanie łańcuchami przy użyciu klasy AnsiString ułatwia współpracę tworzonego kodu z biblioteką VCL. Klasa ta posiada wiele użytecznych metod, co wobec częstych (zazwyczaj) operacji na łańcuchach plasuje ją w rzędzie środków zwiększających efektywność programów i ułatwiających ich użycie.

Na takie okazje, kiedy to konieczne jest użycie wskaźnika do ciągu znaków (char*) - na przykład w celu przekazywania łańcuchów jako parametrów wywołań funkcji Win 32 API - klasa AnsiString posiada funkcję składową c_str(), zwracającą aktualny łańcuch w takiej właśnie reprezentacji. Zmiany aktualnego łańcucha, reprezentującego instancję klasy, dokonuje się za pomocą funkcji składowych sprintf() i printf(); funkcje te, dołączające dotychczas podany łańcuch na końcu łańcucha aktualnego, począwszy od wersji 5. C++Buildera zastępują aktualny łańcuch nową wartością. Aby wykonać wspomnianą konkatenację, należy użyć funkcji składowych cat_sprintf() i cat_printf(). Różnica pomiędzy funkcjami sprintf() i printf() dotyczy zwracanego przez nie wyniku: pierwsza z nich zwraca referencję do odnośnej instancji klasy AnsiString, druga natomiast - długość nowego łańcucha po jego sformatowaniu. Podobna relacja zachodzi pomiędzy funkcjami cat_sprintf() i cat_printf() z tą różnicą, iż wynik zwracany przez cat_printf()jest długością dołączanego łańcucha. Oto deklaracje wspomnianych funkcji:

int __cdecl printf(const char* format, ...);

int __cdecl cat_printf(const char* format, ...);

AnsiString& __cdecl sprintf(const char* format, ...);

AnsiString& __cdecl cat_sprintf(const char* format, ...);

Powyższe funkcje korzystają w swych implementacjach z funkcji vprintf() i cat_vprintf():

int __cdecl AnsiString::printf(const char* format, ...)

{

int rc;

va_list paramList;

va_start(paramList, format);

rc = vprintf(format, paramList);

va_end(paramList);

return rc;

}

AnsiString& __cdecl AnsiString::sprintf(const char* format, ...)

{

va_list paramList;

va_start(paramList, format);

vprintf(format, paramList);

va_end(paramList);

return *this;

}

int __cdecl AnsiString::cat_printf(const char* format, ...)

{

int rc;

va_list paramList;

va_start(paramList, format);

rc = cat_vprintf(format, paramList);

va_end(paramList);

return rc;

}

AnsiString& __cdecl AnsiString::cat_sprintf(const char* format, ...)

{

va_list paramList;

va_start(paramList, format);

cat_vprintf(format, paramList);

va_end(paramList);

return *this;

}

Funkcje vprintf() i cat_vprintf()używają - jako drugiego argumentu - listy parametrów va_list

int __cdecl AnsiString::vprintf(const char* format, va_list paramList);

int __cdecl AnsiString::cat_vprintf(const char* format, va_list paramList);

co wymaga dołączenia pliku nagłówkowego <stdarg.h>.

Raz jeszcze przypominamy, iż w wersji 5. C++Buildera zmieniło się działanie funkcji sprintf() i printf(): zastępują one teraz aktualny łańcuch nową zawartością, zaś realizowane przez nie dotychczas dołączanie łańcucha do aktualnej zawartości przejęte zostało przez nowe funkcje cat_sprintf() i cat_printf(). Należy o tym pamiętać przy przenoszeniu aplikacji z poprzednich wersji C++Buildera.

Referencje i ich poprawne używanie

Referencje bywają niedoceniane przez programistów, ponadto niektórzy początkujący programiści mają kłopoty z ich zrozumieniem. Tymczasem wyeliminowanie operacji na wskaźnikach na rzecz posługiwania się referencjami może uczynić program bardziej zrozumiałym. W punkcie tym przedstawimy kilka istotnych cech referencji i zaprezentujemy sposoby ich wykorzystania; wytłumaczymy również powody, dla których funkcje biblioteki VCL posługują się wskaźnikami, nie referencjami.

Referencja zawsze jest powołaniem się na konkretny obiekt (który dla prostoty nazwiemy tu obiektem powoływanym). Referencji nie można zmienić tak, by powoływała się na inny obiekt, nie istnieje też nic w rodzaju pustej referencji (tj. nie powołującej się na nic). Cechy te odróżniają referencję od wskaźnika, który może w różnym czasie wskazywać różne obiekty lub nie wskazywać żadnego (wskaźnik pusty NULL). Referencja powinna być traktowana jako alternatywna nazwa dla powoływanego obiektu, podczas gdy wskaźnik najczęściej bywa utożsamiany ze wskazywanym obiektem. Odwołanie się do wskazywanego przez wskaźnik obiektu wymaga jawnej dereferencji (operatora *), podczas gdy referencja sama jest ze swej istoty dereferencją.

Cokolwiek zrobimy z referencją, to samo stanie się z powoływanym przez tę referencję obiektem. Oto prosty przykład:

int X = 12; // deklaracja zmiennej X i zainicjowanie jej wartością 12

int& Y = X; // deklaracja referencji do zmiennej typu int i zainicjowanie

// tej referencji powołaniem na zmienną X

Jeżeli zmienimy X, automatycznie zmieni się również Y, i vice versa.

Referencje są również użyteczne w przypadku odwołań do zmiennych tworzonych dynamicznie, na przykład:

TBook* Book1 = new TBook(); // deklaracja i utworzenie obiektu TBook

TBook& Book2 = *Book1; // deklaracja referencji do obiektu TBook

// i zainicjowanie jej odwołaniem do obiektu

// wskazywanego przez Book1

Referencja Book2 powołuje się tu na obiekt wskazywany przez Book1.

Jednym z typowych przypadków użycia referencji jest przekazywanie parametrów do funkcji w sytuacji, gdy wartość tych parametrów ulec ma zmianie. Deklaruje się wówczas parametry jako referencje i wywołuje funkcję w taki sam sposób, jak gdyby parametry te przekazywane były przez wartość. Oto przykład prostej funkcji zamieniającej dwie wartości typu int:

void swap(int& X, int& Y)

{

int temp;

temp = X;

X = Y;

Y = temp;

}

Funkcję tę wywołuje się w typowy sposób:

int Liczba1 = 12;

int Liczba2 = 68;

swap(Liczba1, Liczba2);

Po wykonaniu funkcji zmienna Liczba1 ma wartość 68, zaś zmienna Liczba2 - wartość 12; zmienne zamieniły się więc zawartością. Zmienne X i Y w treści funkcji stanowią referencję do zmiennych (odpowiednio) Liczba1 i Liczba2; cokolwiek stanie się ze zmienną X (odpowiednio: Y), dzieje się również ze zmienną Liczba1 (odpowiednio: Liczba2).

Przekazywanie przez referencję parametrów o typach predefiniowanych (np. int) ma sens jedynie wtedy, gdy wartość tych parametrów ulec ma zmianie w wyniku wykonania funkcji. Z typami definiowanymi przez użytkownika (klasami, strukturami itp.) rzecz ma się nieco inaczej: alternatywą dla przekazywania ich przez wartość jest znacznie efektywniejsze przekazanie ich przez referencję poprzedzoną klauzulą const. Oto przykład:

void DisplayMessage(const AnsiString& message)

{

// wyświetlenie komunikatu; "message" stanowi tu alias dla przekazanego

// parametru aktualnego. Nie jest wykonywane żadne kopiowanie, zaś

// klauzula "const" uniemożliwia użycie w treści funkcji takich konstrukcji,

// które zmieniałyby wartość zmiennej "message"

}

Taka wersja przedstawionej funkcji jest znacznie efektywniejsza od następującej:

void DisplayMessage(AnsiString message)

{

// wyświetlenie komunikatu; "message" stanowi kopię parametru aktualnego

}

Funkcja w pierwszej wersji operuje bezpośrednio na przekazanym parametrze aktualnym (symbolizowanym przez zmienną message); nie jest tworzona kopia parametru, nie następuje więc wywołanie konstruktora kopiującego ani destruktora. Klauzula const zabezpiecza przed zmianą przekazanego parametru. Wykonanie drugiej z przedstawionych wersji funkcji pociąga za sobą zarówno stworzenie kopii parametru aktualnego, jak i późniejszą jej destrukcję.

Obydwie wersje mogą być jednakże wywoływane w taki sam sposób:

AnsiString Message = "Cześć!";

DisplayMessage(Message);

co dowodzi, iż opisane usprawnienie funkcji pozostaje bez wpływu na wywołujący ją kod.

Referencja może również stanowić wynik funkcji, co może prowadzić do efektów ubocznych w sytuacji, gdy funkcja taka użyta jest w roli L-wyrażenia, czyli konstrukcji mogącej wystąpić po lewej stronie operatora przypisania. Dzięki referencjom możliwe jest również definiowanie operatorów zdolnych wystąpić jako część składowa L-wyrażenia, na przykład operatora tablicowego (subscript operator) - oto jak zrealizować w ten sposób wirtualną tablicę książek (z których każda reprezentowana jest przez obiekt klasy Book):

class Book

{

public:

Book();

int NumberOfPages;

};

class ArrayOfBooks

{

private:

static const unsigned NumberOfBooks = 100;

public:

Book& operator[] (unsigned i);

};

Instancja klasy ArrayOfBooks może być używana na równi ze „zwykłą” tablicą - jej elementy mogą być zarówno odczytywane, jak i zapisywane:

ArrayOfBooks ShelfOfBooks;

unsigned PageCount = 0;

ShelfOfBooks[0].NumberOfPages = 45;

PageCount := ShelfOfBooks.NumberOfPages; //PageCount == 45

Jest to możliwe, ponieważ operator [] zwraca referencję do obiektu, nie zaś jego kopię.

Reasumując - referencje są wygodniejsze i bezpieczniejsze od wskaźników z dwóch powodów: po pierwsze, mają ustaloną wartość (powołują się cały czas na ten sam obiekt), po drugie, wobec nieistnienia „pustych” referencji nie jest potrzebna ich weryfikacja (w rodzaju testowania wskaźników na wartość NULL).

Skoro więc referencje posiadają tyle zalet, dlaczego implementacja biblioteki VCL opiera się głownie na wskaźnikach?

Przyczyną takiego stanu rzeczy jest fakt, iż biblioteka VCL zrealizowana została w języku Object Pascal i posługuje się referencjami charakterystycznymi dla tegoż języka. Referencje Object Pascala podobne są bardziej do wskaźników C++ niż jego referencji - można im bowiem dynamicznie przypisywać powoływane obiekty, można też uczynić referencję pustą, czyli nie powołującą się na żaden obiekt. Referencje Object Pascala zostały więc zastąpione wskaźnikami C++; w niektórych przypadkach można by co prawda zastosować referencje C++, lecz obiekty VCL tworzone są dynamicznie, więc i tak konieczne jest użycie wskaźników do tworzonych instancji. Jako że wskaźniki przekazywane są (jako parametry) przez wartość, nie mogą one ulec zmianie w wyniku wykonania funkcji; poprzedzając wskaźnik-parametr klauzulą const, zabezpieczamy się również przed zmianą wskazywanego obiektu.

Unikanie zmiennych globalnych

Należy ograniczyć używanie zmiennych globalnych do tych przypadków, w których są one niezbędnie potrzebne. Oprócz „zaśmiecania” globalnej przestrzeni nazw zmienne globalne przyczyniają się do powstawania uzależnień pomiędzy modułami, co utrudnia ich konserwację i niezależne przenoszenie pomiędzy programami. Poza tym przypadki, kiedy definicji zmiennej wykorzystywanej w jednym module poszukiwać trzeba w innym, z pewnością nie przyczyniają się do polepszenia czytelności programu.

Kiedy mowa o zmiennych globalnych, programiści wykorzystujący C++Buildera (a także Delphi) natychmiast przypominają sobie globalne wskaźniki do poszczególnych formularzy, znajdujące się w modułach źródłowych związanych z tymi formularzami - to właśnie przykład wspomnianej na wstępie absolutnej konieczności. W większości przypadków odpowiedniejsze są jednak rozwiązania alternatywne do używania zmiennych globalnych w sposób bezpośredni, których przykłady zaprezentujemy w tym punkcie.

Rozwiązania te polegają na wykorzystaniu konstrukcji, które, zachowując się podobnie do zmiennych globalnych, wolne są jednocześnie od wielu wad związanych z bezpośrednim dostępem do nich. Jednym z takich rozwiązań jest tzw. klasa modularna, zwana krótko modułem (ang. module). Obudowuje ona statyczne zmienne, które są jej prywatnymi składowymi, dostępnymi wyłącznie za pośrednictwem statycznych funkcji dostępowych typu Get… i Set…; ze względu na statyczny charakter zarówno samych zmiennych, jak i wspomnianych funkcji dostępowych, nie jest konieczne tworzenie instancji tej klasy. Oto przykład klasy modularnej obudowującej dwie zmienne:

class Global;

{

private:

static int Number;

static double Average;

// prywatny konstruktor

Global(); // bez implementacji, instancje nie będą tworzone

public :

// funkcje dostępowe:

static void setNumber(int NewNumber) { Number = NewNumber; }

static void setAverage(double NewAverage) { Average = NewAverage; }

static int getNumber() { return Number; }

static double getAverage { return Average; }

}

Zmienna Number dostępna jest teraz wyłącznie poprzez funkcje Global::getNumber() i Global::setNumber(); analogicznie ma się rzecz ze zmienną Average. Umożliwia to lepszą kontrolę wykorzystania wspomnianych zmiennych, ponadto przyczynia się do „odciążenia” globalnej przestrzeni nazw.

Inne rozwiązanie wykorzystuje koncepcję tzw. singletonu - pod tym określeniem rozumiemy obiekt, którego jedyna instancja tworzona jest przy pierwszym odwołaniu do niego. Prezentowana poniżej klasa posiada statyczną funkcję składową Instance(), której każde wywołanie zwraca referencję do instancji tworzonej przy pierwszym wywołaniu:

class Singleton

{

public:

static Singleton& Instance();

protected:

Singleton(); // bez implementacji

};

.....

Singleton& Singleton::Instance()

{

static Singleton* NewSingleton = new Singleton();

return *NewSingleton;

}

Rozwiązanie takie ma jednak tę wadę, iż destruktor obiektu nie jest nigdy wywoływany, przez co obiekt ten być może niepotrzebnie zajmował będzie pamięć. Jeżeli w treści destruktora wykonywane są jakieś szczególnie istotne operacje, należy zastosować następującą implementację, gwarantującą jego wywołanie:

Singleton& Singleton::Instance()

{

static Singleton NewSingleton;

return NewSingleton;

}

Ta implementacja ma z kolei tę wadę, iż inny statyczny obiekt może mieć dostęp do obiektu Singleton już po jego destrukcji; rozwiązaniem tego problemu jest zastosowanie statycznego licznika odwołań, kontrolującego dostęp do obiektu, zabezpieczającego przed jego destrukcją, gdy jest jeszcze potrzebny.

Globalne wskaźniki do formularzy

Zajmijmy się teraz przez chwilę formularzami, a ściślej - ich globalnymi wskaźnikami zawartymi w odnośnych modułach źródłowych. Wskaźniki te niezbędne są do obsługi formularzy niemodalnych; standardowo niemodalnym jest każdy formularz projektu - IDE wpisuje go na listę formularzy tworzonych automatycznie, przez co w funkcji WinMain() pojawia się następująca instrukcja:

Application->CreateForm(__classid(TFormX), &FormX);

(X oznacza tu numer formularza). Formularze obsługiwane w sposób modalny nie wymagają odrębnego wskaźnika, gdyż ich destrukcję wykonuje automatycznie metoda ShowModal(); formularze takie nie powinny być oczywiście tworzone automatycznie przy starcie aplikacji, należy więc usunąć każdy z nich z listy Auto_create forms: na karcie Forms opcji projektu przez przesunięcie go na listę Available forms:.

Możliwe jest generalne wyłączenie funkcji autokreacji formularzy przez IDE - należy w tym celu wyłączyć opcję Auto create forms w sekcji Form designer karty Preferences opcji środowiska (Tools|Environment Options). Wszystkie formularze projektu „lądują” wówczas na liście Available forms: i można je selektywnie przeciągać na listę Auto_create forms:.

Dla formularzy modalnych nie są ponadto niezbędne wspomniane wskaźniki globalne, można je więc usunąć - usuwając najpierw z odnośnego pliku deklarację:

TFormX FormX;

i ewentualną deklarację z pliku nagłówkowego:

extern PACKAGE TFormX FormX;

tę ostatnią dobrze jest „wykomentować” zamiast fizycznego usuwania i opatrzyć dodatkowym komentarzem, wyjaśniającym przyczynę tego zabiegu:

// ten formularz jest formularzem MODALNYM i powinien być obsługiwany

// wyłącznie przez metodę ShowModal()

Aktywacja formularza modalnego nie jest bynajmniej sprawą skomplikowaną:

TFormX* FormX = new TFormX (0);

try

{

FormX->ShowModal();

}

__finally

{

delete FormX;

}

Ponieważ mało prawdopodobne jest, by wskaźnik FormX używany był jeszcze do czegoś innego, można opatrzyć go klauzulą const:

TFormX* const FormX = new TFormX (0);

try

{

FormX->ShowModal();

}

__finally

{

delete FormX;

}

Konstrukcja try…finally zapewnia tutaj odporność kodu na wystąpienie wyjątku, w szczególności gwarantuje zwolnienie instancji formularza.

Innym zalecanym rozwiązaniem jest wykorzystanie szablonu auto_ptr z biblioteki standardowej:

auto_ptr<TFormX> FormX(new TFormX(0));

FormX->ShowModal();

Natura szablonu auto_ptr zapewnia destrukcję instancji formularza, gdy jej wskaźnik opuści zakres obowiązywania deklaracji (wskaźnik ten lokowany jest bowiem na stosie - przyp. tłum.).

Wykorzystywanie szablonu auto_ptr jest techniką ze wszech miar polecaną w odniesieniu do obiektów bazujących na VCL, ze względu na jej bezpieczeństwo w zakresie wyjątków oraz prostotę użycia. Dla tych komponentów VCL, które w swym konstruktorze wymagają podania wskaźnika właściciela, należy w tej roli podać wskaźnik pusty (0) - jak przed chwilą napisaliśmy, destrukcja autowskaźnika dokonuje się automatycznie, gdy opuszcza on zakres swej deklaracji, nie należy więc powierzać tej funkcji właścicielowi komponentu.

Wyświetlenie formularza niemodalnego realizowane jest przez jego metodę Show():

FormX->Show();

Jeżeli instancja formularza jeszcze nie istnieje, należy ją oczywiście utworzyć:

FormX = new TFormX(this);

FormX->Show();

Przy operowaniu formularzami niemodalnymi należy jednak zachować ostrożność w stosunku do ich wskaźników. Po pierwsze, nie wolno dokonywać powtórnego tworzenia formularza utworzonego automatycznie przez aplikację, gdyż doprowadziłoby to do zagubienia używanej właśnie jego instancji i zastąpienia jej instancją nowo utworzoną, co przede wszystkim doprowadzić może do kompletnej dezorganizacji programu. Po drugie - instancja formularza niemodalnego, utworzona automatycznie przez aplikację, nie powinna być nigdy jawnie zwalniana.

Aby uchronić się zarówno przed brakiem instancji formularza, jak również powtórnym jego tworzeniem, należy sprawdzić jego globalny wskaźnik; jeżeli ma on „pustą” wartość, oznacza to, że odnośna instancja nie została jeszcze utworzona:

if (FormX == 0) FormX = new TFormX(this);

FormX->Show();

Użycie klauzuli const

Klauzula const, mówiąc ogólnie, wskazuje na intencje programisty, dotyczące niezmienności danego obiektu i wymusza na kompilatorze egzekwowanie wymogu tej niezmienności. Może być ona używana na wiele sposobów.

Po pierwsze, można za jej pomocą definiować stałe:

const double PI = 3.141592654;

Jest to metoda zalecana na gruncie C++ - należy unikać definiowania stałych poprzez dyrektywę #define. Zmienne deklarowane jako stałe muszą być oczywiście inicjowane - i jest to jedyny sposób bezpośredniego nadawania im wartości.

Deklaracje stosujące klauzulę const w kontekście wskaźników i referencji powinny być czytane od strony prawej do lewej. W poniższym fragmencie:

int Y = 12;

const int X = Y;

najpierw zmiennej Y nadawana jest wartość 12, po czym wartość ta powielana jest w zmiennej X. Zmienna X jest następnie kwalifikowana jako stała (const), a więc jej wartość nie może być zmieniana; można za to swobodnie zmieniać wartość zmiennej Y.

W poniższej deklaracji:

int* const P = &Y;

adres zmiennej Y przypisywany jest wskaźnikowi P; wskaźnik ten jest następnie kwalifikowany jako stała, nie można więc zmieniać jego zawartości, można za to swobodnie zmieniać zawartość wskazywanej przezeń zmiennej. Zgodnie bowiem z regułą czytania deklaracji od prawej do lewej klauzula const odnosi się do poprzedzającego ją operatora *.

Jeżeli jednak przestawić nieco szyk deklaracji:

const int* P = &Y;

to klauzula const odnosi się do dereferencji wskaźnika: zmienna Y może być mianowicie modyfikowana bezpośrednio (Y = ...), ale pośrednio (*P =...) już nie.

Zgodnie natomiast z poniższą deklaracją:

const int* const P = &Y;

niezmienny musi być zarówno sam wskaźnik P, jak i wskazywana przez niego zmienna.

Rozpatrzmy analogiczne sytuacje w odniesieniu do referencji. Klauzula const w poniższej deklaracji:

int& const R = Y;

nie powoduje praktycznie żadnych skutków, potwierdza bowiem jedynie fakt, iż referencje są w C++ niezmienne. Jeżeli jednak zamienić miejscami słowa kluczowe:

const int& R = Y;

otrzymamy wymóg niezmienności powoływanego przez referencję obiektu, innymi słowy zmienna Y może być modyfikowana bezpośrednio (Y =...), lecz nie przez referencję (R = ...).

Nie powoduje żadnych skutków przestawienie słów const i int, poprzedzających operator & lub *, jeżeli operator ten pozostawimy na swoim miejscu; tak więc poniższe deklaracje:

const int* P = &Y;

const int* const P = &Y;

const int& R = Y;

równoważne są następującym:

int const* P = &Y;

int const* const P = &Y;

int const& R = Y;

Deklarując literał łańcuchowy, należy stosować jedną z następujących form:

const char* const LiteralString = "Hello World";

char const * const LiteralString = "Hello World";

W obydwu przypadkach zarówno same łańcuchy, jak i ich wskaźniki pozostać muszą niezmienne.

W odniesieniu do parametrów funkcji klauzula const użyteczna jest wówczas, gdy chcemy zagwarantować niezmienność obiektu wskazywanego przez wskaźnik, będący parametrem (samego wskaźnika funkcja i tak nie może zmienić, gdyż przekazywany jest on przez wartość):

double GetAverage(const double* ArrayOfDouble, int LengthOfArray)

{

double Sum = 0;

for (int i=0; i<LengthOfArray; ++i)

{

Sum += ArrayOfDouble[i];

}

double Average = Sum/LengthOfArray;

return Average;

}

Funkcja w tej postaci gwarantuje niezmienność tablicy wskazywanej przez ArrayOfDouble. Zauważmy, iż deklaracja postaci:

const double* const ArrayOfDouble

byłaby wyraźnie redundantna, bowiem niezmienność samego wskaźnika ArrayOfDouble zagwarantowana jest przez fakt, iż jest on przekazywany przez wartość. Z tych samych powodów zbędne jest opatrywanie klauzulą const parametru LengthOfArray.

Typ wyniku zwracanego przez funkcję również może być zadeklarowany jak stały. Ma to jednak sens w przypadku, gdy wynik funkcji jest referencją lub wskaźnikiem, jak również w sytuacji przeciążanej funkcji składowej typu const. Opatrzenie deklaracji funkcji składowej klauzulą const zabrania tej funkcji dokonywania jakichkolwiek modyfikacji w obrębie instancji ukrywającej się pod wskaźnikiem this; wynika stąd wymóg, iż wszystkie wywoływane przez nią funkcje składowe muszą również należeć do kategorii const. Zwyczajowo do kategorii const należą funkcje dostępowe typu Get..., pobierają one bowiem interesującą wartość, nie ingerując w zawartość obiektu. Oto prosty przykład:

class Book

{

private:

int NumberOfPages;

public:

Book();

int GetNumberOfPages() const;

};

Definicja funkcji GetNumberOfPages() jest następująca:

int Book::GetNumberOfPages() const

{

return NumberOfPages;

}

Jak łatwo zauważyć, odnosząca się do wyniku funkcji klauzula const wystąpić musi na końcu jej nagłówka zarówno w deklaracji, jak i w części implementacyjnej.

Ostatnim obszarem częstych zastosowań klauzuli const jest przeciążanie operatora przez klasę w sytuacji, gdy ma on realizować dostęp zarówno do zmiennych objętych klauzulą niezmienności, jak i do zwykłych zmiennych pozbawionych tej klauzuli. W charakterze przykładu przywołajmy opisywany wcześniej operator tablicowy [] klasy ArrayOfBooks:

class ArrayOfBooks

{

public:

Book& operator[] (unsigned i);

const Book& operator[] (unsigned i) const;

};

Załóżmy teraz, iż obiekt klasy ArrayOfBooks przekazywany jest jako parametr pod postacią referencji typu const ArrayOfBooks&; obiekt powoływany przez tę referencję musi wówczas pozostać niezmienny, a więc przypisywanie wartości jego „elementom” zwracanym (przez referencję) przez operator [] jest niedozwolone. Przeciążenie tego operatora i użycie jego drugiego aspektu (tego z klauzulą const) czyni takie przypisania nielegalnymi a priori.

Obsługa wyjątków

Wyjątki (ang. exceptions) stanowią wygodny sposób reakcji na błędy występujące w czasie wykonywania programu. Spośród innych, alternatywnych sposobów obsługi błędów wykonania, powodujących z reguły zakończenie wykonania programu (w sposób nieoczekiwany lub zgodnie z procedurą zdefiniowaną przez użytkownika), wyjątki wydają się być mechanizmem najbardziej poręcznym. W niektórych przypadkach wyjątki zdają się nie mieć alternatywy - na przykład w sytuacji wystąpienia błędu w konstruktorze.

W programach stworzonych z użyciem C++Buildera występować mogą dwa rodzaje wyjątków: wyjątki C++ i wyjątki biblioteki VCL. Ogólne zasady obsługi obydwu tych wariantów są w zasadzie takie same, istnieje jednak między nimi kilka znaczących różnic.

W związku z obsługą wyjątków C++ używa trzech słów kluczowych: try, catch i throw; C++Builder wzbogaca tę listę o słowo kluczowe __finally. Każde z wymienionych słów kluczowych pełni rolę nagłówka pewnego bloku (czyli kodu zamkniętego w nawiasy {}); po każdym bloku try wystąpić musi jeden lub więcej bloków catch albo dokładnie jeden blok __finally.

Słowo kluczowe try

Słowo kluczowe try wystąpić może w C++ w dwóch odmianach. Pierwsza z nich to wspomniany przed chwilą nagłówek bloku try, druga natomiast, zwana funkcyjnym blokiem try, polega na poprzedzeniu słowem try nawiasu otwierającego treść funkcji lub - w przypadku konstruktora - dwukropka rozpoczynającego listę inicjacyjną.

C++Builder nie obsługuje obecnie funkcyjnych bloków try, co jednak wobec ograniczonego ich zastosowania (głównie w odniesieniu do konstruktorów) nie powinno być szczególnie dotkliwe dla programistów. Zgodnie z obietnicą Borlanda funkcyjne bloki try mają się pojawić w wersji 6. kompilatora.

Słowo kluczowe catch

W najprostszym wariancie obsługi wyjątków pojedynczy blok catch występuje bezpośrednio po bloku try (lub funkcyjnym bloku try). Jego nagłówek rozpoczyna się słowem catch, po którym w nawiasach okrągłych specyfikowany jest typ obsługiwanego wyjątku i (opcjonalnie) zmienna reprezentująca instancję wyjątku (jako klasy). Blok taki - zwany zwyczajowo „obsługą wyjątku” (ang. exception handler) - wykonywany jest wówczas, gdy w czasie realizacji poprzedzającego go bloku try, wystąpi wyjątek dokładnie takiej klasy, jak specyfikowana w nagłówku. Podanie wielokropka (...) zamiast nazwy klasy powoduje przechwycenie przez blok catch wszystkich nie obsłużonych jeszcze wyjątków. Oto przykład typowej konstrukcji try…catch:

try

{

// kod, w którym wystąpić może wyjątek

}

catch (exception1& e)

{

// obsługa wyjątku klasy exception1

}

catch (exception1& e)

{

// obsługa wyjątku klasy exception2

}

catch (...)

{

// obsługa wszystkich nie obsłużonych jeszcze wyjątków

}

Słowo kluczowe __finally

Istnieją sytuacje, gdy dany blok kodu musi być bezwzględnie wykonany, niezależnie od uprzedniego wystąpienia ewentualnych wyjątków. Osiągnięcie tego celu umożliwia konstrukcja try…__finally, składająca się z bloku try, po którym występuje dokładnie jeden blok __finally. Wykonanie takiej konstrukcji rozpoczyna się od wykonania bloku try; gdy wykonanie to zostanie normalnie zakończone lub przerwane wskutek wystąpienia wyjątku, rozpoczyna się wykonanie bloku __finally:

try

{

// kod, w którym wystąpić może wyjątek

}

__finally

{

// ten fragment kodu wykonany zostanie zawsze

// niezależnie od tego, w jaki sposób zakończyło się

// wykonanie poprzedzającego bloku try

}

Należy zauważyć, iż konstrukcje try…catch i try…finally mogą być nawzajem w sobie zagnieżdżane, na przykład tak:

try

{

try

{

.....

}

catch (exception1& e)

{

.....

}

catch (exception2& e)

{

.....

}

catch (...)

{

.....

}

}

__finally

{

.....

}

Słowo kluczowe throw

Słowo kluczowe throw może wystąpić w dwóch wariantach. W pierwszym wariancie służy ono do wygenerowania (throw) lub ponowienia (rethrow) wyjątku określonego typu. Może ono wówczas poprzedzać zmienną reprezentującą wyjątek (najczęściej obiekt), zamkniętą w nawiasy okrągłe lub po prostu oddzieloną spacją (jak np. w instrukcji return), może też wystąpić samodzielnie. Znaczenie tego ostatniego przypadku zależne jest od miejsca wystąpienia słowa throw: jego wystąpienie w obszarze bloku catch powoduje ponowienie obsługiwanego właśnie wyjątku, wystąpienie w innym miejscu powoduje natomiast wywołanie funkcji terminate() standardowo kończącej wykonanie programu.

Słowo kluczowe throw nie może być używane w obrębie kodu biblioteki VCL.

Drugi wariant słowa throw - tzw. klauzula throw - pozwala na wyspecyfikowanie listy typów wyjątków, które mogą być generowane przez daną funkcję. Lista ta zamknięta w nawiasy okrągłe następuje po słowie throw i stanowi przyrostek deklaracji funkcji, na przykład:

void* MyFunc(int a, int b) throw (myexcept1, myexcept2);

Pusta lista oznacza, iż funkcja nie może generować żadnych wyjątków.

Nie obsłużone i nieoczekiwane wyjątki

Poza wymienionymi słowami kluczowymi C++ dostarcza dodatkowe środki umożliwiające uporanie się z wyjątkami nie obsłużonymi lub niespodziewanymi - te ostatnie pochodzić mogą od funkcji generujących wyjątki nie wyspecyfikowane w klauzuli throw (o której pisaliśmy przed chwilą).

Jeżeli zaistniały wyjątek nie zostanie obsłużony, wywoływana jest funkcja terminate(); wywołuje ona z kolei funkcję abort(), kończącą działanie programu. Sytuacja taka nie jest pożądana, bowiem funkcja abort() nie zapewnia destrukcji lokalnych obiektów. Można bardzo łatwo zabezpieczyć się przed wywołaniem funkcji terminate(), „zanurzając” treść funkcji WinMain() (main() w aplikacjach konsolowych) w bloku try…catch, dzięki czemu przechwycone zostaną wyjątki nie obsłużone na niższych poziomach. Można również zmienić standardowe zachowanie funkcji terminate(), definiując własną funkcję obsługi nie obsłużonych wyjątków; nazwę tej funkcji należy podać jako argument wywołania funkcji std::set_terminate(). Konieczne jest ponadto dołączenie pliku nagłówkowego <stdexcept>:

void TerminateHandler();

.....

.....

#include <stdexcept>

std::set_terminate(TerminateHandler);

Reakcją na wystąpienie nieoczekiwanego wyjątku jest wywołanie funkcji unexpected(). Jej standardowe zachowanie, sprowadzające się do wywołania funkcji terminate(), można zmienić, podając nazwę własnej funkcji obsługi jako argument wywołania funkcji std::set_unexpected(); ponownie należy pamiętać o dołączeniu pliku nagłówkowego <stdexcept>.

Posługiwanie się wyjątkami

Tworząc kod źródłowy, należy liczyć się z tym, iż jego wykonanie może powodować wystąpienie różnego rodzaju wyjątków. Nie należy odkładać „oprogramowania wyjątków” na później, lecz uczynić ich obsługę integralną częścią zadania. Prosta na pozór czynność wyświetlenia formularza modalnego może być co prawda zapisana w trzech wierszach:

TFormX* const FormX = new TFormX(0);

FormX->ShowModal();

delete FormX;

jest ona jednak kompletnie nieprzygotowana na ewentualne wyjątki - jeżeli takowe wystąpią w czasie realizacji metody ShowModal(), ostatnia z instrukcji nie wykona się i instancja formularza pozostanie nieusunięta. Namiastkę „bezpiecznej” wersji wyświetlenia formularza modalnego przedstawiliśmy już podczas dyskusji na temat globalnych wskaźników formularzy - nie dokonuje on co prawda obsługi wyjątków, lecz przynajmniej zapewnia bezwarunkowe wykonanie instrukcji delete.

W odniesieniu do każdego fragmentu programu należy zatem zdecydować, czy powodowane przez niego wyjątki mają zostać również przezeń obsłużone, czy też ich obsługa odbywać się będzie w innym miejscu kodu, na wyższym poziomie wywołania. Należy przy tym zdawać sobie sprawę z różnego charakteru wyjątków - mogą być one związane z samym językiem, z biblioteką standardową C++, z biblioteką VCL, mogą być także generowane ad hoc przez sam kod stworzony przez użytkownika.

W tym ostatnim przypadku należy uważać, by w ramach żadnej z funkcji nie generować wyjątków innych typów niż określone w jej klauzuli throw (o ile deklaracja funkcji klauzulę taką posiada), w przeciwnym razie wyjątki potraktowane zostaną jako nieoczekiwane (sterowanie przekazane zostanie do funkcji unexpected() zamiast odpowiedniego bloku try…catch.

Specyfikując bloki catch, nie należy podejmować się obsługi tych typów wyjątków, których program tak naprawdę obsłużyć nie potrafi; podjęta obsługa wyjątku nie musi być jednak doprowadzona do końca - jeżeli w konkretnej sytuacji nie jest możliwe kompletne obsłużenie wyjątku, po przeprowadzeniu niezbędnych czynności „organizacyjnych” należy ponowić wyjątek za pomocą instrukcji throw (zdanie to należy oczywiście interpretować w kategorii tworzonego kodu źródłowego). Jeżeli nagłówek bloku catch specyfikuje zmienną reprezentującą instancję wyjątku, jej typ powinien być określony jako referencja - bowiem niektóre wyjątki, jak np. wyjątki biblioteki VCL nie mogą być „przechwytywane” przez wartość.

Definiując klasy-kontenery (container classes) należy uczynić ich kod „obojętnym na wyjątki” (exception-neutral) - ich obsługa powinna dokonywać się w funkcji wywołującej metodę klasy-kontenera.

Nie należy nigdy generować wyjątków w ramach destruktora - destruktory mogą być bowiem wywoływane w ramach tzw. porządkowania stosu (ang. stack unwinding) związanego z przejściem do realizacji bloku catch; wystąpienie wyjątku na tym etapie ma charakter rekursywny i kończy się wywołaniem funkcji terminate(). Z tego względu zaleca się również opatrywanie deklaracji destruktorów klauzulą throw z pustą listą specyfikacji.

Zarządzanie pamięcią operacyjną przy użyciu operatorów new i delete

Specyfikacja biblioteki VCL wymaga, by wszystkie obiekty wywodzące się z klasy TObject alokowane były dynamicznie w wolnej pamięci. Obszar, z którego przydzielana jest pamięć na potrzeby obiektów programu, nazywany jest zwyczajowo „stertą”. Określenie to jest jednak adekwatne raczej w odniesieniu do gospodarowania pamięcią za pomocą funkcji malloc() i free(); opisując operatory new i delete pozostaniemy przy określeniu „wolna pamięć”.

W przeciwieństwie do funkcji malloc(), zwracającej amorficzny wskaźnik (typu void* ) - co w przypadku tworzenia obiektu wymaga rzutowania na odpowiedni typ - typ wskaźnika zwracanego przez operator new zgodny jest z typem tworzonego obiektu. Ponadto operatory new i delete mogą być przeciążane przez poszczególne klasy (dla realizacji pewnych specyficznych schematów przydziału i zwalniania pamięci) - funkcje malloc() i free() nie stwarzają takich możliwości.

Należy unikać wywoływania metody Free() klas biblioteki VCL, gdyż nie daje ona gwarancji zwolnienia pamięci przydzielonej przez operator new ani wywołania destruktora. W celu destrukcji obiektu VCL zawsze należy posługiwać się operatorem delete.

W przypadku gdy operator new nie potrafi przydzielić żądanej pamięci, generowany jest wyjątek std::bad_alloc (jest on zdefiniowany w pliku nagłówkowym <new>, o którego dołączeniu należy pamiętać). Wskaźnik zwracany przez operator new nie jest więc (standardowo) wskaźnikiem pustym, nie musi być więc testowany na wartość NULL, w zamian za co konieczne jest oczywiście odpowiednie oprogramowanie wspomnianego wyjątku. Można go na przykład obsłużyć „na gorąco”:

void CreateObject(TMyObject* MyObject) throw()

{

try

{

MyObject = new TMyObject();

}

catch(std::bad_alloc)

{

// wyświetl komunikat o niemożności przydziału pamięci

// i uporaj się z problemem albo podstaw NULL pod MyObject

// dla sygnalizacji, iż żądany obiekt nie został utworzony

}

}

albo zlecić tę obsługę funkcji wywołującej:

void CreateObject(TMyObject* MyObject) throw(std::bad_alloc)

{

MyObject = new TMyObject();

}

Klauzula throw została tu użyta dla podkreślenia, iż pierwszy wariant funkcji nie „wypuszcza” nie obsłużonych wyjątków, drugi zaś może przekazywać jedynie wyjątek std::bad_alloc.

Tę standardową reakcję na niepowodzenie przydziału pamięci (przez operator new) można zmienić, definiując własną funkcję obsługi (jeżeli jest ona funkcją składową klasy, musi być funkcją statyczną) i przekazanie jej jako argumentu wywołania funkcji std::set_new_handler() (ponownie nie należy zapominać o dołączeniu pliku <new>), na przykład:

#include <new>

void OutOfMemory()

{

// spróbuj zwolnić niepotrzebną pamięć

// jeżeli w wyniku tego powtórna próba alokacji powiedzie się,

// niniejsza funkcja nie zostanie ponownie wywołana

//

// w przeciwnym razie zainstaluj inną funkcję obsługi

// lub wygeneruj wyjątek

}

.....

std::set_new_handler(OutOfMemory);

Jeżeli za pomocą funkcji std::set_new_handler()zdefiniujemy własną funkcję obsługi braku pamięci, menedżer pamięci wywoływał będzie tę ostatnią aż do skutku (pomyślnego przydziału) albo anulowania wspomnianej definicji (przez wywołanie funkcji std::set_new_handler() z argumentem 0) - w tym ostatnim przypadku cała sprawa zakończy się wygenerowaniem wyjątku std::bad_alloc. Można się o tym przekonać, studiując definicję operatora new w pliku new.cpp:

void * _RTLENTRY _EXPFUNC operator new( size_t size )

{

void * p;

.....

while ( (p = malloc(size)) == NULL)

if (_new_handler)

_new_handler();

else

throw __bad_alloc;

return p;

}

Za lakonicznym stwierdzeniem „spróbuj zwolnić niepotrzebną pamięć” w opisie działania funkcji OutOfMemory() kryje się zazwyczaj zadanie bardzo skomplikowane, bowiem znaczenie słowa „niepotrzebna” ściśle związane jest ze specyfiką konkretnego programu. Najczęściej taką „niepotrzebną” - czy też „niekoniecznie potrzebną” - pamięcią są wszelkiego rodzaju bufory, nie wpływające na samą logikę programu, lecz zwiększające (czasem nawet znacznie) jego efektywność. Niekiedy jednak brak pamięci wynika z uwarunkowań zewnętrznych w stosunku do samego programu - np. „ubogiej” konfiguracji komputera lub dużej liczby uruchomionych zadań. W takich warunkach wspomniane stwierdzenie staje się po prostu pustosłowiem, zaś żadna procedura obsługi nie jest w stanie odzyskać nieistniejącej pamięci. Jedyne, co można wówczas zrobić, to starać się z wyprzedzeniem przewidzieć brak pamięci, informując o tym użytkownika programu - umożliwia to prosty, acz użyteczny trick.

Na samym początku realizacji programu należy mianowicie zaalokować pewien obszar pamięci (jak duży? - to zależy od specyfiki programu) w charakterze li tylko owej „niepotrzebnej” pamięci i kontynuować normalne wykonanie programu; w sytuacji, gdy operator new zasygnalizuje niepowodzenie, należy pamięć tę zwolnić (w procedurze niestandardowej obsługi) i spowodować ponowienie próby przydziału (zwyczajnie wychodząc z tej procedury), uprzednio jednak sygnalizując użytkownikowi (za pomocą stosownego komunikatu), iż być może właśnie rozpoczynają się problemy z brakiem pamięci.

Operator new może być używany nie tylko do tworzenia instancji klas, lecz również przydziału „surowej” pamięci (raw memory) w sytuacji, gdy żądany jej rozmiar znany jest dopiero w momencie przydziału. Jest to charakterystyczne zwłaszcza w odniesieniu do struktur Win32 API, na przykład:

DWORD StructureSize = APIFunctionToGetSize(SomeParameter);

WIN32STRUCTURE* PointerToStructure;

PointerToStructure =

static_cast<WIN32STRUCTURE*>(operator new(StructureSize));

Tak przydzielona struktura musi być zwolniona za pomocą operatora delete:

operator delete(PointerToStructure);

W taki oto sposób można obyć się bez funkcji malloc() i free().

Osobnego komentarza wymaga sposób przydziału pamięci na potrzeby tablic obiektów. Przydział taki realizowany jest za pomocą operatora new[], zaś przydzielona pamięć musi być zwalniana przy użyciu operatora delete[]- nie należy mylić ich z operatorami new i delete. Gdy za pomocą operatora new żądamy utworzenia obiektu będącego tablicą obiektów, najpierw konstruowana jest sama tablica (za pomocą operatora new[]), a następnie każdy jej obiekt składowy konstruowany jest za pomocą swego domyślnego konstruktora. Destrukcja tablicy obiektów musi przebiegać w odwrotnej kolejności - najpierw wywoływane są destruktory obiektów składowych, a następnie destruktor tablicy jako takiej. Aby wymusić taki właśnie scenariusz, należy zwalniać tę tablicę za pomocą operatora delete[], nie delete:

delete [] SomeArray;

Jeżeli przez pomyłkę opuścilibyśmy nawiasy kwadratowe, w najlepszym przypadku uzyskamy jedynie zwolnienie pierwszego obiektu z tablicy.

Aby zademonstrować rzecz całą na konkretnym przykładzie, przywołajmy naszą klasę TBook i utwórzmy dwuwymiarową tablicę odzwierciedlającą regał podzielony na półki, z których każda zawiera określoną liczbę książek (dla lepszej czytelności użyłem polskich liter, których oczywiście kompilator tolerował nie będzie - przyp. tłum.):

TBook** Regał = new TBook*[LiczbaPółek];

for(int i=0; i<LiczbaPółek; ++i)

{

Regał[i] = new TBook[LiczbaKsiążekNaPółce]

}

Zwolnienie obiektu Regał musi być wykonane następująco:

delete [] Regał;

Jeśli już mowa o tablicach obiektów, to godnym polecenia w tym względzie jest szablon o nazwie <vector> z biblioteki standardowej. Umożliwia on użycie dowolnego (niekoniecznie domyślnego) konstruktora obiektu składowego, zajmuje się również alokacją i zwalnianiem pamięci, a także jej realokacją - w tym przypadku zbędna staje się więc funkcja realloc().

Ze względu na ograniczoną objętość książki nie opisujemy tu operatora nothrow new (w przypadku niemożności przydziału zwraca on pusty wskaźnik, nie generując wyjątku), ani też wykorzystania operatora new do alokowania pamięci pod określonym adresem.

Rzutowanie typów w stylu C++

W C++ istnieją cztery rodzaje rzutowania typów (casts). Ich krótki opis przedstawia tabela 2.1.

Tabela 2.1. Rodzaje rzutowania typów w C++

Rzutowanie

Zastosowanie

static_cast<T>(exp)

Stosowane do rzutowania w obrębie jednej grupy wyrażeń arytmetycznych, referencji, wskaźników lub typów wyliczeniowych. Nie jest możliwe rzutowanie pomiędzy ww. grupami, na przykład rzutowanie wskaźnika na typ arytmetyczny.

dynamic_cast<T>(exp)

Używane do rzutowania w obrębie hierarchii dziedziczonych klas. Jeżeli przykładowo klasa X wywodzi się z klasy O, to wskaźnik do klasy O może być w ten sposób rzutowany na wskaźnik do klasy X, o ile oczywiście konwersja taka jest wykonalna.

T może być wskaźnikiem lub referencją do klasy docelowej lub wskaźnikiem amorficznym (void*).

exp może być wskaźnikiem lub referencją. Aby rzutowanie dynamiczne było wykonalne, klasa bazowa musi być klasą polimorficzną, tzn. zawierać musi przynajmniej jedną wirtualną funkcję składową.

Jeżeli rzutowanie pomiędzy wskaźnikami nie jest wykonalne, jego wynikiem jest wskaźnik pusty (NULL). Jeżeli nie jest wykonalne rzutowanie pomiędzy referencjami, generowany jest wyjątek std::bad_cast (należy dołączyć plik <typeinfo>).

const_cast<T>(exp)

To rzutowanie jako jedyne ma wpływ na „niezmienność” i (lub) „ulotność” wyrażenia wynikających z klauzul (odpowiednio) const i volatile. Przydaje się to w sytuacji, gdy chcemy przekazać niezmienny (const) obiekt do funkcji, w której deklaracja parametru nie gwarantuje co prawda jego niezmienności, lecz niezmienność taka wynika z konkretnych warunków na aktualnym etapie wykonania programu.

Typ T i typ wyrażenia exp mogą różnić się od siebie co najwyżej pod względem klauzul const i volatile.

reinterpret_cast<T>(exp)

Rzutowanie to sprowadza się do odmiennej interpretacji wzorca bitowego wyrażenia exp z ewentualnym jego poszerzeniem lub obcięciem. Jest ono w dużym stopniu zależne od konkretnej implementacji języka, a więc jego wynik zwykle trudno jest a priori określić bez przestudiowania dokumentacji; w C++Builderze ten rodzaj rzutowania wykorzystywany jest najczęściej do konwersji pomiędzy wskaźnikami a typem int, jak również pomiędzy wskaźnikami do funkcji.

T może być referencją, typem arytmetycznym, wskaźnikiem do danych, funkcji lub funkcji składowej.

Do najczęściej wykorzystywanych rodzajów rzutowania należą: static_cast (dla trywialnych konwersji pomiędzy np. int a double) i dynamic_cast. Przykład rzutowania statycznego prezentuje poniższy fragment kodu, znany już z wydruku 2.4:

int Sum = 0;

int* Numbers = new int[20];

for(int i=0; i<20; ++i)

{

Numbers[i] = i*i;

Sum += Numbers[i];

}

double Average = static_cast<double>(Sum)/20;

Rzutowanie dynamiczne w C++Builderze stosowane jest najczęściej do konwersji parametrów TObject* (w funkcjach zdarzeniowych VCL) oraz TComponent* (przy wskazaniu właściciela komponentu) na rzeczywisty typ komponentu, na przykład TForm*. Poniższy przykład ilustruje prosty sposób sprawdzenia, czy dany komponent ulokowany został wprost na formularzu, czy też w ramach jakiegoś dodatkowego komponentu (np. TPanel) - w tym drugim przypadku nie będzie wykonalna konwersja wskazania właściciela komponentu na typ TForm*, jej wynikiem będzie więc NULL:

.....

TForm* OwnerForm = dynamic_cast<TForm*>(Owner);

if (OwnerForm)

{

// komponent znajduje się bezpośrednio na formularzu

}

Rzutowania typów w C++ realizują dość specyficzne funkcje, więc każdy ich rodzaj powinien być starannie dobrany do rzeczywistych potrzeb. Ich zaletą jest prostota specyfikacji, mają więc one dodatni wpływ na czytelność i przejrzystość kodu.

Używanie preprocesora

Nie należy wykorzystywać preprocesora do definiowania stałych - do tego celu służy dyrektywa const - ani do tworzenia makr funkcyjnych - w tym celu wykorzystać możemy funkcje „wstawialne” inline (lub szablony takich funkcji).

Dla przykładu stałą reprezentującą liczbę π zdefiniować można następująco:

const double PI = 3.1415926534;

Definicję tę można również zawrzeć w ramach klasy, na przykład:

class Circle

{

public:

static const double PI; // to tylko deklaracja

}

W pliku implementacyjnym (*.cpp) stosowna definicja mogłaby wyglądać następująco:

const double Circle::PI = 3.1415926534; // to jest definicja i inicjalizacja

Zwróć uwagę, iż ze względu na statyczny charakter stałej PI wszystkie instancje klasy Circle będą dzielić jej pojedynczą kopię. Zauważ również, iż stała ta inicjowana jest w pliku implementacyjnym (zazwyczaj definicję taką umieszcza się bezpośrednio po dyrektywie #include, włączającej plik nagłówkowy, zawierający deklarację klasy). Wyjątkiem od tej zasady są stałe mające „całkowitoliczbową” reprezentację wewnętrzną: char, short, long, unsigned i int; mogą być one inicjowane wprost w definicji klasy.

Dla połączenia kilku stałych w jedną grupę znakomicie nadaje się dyrektywa enum:

enum LanguagesSupported { English, Chinese, Japanese, French };

Niekiedy dyrektywę enum wykorzystuje się do zadeklarowania pojedynczej stałej całkowitej:

enum { LENGTH = 255 };

Takie deklaracje spotyka się najczęściej w definicjach klas; wydaje się jednak, iż rozwiązanie zaprezentowane przed chwilą w związku z liczbą PI jest lepszym rozwiązaniem.

Równie łatwo wyeliminować można z kodu programu makra funkcyjne. Poniższe makro:

#define cubeX(x) ( (x)*(x)*(x) )

zastąpić można z powodzeniem następującą funkcją:

inline double cubeX(double x) { return x*x*x }

Niestety, funkcja ta wymaga jako argumentu wyrażenia typu double - przekazanie parametru typu int musiałoby by poprzedzone jego rzutowaniem na typ double; analogiczne makro nie posiada takiej wady. Problem ten może zostać rozwiązany w dwojaki sposób: przez zdefiniowanie funkcji jako przeciążonej lub stworzenie szablonu funkcji. To ostatnie rozwiązanie nie jest w tym przypadku szczególnie odpowiednie, gdyż szablony mogą być stosowane również w odniesieniu do klas, co w przypadku operacji podnoszenia do potęgi miałoby niewielki sens. Pozostaje więc pierwsze rozwiązanie - uzupełnienie definicji funkcji o aspekt odpowiedni dla parametru i wyniku typu int:

inline int cubeX(int x) { return x*x*x }

Preprocesor bywa szczególnie użyteczny w realizacji tzw. strażników wstawiania (ang. include guards), zapewniających, iż dany fragment pojawi się w treści kodu źródłowego tylko jednokrotnie. Oto typowy wygląd pliku nagłówkowego C++Buildera:

#ifndef Unit1H // czy symbol Unit1H jest już zdefiniowany?

#define Unit1H // nie, zdefiniuj go i uwidocznij związany z nim kod

// tutaj następuje zasadnicza treść pliku nagłówkowego

#endif // koniec uwarunkowanego fragmentu

Niezależnie od tego, ile razy dyrektywa #include włączająca powyższy plik nagłówkowy pojawi się w treści modułu źródłowego, treść tego pliku wstawiona zostanie do modułu tylko raz; zapewnia to symbol Unit1H, będący właśnie wspomnianym strażnikiem.

Może to wydawać się trywialne, lecz oddaje nieocenione usługi w przypadku dużych projektów nafaszerowanych dyrektywami #include. W poniższym przykładzie symbole - strażnicy zapewniają, iż każdy z plików nagłówkowych włączony będzie co najwyżej raz:

#ifndef Unit1H // czy symbol Unit1H jest już zdefiniowany?

#define Unit1H // zdefiniuj go i uwidocznij związany z nim kod

#ifndef ModalUnit2H

#include "ModalUnit2.h"

#endif

#ifndef ModalUnit3H

#include "ModalUnit3.h"

#endif

......

#endif // koniec uwarunkowanego fragmentu

W powyższych przykładach nazwa strażnika tworzona była poprzez dołączenie litery H na końcu nazwy modułu. Nie jest to kwestia przypadku - poza oczywistymi względami czytelności ma to związek z menedżerem projektów. W poprzednich wersjach C++Buildera pliki *.CPP i *.H o tej samej nazwie traktowane były łącznie, jako pojedynczy moduł źródłowy; począwszy od wersji 5. są one traktowane łącznie tylko wówczas, gdy nazwa symbolu - strażnika w pliku *.H stosuje się do takiej właśnie konwencji.

Menedżer projektów w wersji 5. C++Buildera usprawniony został o graficzne uwidocznienie zależności kodu poszczególnych modułów źródłowych od poszczególnych plików nagłówkowych. Informacja o tej zależności pobierana jest z plików wynikowych *.obj, tak więc jej pojawienie się (a także jej aktualność) wymaga przynajmniej jednokrotnej kompilacji projektu i rekompilacji po dokonaniu zmian w projekcie.

Wykorzystanie standardowej biblioteki C++

Standardowa biblioteka C++ (C++ Standard Library), włączając w to standardową bibliotekę szablonów (STL - Standard Template Library), stanowi integralną część standardu ANSI/ISO C++. Poznanie jej zasobów i możliwości pozwala niejednokrotnie uniknąć niepotrzebnego kodowania. Biblioteka standardowa ma niezaprzeczalną przewagę nad chałupniczo tworzonym kodem, została bowiem gruntownie przetestowana i zoptymalizowana, a jako element standardu jest oczywiście przenośna pomiędzy różnymi platformami sprzętowymi i systemowymi.

Gdyby charakteryzować krótko bibliotekę standardową, należałoby wymienić następujące jej główne elementy:

Dominującą część biblioteki standardowej stanowią szablony, co czyni jej kod niezwykle użytecznym. Na przykład szablon vector może być używany do tworzenia tablic danych dowolnego typu, jest więc godnym następcą tablic C++ i powinien być używany zamiast nich (z wyjątkiem być może trywialnych przypadków) wszędzie, gdzie jest to możliwe.

W tym miejscu pojawia się w oryginale sekcja, która w polskiej wersji mogłaby mieć tytuł „Literatura zalecana” lub „Warto przeczytać…”. Ze względu na specyfikę polecanych pozycji (wydawnictwa anglojęzyczne dostępne na rynku amerykańskim) nie włączyłem jej do tłumaczenia. Być może HELION ma jakieś propozycje co do polskiej literatury o programowaniu w C++.

Podsumowanie

W rozdziale tym przedstawiliśmy wybrane zagadnienia związane z tworzeniem kodu źródłowego w języku C++, między innymi:

Proszę zwrócić uwagę, iż rozbudowana instrukcja postaci

if ....
{
...
}
else if ....
{
...
}
else if ....
{
...
}
itd. jest tak naprawdę zagnieżdżeniem instrukcji if, bowiem badanie kolejnych warunków uzależnione jest od spełnienia bądź niespełnienia warunków poprzedzających - przyp. tłum.

W przeznaczonym do kompilacji tekście programu źródłowego - wraz z komentarzami - istotna jest oczywiście tylko jego treść, zaś wspomniane formatowanie może tylko przeszkodzić w kompilacji (proszę spróbować skompilować program zapisany w postaci dokumentu MS Worda!); uwaga o odmiennym stylu może więc co najwyżej odnosić się do sposobu prezentacji kodu w edytorze środowisk zintegrowanych, takich jak np. C++Builder - przyp. tłum.

Określenia „obiekt” używamy tu w znaczeniu potocznym, na oznaczenie dowolnego bytu języka C++, nie zaś w konkretnym znaczeniu instancji klasy.

W rozdziale 1. stwierdziłem, iż analogią referencji C++ jest w Object Pascalu przekazywanie parametrów przez zmienną oraz „mapowanie” zmiennych za pomocą klauzuli absolute; tak rozumiane referencje nie mogą ulegać zmianie i mogą być referencjami pustymi (jeżeli zadeklarować je jako absolute 0); Autorowi oryginału prawdopodobnie chodzi tu jednak o coś innego - obiekty Object Pascala reprezentowane są pod postacią wskaźników, jednak dostęp do ich pól, metod i właściwości odbywa się bez użycia operatora dereferencji „^”, zewnętrznie więc wygląda to tak, jakbyśmy mieli do czynienia właśnie z referencjami. W przeciwieństwie do C++ nie istnieją bowiem w Object Pascalu zmienne reprezentujące bezpośrednio instancje obiektów - przyp. tłum.

W sprawie dalszych szczegółów Autorzy oryginału odsyłają Czytelników do następujących pozycji: C++ FAQs Second Edition, Cline i in., 1999, str. 235 oraz Large-Scale C++ Software Design, Lakos, 1996, str. 537 - przyp. red.

Autorzy oryginału polecają w tym miejscu książkę H.Suttera Exceptional C++: 47 Engineering Puzzles, Programming Problems and Solutions, wyd. Addison-Wesley Longman Inc. 2000 - przyp. red.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 D:\helion\C++Builder 5\R02-03.DOC



Wyszukiwarka

Podobne podstrony:
R14-03, ## Documents ##, C++Builder 5
R17-03, ## Documents ##, C++Builder 5
R18-03, ## Documents ##, C++Builder 5
R04-03, ## Documents ##, C++Builder 5
R13-03, ## Documents ##, C++Builder 5
R08-03, ## Documents ##, C++Builder 5
R09-03, ## Documents ##, C++Builder 5
R05-03, ## Documents ##, C++Builder 5
R07-03, ## Documents ##, C++Builder 5
R03-03, ## Documents ##, C++Builder 5
R15-03, ## Documents ##, C++Builder 5
R16-03, ## Documents ##, C++Builder 5
R11-03, ## Documents ##, C++Builder 5
r-13-00, ## Documents ##, C++Builder 5
r-12-00, ## Documents ##, C++Builder 5
r02 p 03 VB4TWEFWVYXRAWRJ4Q3YLXJCA7EQLI5NXUADAUI
2011 03 03 Document 001
r-10-00, ## Documents ##, C++Builder 5

więcej podobnych podstron