BAZA WIEDZY
KURSY
Bazy danych w PHP
Kurs AdvancedAJAX
Kurs ASP
Kurs ASP.NET
Kurs C++
Kurs CSS
Kurs HTML
Kurs HTML drugi
Kurs JavaScript
Kurs MySQL
Kurs PHP
Kurs RSS
Kurs XHTML
Obiekty DOM
MANUALE
CSS1 - W3C
DOM - w budowie
PHP 2005
PHP 2006
Wyrażenia regularne
SHOUTBOX
STAT
Online: 4 | UU: 208

Typy zmiennych

W C++ typ zmiennej jest sprawą niezwykle ważną. Gdy określamy go przy deklaracji, zostaje on trwale „przywiązany” do zmiennej na cały czas działania programu. Nie może więc zajść sytuacja, w której zmienna zadeklarowana na przykład jako liczba całkowita zawiera informację tekstową czy liczbę rzeczywistą.

Niektóre języki programowania pozwalają jednak na to. Delphi i Visual Basic są wyposażone w specjalny typ Variant , który potrafi przechowywać zarówno dane liczbowe, jak i tekstowe. PHP natomiast w ogóle nie wymaga podawania typu zmiennych.

Chociaż wymóg ten wygląda na poważny mankament C++, w rzeczywistości wcale nim nie jest. Bardzo trudno wskazać czynność, która wymagałaby zmiennej „uniwersalnego typu”, mogącej przechowywać każdy rodzaj danych. Jeżeli nawet zaszłaby takowa konieczność, możliwe jest zastosowanie przynajmniej kilku niemal równoważnych rozwiązań 1 .

Generalnie jednak jesteśmy „skazani” na korzystanie z typów zmiennych, co mimo wszystko nie powinno nas smucić :) Na osłodę proponuję bliższe przyjrzenie się im. Będziemy mieli okazję zobaczyć, że ich możliwości, elastyczność i zastosowania są niezwykle szerokie.

Modyfikatory typów liczbowych

Dotychczas w swoich programach mieliśmy okazję używać głównie typu int , reprezentującego liczbę całkowitą. Czasem korzystaliśmy także z float , będącego typem liczb rzeczywistych.

Dwa sposoby przechowywania wartości liczbowych to, zdawałoby się, bardzo niewiele. Zważywszy, iż spora część języków programowania udostępnia nawet po kilkanaście takich typów, asortyment C++ może wyglądać tutaj wyjątkowo mizernie.

Domyślasz się zapewne, że jest to tylko złudne wrażenie :) Do każdego typu liczbowego w C++ możemy bowiem dołączyć jeden lub kilka modyfikatorów , które istotnie zmieniają jego własności. Spróbujmy dokładnie przyjrzeć się temu mechanizmowi.

Typy ze znakiem i bez znaku

Typ liczbowy int może nam przechowywać zarówno liczby dodatnie, jak i ujemne. Dosyć często jednak nie potrzebujemy wartości mniejszych od zera. Przykładowo, ilość punktów w większości gier nigdy nie będzie ujemna; to samo dotyczy liczników upływającego czasu, zmiennych przechowujących wielkość plików, długości odcinków, rozmiary obrazków - i tak dalej.

Możemy rzecz jasna zwyczajnie zignorować obecność liczb ujemnych i korzystać jedynie z wartości dodatnich. Wadą tego rozwiązania jest marnotrawstwo: tracimy wtedy połowę miejsca zajmowanego w pamięci przez zmienną. Jeżeli na przykład int mógłby zawierać liczby od -10 000 do +10 000 (czyli 20 000 możliwych wartości 2 ), to ograniczylibyśmy ten przedział do 0…+10 000 (a więc skromnych 10 000 możliwych wartości).

Nie jest to może karygodna niegospodarność w przypadku jednej zmiennej, ale gdy mówimy o kilku czy kilkunastu tysiącach podobnych zmiennych 3 , ilość zmarnowanej pamięci staje się znaczna.

Należałoby zatem powiedzieć kompilatorowi, że nie potrzebujemy liczb ujemnych i w zamian za nie chcemy zwiększenia przedziału liczb dodatnich. Czynimy to poprzez dodanie do typu zmiennej int modyfikatora unsigned (‘nieoznakowany', czyli bez znaku; zawsze dodatni). Deklaracja będzie wtedy wyglądać na przykład tak:

unsigned int uZmienna; // przechowuje liczby naturalne

Analogicznie, moglibyśmy dodać przeciwstawny modyfikator signed (‘oznakowany', czyli ze znakiem; dodatni lub ujemny) do typów zmiennych, które mają zawierać zarówno liczby dodatnie, jak i ujemne:

signed int nZmienna; // przechowuje liczby całkowite

Zazwyczaj tego nie robimy, gdyż modyfikator ten jest niejako domyślnie tam umieszczony i nie ma potrzeby jego wyraźnego stosowania.

Jako podsumowanie proponuję diagram obrazujący działanie poznanych modyfikatorów:


Schemat 6. Przedział wartości typów liczbowych ze znakiem ( signed ) i bez znaku ( unsigned )

Widzimy, że zastosowanie unsigned powoduje „przeniesienie” ujemnej połowy przedziału zmiennej bezpośrednio za jej część dodatnią. Nie mamy wówczas możliwości korzystania z liczb ujemnych, ale w zamian otrzymujemy dwukrotnie więcej miejsca na wartości dodatnie. Tak to już jest w programowaniu, że nie ma nic za darmo :D

Rozmiar typu całkowitego

W poprzednim paragrafie wspominaliśmy o przedziale dopuszczalnych wartości zmiennej, ale nie przyglądaliśmy się bliżej temu zagadnieniu. Teraz zatrzymamy się na nim trochę dłużej i zajmiemy rozmiarem zmiennych całkowitych.

Wiadomo nam doskonale, że pamięć komputera jest ograniczona, zatem miejsce zajmowane w tej pamięci przez każdą zmienną jest również limitowane. W przypadku typów liczbowych przejawia się to ograniczonym przedziałem wartości, które mogą przyjmować zmienne należące do takich typów.

Jak duży jest to przedział? Nie ma uniwersalnej odpowiedzi na to pytanie. Okazuje się bowiem, że rozmiar typu int jest zależny od kompilatora . Wpływ na tę wielkość ma pośrednio system operacyjny oraz procesor komputera.

Nasz kompilator (Visual C++ .NET), podobnie jak wszystkie tego typu narzędzia pracujące w systemie Windows 95 i wersjach późniejszych, jest 32-bitowy . Oznacza to między innymi, że typ int ma u nas wielkość równą 32 bitom właśnie, a więc w przeliczeniu 1 4 bajtom .

Cztery bajty to cztery znaki (na przykład cyfry) – czyżby zatem największymi i najmniejszymi możliwymi do zapisania wartościami były +9999 i -9999?…

Oczywiście, że nie! Komputer przechowuje liczby w znacznie efektywniejszej postaci dwójkowej. Wykorzystanie każdego bitu sprawia, że granice przedziału wartości typu int to aż ą2 31 – nieco ponad dwa miliardy !

Więcej informacji na temat sposobu przechowywania danych w pamięci operacyjnej możesz znaleźć w Dodatku B, Reprezentacja danych w pamięci .

Przedział ten sprawdza się dobrze w wielu zastosowaniach. Czasem jednak jest on zbyt mały (tak, to możliwe :D) lub zwyczajnie zbyt duży. Daje się to odczuć na przykład przy odczytywaniu plików, w których każda wartość zajmuje obszar o ściśle określonym rozmiarze, nie zawsze równym int -owym 4 bajtom (tzw. plików binarnych).

Dlatego też C++ udostępnia nam poręczny zestaw dwóch modyfikatorów, którymi możemy wpływać na wielkość typu całkowitego. Są to: short (‘krótki') oraz long (‘długi'). Używamy ich podobnie jak signed i unsigned – poprzedzając typ int którymś z nich:

short int nZmienna; // "krótka" liczba całkowita
long int nZmienna; // "długa" liczba całkowita

Cóż znaczą jednak te, nieco żartobliwe, określenia „krótkiej” i „długiej” liczby? Chyba najlepszą odpowiedzią będzie tu… stosowna tabelka :)

nazwa

rozmiar

przedział wartości

int

4 bajty

od –2 31 do +2 31 - 1

short int

2 bajty

od -32 768 do +32 767

long int

4 bajty

od –2 31 do +2 31 - 1

Tabela 4. Typy całkowite w 32-bitowym Visual C++ .NET 2

Niespodzianką może być brak typu o rozmiarze 1 bajta. Jest on jednak obecny w C++ – to typ… char :) Owszem, reprezentuje on znak . Nie zapominajmy jednak, że komputer operuje na znakach jak na odpowiadającym im kodom liczbowym . Dlatego też typ char jest w istocie także typem liczb całkowitych!

Visual C++ udostępnia też nieco lepszy sposób na określenie wielkości typu liczbowego. Jest nim użycie frazy __int n , gdzie n oznacza rozmiar zmiennej w bitach. Oto przykłady:

__int8 nZmienna; // 8 bitów == 1 bajt, wartości od -128 do 127

__int16 nZmienna; // 16 bitów == 2 bajty, wartości od -32768 do 32767

__int32 nZmienna; // 32 bity == 4 bajty, wartości od -2 31 do 2 31 – 1

__int64 nZmienna; // 64 bity == 8 bajtów, wartości od -2 63 do 2 63 – 1

__int8 jest więc równy typowi char , __int16short int , a __int32int lub long int . Gigantyczny typ __int64 nie ma natomiast swojego odpowiednika.


Precyzja typu rzeczywistego

Podobnie jak w przypadku typu całkowitego int , typ rzeczywisty float posiada określoną rozpiętość wartości, które można zapisać w zmiennych o tym typie. Ponieważ jednak jego przeznaczeniem jest przechowywanie wartości ułamkowych, pojawia się kwestia precyzji zapisu takich liczb.

Szczegółowe wyjaśnienie sposobu, w jaki zmienne rzeczywiste przechowują wartości, jest dość skomplikowane i dlatego je sobie darujemy 1 :) Najważniejsze są dla nas wynikające z niego konsekwencje. Otóż:

Precyzja zapisu liczby w zmiennej typu rzeczywistego maleje wraz ze wzrostem wartości tej liczby

Przykładowo, duża liczba w rodzaju 10000000.0023 zostanie najpewniej zapisana bez części ułamkowej. Natomiast mała wartość, jak 1.43525667 będzie przechowana z dużą dokładnością, z kilkoma cyframi po przecinku. Ze względu na tę właściwość (zmienną precyzję) typy rzeczywiste nazywamy często zmiennoprzecinkowymi .

Zgadza się – typy. Podobnie jak w przypadku liczb całkowitych możemy dodać do typu float odpowiednie modyfikatory. I podobnie jak wówczas, ujrzymy je w należytej tabelce :)

nazwa

rozmiar

precyzja

float

4 bajty

6–7 cyfr

double float

8 bajtów

15-16 cyfr

Tabela 5. Typy zmiennoprzecinkowe w C++

double (‘podwójny'), zgodnie ze swoją nazwą, zwiększa dwukrotnie rozmiar zmiennej oraz poprawia jej dokładność. Tak zmodyfikowana zmienna jest nazywana czasem liczbą podwójnej precyzji - w odróżnieniu od float , która ma tylko pojedynczą precyzję .

Skrócone nazwy

Na koniec warto nadmienić jeszcze o możności skrócenia nazw typów zawierających modyfikatory. W takich sytuacjach możemy bowiem całkowicie pominąć słowa int i float .

Przykładowe deklaracje:

unsigned int uZmienna;

short int nZmienna;

unsigned long int nZmienna;

double float fZmienna;

mogą zatem wyglądać tak:

unsigned uZmienna;

short nZmienna;

unsigned long nZmienna;

double fZmienna;

Mała rzecz, a cieszy ;) Mamy też kolejny dowód na dużą kondensację składni C++.

***

Poznane przed chwilą modyfikatory umożliwiają nam większą kontrolę nad zmiennymi w programie. Pozwalają bowiem na dokładne określenie, jaką zmienną chcemy w danej chwili zadeklarować i nie dopuszczają, by kompilator myślał za nas ;D

Pomocne konstrukcje

Zapoznamy się teraz z dwoma elementami języka C++, które ułatwiają nieco pracę z różnymi typami zmiennych. Będzie to instrukcja typedef oraz operator sizeof .

Instrukcja typedef

Wprowadzenie modyfikatorów sprawiło, że oto mamy już nie kilka, a przynajmniej kilkanaście typów zmiennych. Nazwy tychże typów są przy tym dosyć długie i wielokrotne ich wpisywanie może nam zabierać dużo czasu. Zbyt dużo.

Dlatego też (i nie tylko dlatego) C++ posiada instrukcję typedef (ang. type definition – definicja typu). Możemy jej użyć do nadania nowej nazwy ( aliasu ) dla już istniejącego typu . Zastosowanie tego mechanizmu może wyglądać choćby tak:

typedef unsigned int UINT;

Powyższa linijka kodu mówi kompilatorowi, że od tego momentu typ unsigned int posiada także dodatkową nazwę - UINT . Staję się ona dokładnym synonimem pierwotnego określenia. Odtąd bowiem obie deklaracje:

unsigned int uZmienna;

oraz

UINT uZmienna;

są w pełni równoważne .

Użycie typedef , podobnie jak jej składnia, jest bardzo proste:

typedef typ nazwa ;

Skutkiem skorzystania z tej instrukcji jest możliwość wstawiania nowej nazwy tam, gdzie wcześniej musieliśmy zadowolić się jedynie starym typem . Obejmuje to zarówno deklaracje zmiennych, jak i parametrów funkcji tudzież zwracanych przez nie wartości. Dotyczy więc wszystkich sytuacji, w których mogliśmy korzystać ze starego typu – nowa nazwa nie jest pod tym względem w żaden sposób ułomna.

Jaka jest praktyczna korzyść z definiowania własnych określeń dla istniejących typów?

Pierwszą z nich jest przytoczone wcześniej skracanie nazw, które z pewnością pozytywnie wpłynie na stan naszych klawiatur ;)) Oszczędnościowe „przydomki” w rodzaju zaprezentowanego wyżej UINT są przy tym na tyle wygodne i szeroko wykorzystywane, że niektóre kompilatory (w tym i nasz Visual C++) nie wymagają nawet ich jawnego określenia!

Możliwość dowolnego oznaczania typów pozwala również na nadawanie im znaczących nazw, które obrazują ich zastosowania w aplikacji. Z przykładem podobnego postępowania spotkasz się przy tworzeniu programów okienkowych w Windows. Używa się tam wielu typów o nazwach takich jak HWND , HINSTANCE , WPARAM , LRESULT itp., z których każdy jest jedynie aliasem na 32-bitową liczbę całkowitą bez znaku. Stosowanie takiego nazewnictwa poważnie poprawia czytelność kodu – oczywiście pod warunkiem, że znamy znaczenie stosowanych nazw :)

Zauważmy pewien istotny fakt. Mianowicie, typedef nie tworzy nam żadnych nowych typów , a jedynie duplikuje już istniejące . Zmiany, które czyni w sposobie programowania, są więc stricte kosmetyczne, choć na pierwszy rzut oka mogą wyglądać na dość znaczne.
Do kreowania zupełnie nowych typów służą inne elementy języka C++, z których część poznamy w następnym rozdziale.

Operator sizeof

Przy okazji prezentacji różnych typów zmiennych podawałem zawsze ilość bajtów, którą zajmuje w pamięci każdy z nich. Przypominałem też kilka razy, że wielkości te są prawdziwe jedynie w przypadku kompilatorów 32-bitowych, a niektóre nawet tylko w Visual C++.

Z tegoż powodu mogą one szybko stać się po prostu nieaktualne. Przy dzisiejszym tempie postępu technicznego, szczególnie w informatyce, wszelkie zmiany dokonują się w zasadzie nieustannie 1 . W tej gonitwie także programiści nie mogą pozostawać w tyle – w przeciwnym wypadku przystosowanie ich starych aplikacji do nowych warunków technologicznych może kosztować mnóstwo czasu i wysiłku.

Jednocześnie wiele programów opiera swe działanie na rozmiarze typów podstawowych. Wystarczy napomknąć o tak częstej czynności, jak zapisywanie danych do plików albo przesyłanie ich poprzez sieć. Jeśliby każdy program musiał mieć wpisane „na sztywno” rzeczone wielkości, wtedy spora część pracy programistów upływałaby na dostosowywaniu ich do potrzeb nowych platform sprzętowych, na których miałyby działać istniejące aplikacje. A co z tworzeniem całkiem nowych produktów?…

Szczęśliwie twórcy C++ byli na tyle zapobiegliwi, żeby uchronić nas, koderów, od tej koszmarnej perspektywy. Wprowadzili bowiem operator sizeof (‘rozmiar czegoś'), który pozwala na uzyskanie wielkości zmiennej (lub jej typu) w trakcie działania programu.

Spojrzenie na poniższy przykład powinno nam przybliżyć funkcjonowanie tego operatora:

// Sizeof - pobranie rozmiaru zmiennej lub typu
#include <iostream>
#include <conio.h>

void main()
{
std::cout << "Typy liczb calkowitych:" << std::endl;
std::cout << "- int: " << sizeof ( int ) << std::endl;
std::cout << "- short int: " << sizeof ( short int ) << std::endl;
std::cout << "- long int: " << sizeof ( long int ) << std::endl;
std::cout << "- char: " << sizeof ( char ) << std::endl;
std::cout << std::endl;
std::cout << "Typy liczb zmiennoprzecinkowych:" << std::endl;
std::cout << "- float: " << sizeof ( float ) << std::endl;
std::cout << "- double: " << sizeof ( double ) << std::endl;
getch();
}

Uruchomienie programu z listingu powyżej, jak słusznie można przypuszczać, będzie nam skutkowało krótkim zestawieniem rozmiarów typów podstawowych.


Screen 20. sizeof w akcji

Po uważnym zlustrowaniu kodu źródłowego widać jak na dłoni działanie oraz sposób użycia operatora sizeof . Wystarczy podać mu typ lub zmienną jako parametr, by otrzymać w wyniku jego rozmiar w bajtach 1 . Potem możemy zrobić z tym rezultatem dokładnie to samo, co z każdą inną liczbą całkowitą – chociażby wyświetlić ją w konsoli przy użyciu strumienia wyjścia.

Zastosowanie sizeof nie ogranicza się li tylko do typów wbudowanych. Gdy w kolejnych rozdziałach nauczymy się tworzyć własne typy zmiennych, będziemy mogli w identyczny sposób ustalać ich rozmiary przy pomocy poznanego przed momentem operatora. Nie da się ukryć, że bardzo lubimy takie uniwersalne rozwiązania :D

Wartość, którą zwraca operator sizeof , należy do specjalnego typu size_t . Zazwyczaj jest on tożsamy z unsigned int , czyli liczbą bez znaku (bo przecież rozmiar nie może być ujemny). Należy więc uważać, aby nie przypisywać jej do zmiennej, która jest liczbą ze znakiem.

Rzutowanie

Idea typów zmiennych wprowadza nam pewien sposób klasyfikacji wartości. Niektóre z nich uznajemy bowiem za liczby całkowite ( 3 , - 17 , 44 , 67 * 88 itd.), inne za zmiennoprzecinkowe ( 7.189 , 12.56 , - 1.41 , 8.0 itd.), jeszcze inne za tekst ( "ABC" , "Hello world!" itp.) czy pojedyncze znaki 1 ( 'F' , '@' itd.).

Każdy z tych rodzajów odpowiada nam któremuś z poznanych typów zmiennych. Najczęściej też nie są one ze sobą kompatybilne – innymi słowy, „nie pasują” do siebie, jak chociażby tutaj:

int nX = 14 ;
int nY = 0.333 * nX;

Wynikiem działania w drugiej linijce będzie przecież liczba rzeczywista z częścią ułamkową, którą nijak nie można wpasować w ciasne ramy typu int , zezwalającego jedynie na wartości całkowite 2 .

Oczywiście, w podanym przykładzie wystarczy zmienić typ drugiej zmiennej na float , by rozwiązać nurtujący nas problem. Nie zawsze jednak będziemy mogli pozwolić sobie na podobne kompromisy, gdyż często jedynym wyjściem stanie się „wymuszenie” na kompilatorze zaakceptowania kłopotliwego kodu.

Aby to uczynić, musimy rzutować (ang. cast ) przypisywaną wartość na docelowy typ – na przykład int . Rzutowanie działa trochę na zasadzie umowy z kompilatorem, która w naszym przypadku mogłaby brzmieć tak: „Wiem, że naprawdę jest to liczba zmiennoprzecinkowa, ale właśnie tutaj chcę, aby stała się liczbą całkowitą typu int , bo muszę ją przypisać do zmiennej tego typu”. Takie porozumienie wymaga ustępstw od obu stron – kompilator musi „pogodzić się” z chwilowym zaprzestaniem kontroli typów, a programista powinien liczyć się z ewentualną utratą części danych (w naszym przykładzie poświęcimy cyfry po przecinku).

Proste rzutowanie

Zatem do dzieła! Zobaczmy, jak w praktyce wyglądają takie „negocjacje” :) Zostawimy na razie ten trywialny, dwulinijkowy przykład (wrócimy jeszcze do niego) i zajmiemy się poważniejszym programem. Oto i on:

// SimpleCast - proste rzutowanie typów
void main()
{
for ( int i = 32 ; i < 256 ; i += 4 )
{
std::cout << "| " << ( char ) (i) << " == " << i << " | " ;
std::cout << ( char ) (i + 1 ) << " == " << i + 1 << " | " ;
std::cout << ( char ) (i + 2 ) << " == " << i + 2 << " | " ;
std::cout << ( char ) (i + 3 ) << " == " << i + 3 << " |" ;
std::cout << std::endl;
}
getch();
}

Huh, faktycznie nie jest to banalny kod :) Wykonywana przezeń czynność jest jednak dość prosta. Aplikacja ta pokazuje nam tablicę kolejnych znaków wraz z odpowiadającymi im kodami ANSI.


Screen 21. Fragment tabeli ANSI

Najważniejsza jest tu dla nas sama operacja rzutowania, ale warto przyjrzeć się funkcjonowaniu programu jako całości.

Zawarta w nim pętla for wykonuje się dla co czwartej wartości licznika z przedziału od 32 do 255 . Skutkuje to faktem, iż znaki są wyświetlane wierszami, po 4 w każdym.

Pomijamy znaki o kodach mniejszych od 32 (czyli te z zakresu 0…31), ponieważ są to specjalne symbole sterujące, zasadniczo nieprzeznaczone do wyświetlania na ekranie. Znajdziemy wśród nich na przykład tabulator (kod 9), znak „powrotu karetki” (kod 13), końca wiersza (kod 10) czy sygnał błędu (kod 7).

Za prezentację pojedynczego wiersza odpowiadają te wielce interesujące instrukcje:

std::cout << "| " << ( char ) (i) << " == " << i << " | " ;
std::cout << ( char ) (i + 1 ) << " == " << i + 1 << " | " ;
std::cout << ( char ) (i + 2 ) << " == " << i + 2 << " | " ;
std::cout << ( char ) (i + 3 ) << " == " << i + 3 << " |" ;

Sądząc po widocznym ich efekcie, każda z nich wyświetla nam jeden znak oraz odpowiadający mu kod ANSI. Przyglądając się bliżej temu listingowi, widzimy, że zarówno pokazanie znaku, jak i przynależnej mu wartości liczbowej odbywa się zawsze przy pomocy tego samego wyrażenia. Jest nim odpowiednio i , i + 1 , i + 2 lub i + 3 .

Jak to się dzieje, że raz jest ono interpretowane jako znak , a innym razem jako liczba ? Domyślasz się zapewne niebagatelnej roli rzutowania w działaniu tej „magii” :) Istotnie, jest ono konieczne. Jako że licznik i jest zmienną typu int , zacytowane wyżej cztery wyrażenia także należą do tego typu. Przesłanie ich do strumienia wyjścia w niezmienionej postaci powoduje wyświetlenie ich wartości w formie liczb. W ten sposób pokazujemy kody ANSI kolejnych znaków.

Aby wyświetlić same symbole musimy jednak oszukać nieco nasz strumień std::cout , rzutując wspomniane wartości liczbowe na typ char . Dzięki temu zostaną one potraktowane jako znaki i takoż wyświetlone w konsoli.

Zobaczmy, w jaki sposób realizujemy tutaj to osławione rzutowanie. Spójrzmy mianowicie na jeden z czterech podobnych kawałków kodu:

( char ) (i + 1 )

Ten niepozorny fragment wykonuje całą ważką operację, którą nazywamy rzutowaniem. Zapisanie w nawiasach nazwy typu char przed wyrażeniem i + 1 (dla jasności umieszczonym również w nawiasach) powoduje bowiem, iż wynik tak ujętego działania zostaje uznany jako podpadający pod typ char . Tak jest też traktowany przez strumień wyjścia, dzięki czemu możemy go oglądać jako znak, a nie liczbę.

Zatem, aby rzutować jakieś wyrażenie na wybrany typ, musimy użyć niezwykle prostej konstrukcji:

( typ ) wyrażenie

wyrażenie może być przy tym ujęte w nawias lub nie; zazwyczaj jednak stosuje się nawiasy, by uniknąć potencjalnych kłopotów z kolejnością operatorów.

Można także użyć składni typ ( wyrażenie ) . Stosuje się ją rzadziej, gdyż przypomina wywołanie funkcji i może być przez to przyczyną pomyłek.

Wróćmy teraz do naszego pierwotnego przykładu. Rozwiązanie problemu, który wcześniej przedstawiał, powinno być już banalne:

int nX = 14 ;
int nY = ( int ) ( 0.333 * nX);

Po takich manipulacjach zmienna nY będzie przechowywała część całkowitą z wyniku podanego mnożenia. Oczywiście tracimy w ten sposób dokładność obliczeń, co jest jednak nieuniknioną ceną kompromisu towarzyszącego rzutowaniu :)

Operator static_cast

Umiemy już dokonywać rzutowania, poprzedzając wyrażenie nazwą typu napisaną w nawiasach. Taki sposób postępowania wywodzi się jeszcze z zamierzchłych czasów języka C 1 , poprzednika C++. Czyżby miało to znaczyć, że jest on zły?…

Powiedzmy, że nie jest wystarczająco dobry :) Nie przeczę, że na początku może wydawać się świetnym rozwiązaniem – klarownym, prostym, niewymagającym wiele pisania etc. Jednak im dalej w las, tym więcej śmieci: już teraz dokładniejsze spojrzenie ujawnia nam wiele mankamentów, a w miarę zwiększania się twoich umiejętności i wiedzy dostrzeżesz ich jeszcze więcej.

Spójrzmy choćby na samą składnię. Oprócz swojej niewątpliwej prostoty posiada dwie zdecydowanie nieprzyjemne cechy.

Po pierwsze, zwiększa nam ilość nawiasów w wyrażeniach, które zawierają rzutowanie. A przecież nawet i bez niego potrafią one być dostatecznie skomplikowane. Częste przecież użycie kilku operatorów, kilku funkcji (z których każda ma pewnie po kilka parametrów) oraz kilku dodatkowych nawiasów (aby nie kłopotać się kolejnością działań) gmatwa nasze wyrażenia w dostatecznym już stopniu. Jeżeli dodamy do tego jeszcze parę rzutowań, może nam wyjść coś w tym rodzaju:

int nX = ( int ) ((( 2 * nY) / ( float ) (nZ + 3 )) – ( int ) Funkcja(nY * 7 ));

Konwersje w formie ( typ ) wyrażenie z pewnością nie poprawiają tu czytelności kodu.

Drugim problemem jest znowuż kolejność działań. Pytanie za pięć punktów: jaką wartość ma zmienna nY w poniższym fragmencie?

float fX = 0.75 ;
int nY = ( int ) fX * 3 ;

Zatem?… Jeżeli obecne w drugiej linijce rzutowanie na int dotyczy jedynie zmiennej fX , to jej wartość ( 0.75 ) zostanie zaokrąglona do zera, zatem nY będzie przypisane również zero. Jeśli jednak konwersji na int zostanie poddane całe wyrażenie ( 0.75 * 3 , czyli 2.25 ), to nY przyjmie wartość 2 !

Wybrnięcie z tego dylematu to… kolejna para nawiasów, obejmująca tą część wyrażenia, którą faktycznie chcemy rzutować. Wygląda więc na to, że nie opędzimy się od częstego stosowania znaków ( i ) .

Składnia to jednak nie jedyny kłopot. Tak naprawdę o wiele ważniejsze są kwestie związane ze sposobem, w jaki jest realizowane samo rzutowanie. Niestety, na razie jesteś w niezbyt komfortowej sytuacji, gdyż musisz zaakceptować pewien fakt bez uzasadnienia („na wiarę” :D). Brzmi on następująco:

Rzutowanie w formie ( typ ) wyrażenie , zwane też rzutowaniem w stylu C, nie jest zalecane do stosowania w C++.

Dokładnie przyczyny takiego stanu rzeczy poznasz przy okazji omawiania klas i programowania obiektowego 2 .

No dobrze, załóżmy, że uznajemy tą odgórną radę 3 i zobowiązujemy się nie stosować rzutowania „nawiasowego” w swoich programach. Czy to znaczy, że w ogóle tracimy możliwość konwersji zmiennych jednego typu na inne?!

Rzeczywistość na szczęście nie jest aż tak straszna :) C++ posiada bowiem aż cztery operatory rzutowania , które są najlepszym sposobem na realizację zamiany typów w tym języku. Będziemy sukcesywnie poznawać je wszystkie, a zaczniemy od najczęściej stosowanego – tytułowego static_cast .

static_cast (‘rzutowanie statyczne') nie ma nic wspólnego z modyfikatorem static i zmiennymi statycznymi. Operator ten służy do przeprowadzania najbardziej pospolitych konwersji, które jednak są spotykane najczęściej. Możemy go stosować wszędzie, gdzie sposób zamiany jest oczywisty – zarówno dla nas, jak i kompilatora ;)

Najlepiej po prostu zawsze używać static_cast , uciekając się do innych środków, gdy ten zawodzi i nie jest akceptowany przez kompilator (albo wiąże się z pokazaniem ostrzeżenia).

W szczególności, możemy i powinniśmy korzystać ze static_cast przy rzutowaniu między typami podstawowymi. Zobaczmy zresztą, jak wyglądałoby ono dla naszego ostatniego przykładu:

float fX = 0.75 ;
int nY = static_cast < int >(fX * 3) ;

Widzimy, że użycie tego operatora od razu likwiduje nam niejednoznaczność, na którą poprzednio zwróciliśmy uwagę. Wyrażenie poddawane rzutowaniu musimy bowiem ująć w nawiasy okrągłe.

Ciekawy jest sposób zapisu nazwy typu, na który rzutujemy. Znaki < i > , oprócz tego że są operatorami mniejszości i większości, tworzą parę nawiasów ostrych. Pomiędzy nimi wpisujemy określenie docelowego typu.

Pełna składnia operatora static_cast wygląda więc następująco:

static_cast < typ >( wyrażenie )

Być może jest ona bardziej skomplikowana od „zwykłego” rzutowania, ale używając jej osiągamy wiele korzyści, o których mogłeś się naocznie przekonać :)

Warto też wspomnieć, że trzy pozostałe operatory rzutowania mają identyczną postać – oczywiście z wyjątkiem słowa static_cast , które jest zastąpione innym.

***

Tą uwagą kończymy omawianie różnych aspektów związanych z typami zmiennych w języku C++. Wreszcie zajmiemy się tytułowymi zagadnieniami tego rozdziału, czyli czynnościach, które możemy wykonywać na zmiennych.

1 W chwili pisania tych słów – pod koniec roku 2003 – mamy już coraz wyraźniejsze widoki na poważne wykorzystanie procesorów 64-bitowych w domowych komputerach. Jednym ze skutków tego „zwiększenia bitowości” będzie zmiana rozmiaru typu liczbowego int .

1 Zainteresowanych odsyłam do Dodatku B.

1 Można wykorzystać chociażby szablony, unie czy wskaźniki. O każdym z tych elementów C++ powiemy sobie w dalszej części kursu, więc cierpliwości ;)

2 To oczywiście jedynie przykład. Na żadnym współczesnym systemie typ int nie ma tak małego zakresu.

3 Co nie jest wcale niemożliwe, a przy stosowaniu tablic (opisanych w następnym rozdziale) staje całkiem częste.

1 1 bajt to 8 bitów.

2 To zastrzeżenie jest konieczne. Wprawdzie int zajmuje 4 bajty we wszystkich 32-bitowych kompilatorach, ale w przypadku pozostałych typów może być inaczej! Standard C++ wymaga jedynie, aby short int był mniejszy lub równy od int -a, a long int większy lub równy int -owi.

1 Ściślej mówiąc, sizeof podaje nam rozmiar obiektu w stosunku do wielkości typu char . Jednakże typ ten ma najczęściej wielkość dokładnie 1 bajta, zatem utarło się stwierdzenie, iż sizeof zwraca w wyniku ilość bajtów. Nie ma w zasadzie żadnego powodu, by uznać to za błąd.

1 Znaki są typu char , który jak wiemy jest także typem liczbowym. W C++ kod znaku jest po prostu jednoznaczny z nim samym, dlatego możemy go interpretować zarówno jako symbol, jak i wartość liczbową.

2 Niektóre kompilatory (w tym i Visual C++) zaakceptują powyższy kod, jednakże nie obejdzie się bez ostrzeżeń o możliwej (i faktycznej!) utracie danych. Wprawdzie niektórzy nie przejmują się w ogóle takimi ostrzeżeniami, my jednak nie będziemy tak krótkowzroczni :D

| | | |
Copyright © 2006-2013 egrafik.pl | Kontakt | Reklama | Projekty domów
jocker