Przekształcenia typów w języku C++. Awansowanie, rzutowanie.

Wprowadzenie

Zagadnienie, które Ci przedstawię w tej lekcji jest bardzo ważne. Tak naprawdę we wszystkich dotychczasowych programach musiałem starać się unikać sytuacji, w których nie następowałyby niezrozumiałe na pierwszy rzut oka przekształcenia.

Na szczęście, od następnej lekcji nie będę musiał już tego robić, bowiem mam nadzieję, że tutaj uda Ci się zrozumieć jak działają przekształcenia typów.

Problem

Przede wszystkim, aby uświadomić Ci jak ważne zagadnienie będziemy omawiać, przedstawiam poniżej program, który zawiera w sobie pułapkę i nie zadziała zgodnie z Twoimi początkowymi oczekiwaniami:

#include <iostream>

using namespace std;

int main()
{
  int a=2, b=3, wynik1;
  float wynik2;
 
  cout <<"Iloraz liczb wynosi "<< 2/3 <<endl; // (1)  
 
  cout <<"Iloraz liczb wynosi "<< a/b <<endl; // (2)
 
  wynik1=a/b;  // (3)
  cout <<"Iloraz liczb wynosi "<< wynik1 <<endl;
 
  wynik2=a/b; // (4)
  cout <<"Iloraz liczb wynosi "<< wynik2 <<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.1

Jak widać, celem naszego programu jest wypisanie ilorazu dwóch liczb, w tym przypadku konkretnie liczb 2 i 3. Niestety okazuje się, że nie jest to takie proste jak mogłoby się wydawać.

W metodzie pierwszej (oznaczonej komentarzem (1)) usiłujemy wypisać iloraz dwóch liczb bez korzystania ze zmiennych - niestety uzyskujemy wynik 0, który jest oczywiście dla nas niepoprawny.

Stwierdzamy - no dobrze, może lepiej skorzystać ze zmiennych (metoda 2), jednak znowu to samo - problem jak był, tak jest nadal.

Nawet jeśli przypisujemy wynik ilorazu do zmiennej typu int (metoda 3), okazuje się, że nic się nie zmieniło. Wynik jest niestety nadal niepoprawny.

W końcu wpadamy na wręcz genialny pomysł - skoro iloraz dwóch liczb całkowitych nie jest liczbą całkowitą, to przypiszemy wynik ilorazu do zmiennej typu zmiennopozycyjnego, w tym przypadku float (mogłoby być oczywiście również double lub long double). Jednak jak widać w metodzie czwartej, po raz kolejny nie uzyskujemy pożądanych efektów.

Jak zatem widzisz - program i zadanie do zrealizowania bardzo proste, bowiem chcemy po prostu wypisać iloraz dwóch liczb. Jak się jednak okazuje, zadanie to jest jak na razie dla nas za trudne.

Po tej lekcji takich problemów mieć już nie będziesz. Właśnie dlatego ta lekcja będzie tak ważna i lepiej skoncentruj się tak bardzo, jak to tylko możliwe.

Proste przekształcenie typu - pojedyncza zmienna

Podstawową regułą dotyczącą zmiennych jest to, że każda zmienna może przechowywać wartości tylko takiego typu, jakiego została zadeklarowana. Co jednak dzieje się jeśli do zmiennej z jakiegoś powodu przypiszemy zmienną lub literał innego typu? Wtedy nastąpi tzw. niejawne przekształcenie typu.

Niejawne przekształcenie typu następuje kiedy do zmiennej przypisujemy zmienną lub literał innego typu. Ponieważ zmienna nie może przechowywać innego typu, musi nastąpić przekształcenie wartości przypisanej do zmiennej do typu zmiennej tak, aby przechowywanie wartości przez zmienną było w ogóle możliwe.

Poniższy program przedstawia co się dzieje, jeśli do zmiennych przypisujemy wartości innych i tych samych typów. Program powinien się kompilować bez problemów w każdym kompilatorze, chociaż powinny się pojawić 2 ostrzeżenia. Oto program:

#include <iostream>

using namespace std;

int main()
{
  int a;
  int b=3;
  double c=3.657;
  double d;
 
  a=2; // literal typu signed int
  cout <<"Zmienna a ma wartosc "<<a<<endl;

  a=b; // zmienna typu int
  cout <<"Zmienna a ma wartosc "<<a<<endl;
 
  a=6.54345; // literal typu double
  cout <<"Zmienna a ma wartosc "<<a<<endl;
 
  a=c; // zmienna typu double
  cout <<"Zmienna a ma wartosc "<<a<<endl;
 
  d=15.433443; // literal typu double
  cout <<"Zmienna d ma wartosc "<<d<<endl;

  d=c; // zmienna typu double
  cout <<"Zmienna d ma wartosc "<<d<<endl;    
         
  d=65; // literal typu signed int
  cout <<"Zmienna d ma wartosc "<<d<<endl;

  d=a; // zmienna typu int
  cout <<"Zmienna d ma wartosc "<<d<<endl;    

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.2

Jak widzisz, w programie dzieje się kilka różnych rzeczy. Przede wszystkim w niektórych miejscach przypisujemy zmiennej typu int, literał typu int i zmienną typu int, a następnie dla zmiennej typu double przypisujemy literał typu double i zmienną typu double. W tych przypadkach nie ma oczywiście mowy o żadnych przekształceniach, bowiem typy znajdujący się po lewej stronie przypisania i po prawej są identyczne.

Inaczej sytuacja ma się w momencie, kiedy do zmiennej typu int przypisujemy literał lub wartość zmiennej typu double. Ponieważ typ double jest typem bardziej dokładnym od typu int (oprócz części całkowitej zawiera również część ułamkową), to oczywiście nie jest możliwe, aby zmienna typu int mogła przechować wartość typu double. W tym momencie następuje zatem przekształcenie typu double do typu int. Jak łatwo się przekonać, przekształcenie takie polega po prostu na odrzuceniu części ułamkowej ze zmiennej (lub literału) typu double, otrzymując w ten sposób tylko typ całkowity.

Mimo że takie działanie jest poprawne, to jednak jest dość niebezpieczne, bowiem w rezultacie otrzymujemy wartość mniej dokładną, co w przypadku poważnych programów, mogłoby być tragedią. Mimo że każdy kompilator taki kod powinien skompilować, wysyła on jednak najczęściej ostrzeżenie postaci [WARNING] converting to 'int' from 'double', ale program jest kompilowany i można go oczywiście uruchomić. Jeśli osoba pisząca program zauważy takie ostrzeżenie, dobrą praktyką jest jego likwidacja - o tym jak jak zlikwidować takie ostrzeżenie, dowiesz się jeszcze w tej lekcji.

Jeszcze inna sytuacja pojawia się, kiedy do zmiennej typu double przypisujemy literał lub wartość zmiennej typu int. Mamy tutaj sytuację odwrotną. Ponieważ typ int jest mniej dokładny od typu double, to nie utracimy żadnej dokładności podczas dokonywania przekształcenia z typu int do typu double. W tym przypadku kompilator ostrzeżenia już nie wysyła, bo nie może się tutaj nic groźnego stać.

Zmienne tego samego typu

Kolejną sprawą jest uzmysłowienie sobie, jak przebiegają operacje arytmetyczne dla zmiennych lub literałów tego samego typu. Taki problem pojawił się właśnie w pierwszym zademonstrowanym programie, na przykład w tym miejscu:

cout <<"Iloraz liczb wynosi "<< 2/3 <<endl;

Okazuje się, że w języku C++ w przypadku, gdy dokonujemy operacji na zmiennych (literałach) tego samego typu, to wynik operacji jest zawsze tego samego typu co typ obu zmiennych (literałów).

Taka sytuacja ma właśnie miejsce w powyższym fragmencie kodu. Literał 2 jest oczywiście typu int. Podobnie, literał 3 jest również typu int. Skoro tak, to znaczy, że literały są tego samego typu (int), a zatem wynik przeprowadzanej operacji (tutaj akurat dzielenia) będzie również typu int.

Pozostaje teraz odpowiedzieć na pytanie, czemu zwracaną wartością jest 0. Jak wiadomo iloraz dzielenia 2 przez 3 wynosi 0.66666. Ponieważ jednak wynik musi być zgodnie z powyższą regułą typu int, to odrzucana jest część ułamkowa, a pozostaje tylko i wyłącznie część całkowita, czyli w tym przypadku akurat 0.

Gdybyśmy natomiast usiłowali wypisać iloraz liczb na przykład 7 i 2, to rezultatem takiego fragmentu programu

cout <<"Iloraz liczb wynosi "<< 7/2 <<endl;

byłaby wartość 3, bowiem część całkowita z dzielenia liczb 7 przez 2 to właśnie 3.

Zwróć uwagę, że w najbardziej korzystnym przypadku, wynik będzie zgodny z naszymi oczekiwaniami. Przykładowo, gdybyśmy usiłowali wypisać iloraz liczb 12 i 3, otrzymalibyśmy wartość 4, co jest zgodne z naszymi oczekiwaniami, bowiem nie pojawia się tutaj część ułamkowa.

Zauważ też, że w takiej sytuacji, kompilator żadnych ostrzeżeń nie wysyła i stosunkowo łatwo zapomnieć o tym problemie, zwłaszcza gdybyśmy przez dłuższy czas testowali program dla liczb, których iloraz jest zawsze liczbą całkowitą (wspomniane 12 i 3), a nagle zmienilibyśmy te liczby na przykładowo 13 i 3. Jak zatem widzisz, w przypadku dokonywania niektórych operacji na zmiennych lub literałach tego samego typu, trzeba być bardzo ostrożnym.

Jak pamiętasz, faktu wypisywania tylko części całkowitej z dzielenia w pierwszym programie, nie zmieniało nawet przypisanie rezultatu do zmiennej typu int (to jest już mam nadzieję oczywiste), ale również do typu zmiennopozycyjnego - float.

Inaczej mówiąc, taki fragment kodu:

float wynik=2/3;

nie sprawi, że wartość zmiennej wynik będzie wynosiła 0.66666. Dzieje się tak dlatego, że działa tutaj nadal zasada, że jeśli zmienne lub literały są tego samego typu, to wynik działania jest również typu int.

Ponieważ zarówno 2 i 3 to literały całkowite typu signed int, to rezultat działania jest również typu signed int i iloraz liczb 2 i 3 wyniesie 0. Dopiero po obliczeniu ilorazu zostaje stwierdzone, że zmiennej typu float usiłujemy przypisać wartość signed int i ponieważ oba typy nie są zgodne, to wartość signed int zostaje przekształcona do typu float. Ponieważ jednak wartość wyniosła 0, to oczywiście po przekształceniu do typu float, wartość będzie nadal wynosiła 0.

Mam nadzieję, że teraz jest już dla Ciebie jasne jak to działa i dlaczego w pierwszym programie nie udało się uzyskać ilorazu liczb. Najwyższa pora, aby poznać rozwiązanie.

Awansowanie

W ostatnim paragrafie analizowaliśmy co się dzieje, kiedy zmienne lub literały są tego samego typu. Czas zastanowić się, jak wygląda sytuacja, gdy zmienne są różnych typów.

Okazuje się, że w przypadku gdy zmienne lub literały są różnych typów, stosowane jest awansowanie, czyli przekształcenie do typu bardziej dokładnego.

Generalnie można powiedzieć, że jeśli mamy dwie zmienne różnych typów, to typ jednej ze zmiennych jest bardziej dokładny od typu drugiej zmiennej i wówczas, druga zmienna jest awansowana do typu pierwszej zmiennej. Powoduje to z kolei to, że zmienne są traktowane tak, jakby były tego samego typu i działa zasada, że wynik operacji na takich zmiennych będzie typu obu zmiennych (czyli w rzeczywistości typem, który z dwóch typów był bardziej dokładny).

Mając taką podstawową wiedzę, wprowadźmy kilka zmian w początkowym programie i zobaczmy, jak zmienią się rezultaty jego działania:

#include <iostream>

using namespace std;

int main()
{
  long double a=2;
  int b=3, wynik1;
  float wynik2;
 
  cout <<"Iloraz liczb wynosi "<< 2.0/3 <<endl; // (1)  
 
  cout <<"Iloraz liczb wynosi "<< a/b <<endl; // (2)
 
  wynik1=a/b;  // (3)
  cout <<"Iloraz liczb wynosi "<< wynik1 <<endl;
 
  wynik2=a/b; // (4)
  cout <<"Iloraz liczb wynosi "<< wynik2 <<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.3

Zwróć przede wszystkim uwagę na to, że rezultaty działania programu są inne od rezultatów działania pierwszego programu. Tylko w jednym z przypadków otrzymujemy ponownie wartość 0, ale tym razem już tylko na nasze własne życzenie.

W miejscu opatrzonym komentarzem (1) dokonujemy operacji dzielenia na dwóch literałach - tym razem są one różnych typów. Pierwszy literał jest typu double (jeśli tego nie pamiętasz, przeczytaj koniecznie poprzednią lekcję), a drugi literał jest typu signed int. Typy są różne, zatem jeden z typów musi zostać przekształcony. Ponieważ mniej dokładny jest typ int, to literał typu signed int zostanie przekształcony do typu double. Wykonamy zatem operację dzielenia na dwóch literałach tego samego typu (typu double), zatem wynik będzie również typu double. Tak właśnie się dzieje - w rezultacie otrzymujemy tym razem poprawny wynik działania.

W miejscu opatrzonym komentarzem (2) sytuacja jest podobna z tym tylko, że tym razem operujemy na zmiennych. Pierwsza zmienna jest typu long double, a druga typu signed int, zatem w rezultacie wartość zmiennej typu signed int zostanie podczas dokonywania obliczeń przekształcona do typu long double i cały rezultat operacji będzie również typu long double. Na ekranie ujrzymy oczekiwany przez nas wynik.

Nieco inaczej wygląda sytuacja w miejscu opatrzonym komentarzem (3). Co prawda, początkowo sytuacja wygląda tak jak w przypadku (2), czyli rezultat działania będzie typu long double, jednak później musimy przypisać tą wartość do zmiennej wynik1, która jest typu int. Zostanie zatem zastosowana reguła o odrzuceniu części ułamkowej i zmienna wynik1 będzie miała wartość 0. Kompilator powinien Cię ostrzec po raz kolejny przed takim działaniem, wypisując komunikat [WARNING] converting to 'int' from 'long double', co powinno Cię zmusić do zastanowienia się, czy właśnie tak miało być.

Z kolei w przypadku (4), rezultat dzielenia zmiennej jest również oczywiście typu long double. Wynik działania przypisujemy do zmiennej float. Oczywiście typ float jest typem pozwalającym przechowywać mniejsze liczby od typu long double, co może spowodować, że zwrócony wynik będzie niepoprawny dla dużych wartości. Jednak w naszym przypadku nie musimy się tego obawiać, bowiem przechowujemy bardzo małą wartość. Zwróć ponadto uwagę na to, że Twój kompilator dla tego przekształcenia nie wypisze najprawdopodobniej żadnego ostrzeżenia, co niestety może utrudnić wykrycie potencjalnych błędów.

Mimo że przedstawiłem Ci już pokrótce, w jaki sposób działa awansowanie typów, czas teraz na nieco mniej przyjemną część, a mianowicie bardziej formalne zapisanie wszystkich tych reguł.

Zasady dotyczące awansowania typów:

Mimo że te wszystkie reguły mogą Ci się wydawać trudne i nie do końca zrozumiałe, to muszę Cię pocieszyć, że nie wszystkie je trzeba zazwyczaj pamiętać. Najważniejsze jest to, żeby wiedzieć, że typy są zawsze awansowane do typu najbardziej ogólnego oraz to, że typem najmniej dokładnym, który możemy tak naprawdę uzyskać w wyniku operacji na kilku zmiennych jest typ signed int (z powodu wcześniejszego awansowania typów mniej dokładnych od signed int).

Poniżej znajduje się program, który obrazuje jak zachodzą niektóre z awansowań typów w programie. Program działa na literałach, chociaż rezultaty byłyby oczywiście identyczne jak w przypadku zmiennych.

Zwróć uwagę, że dołączony został dodatkowy plik nagłówkowy typeinfo oraz zastosowane dość "dziwne" wywołania, a mianowicie są to typeid(wyrazenie).name(). Kod powinien działać bez problemu, gdyby jednak tak nie było usuń po prostu te fragmenty z kodu programu.

Jeśli chodzi o tajemnicze wywołania, to nie będę Ci ich dokładnie wyjaśniał, bowiem stosuje się je raczej bardzo rzadko i to tylko do takiej prezentacji jak ta tutaj. Chodzi o to, że dzięki zastosowaniu takiego wywołania, otrzymujemy symboliczną nazwę typu, czyli możesz się przekonać, że rzeczywiście awansowania działają tak jak właśnie mają działać.

Żeby zrozumieć to co będzie wypisane na ekranie, musisz wiedzieć że każda z poniższych liter będzie oznaczała pewien typ (nie ucz się tego na pamięć - po prostu skorzystaj z poniższych danych po uruchomieniu programu):

i - typ int
d - typ double
b - typ bool
f - typ float
j - typ unsigned int
m - typ unsigned long int

Poniżej znajduje się wspomniany program, który demonstruje jak przeprowadzane są awansowania:

#include <iostream>
#include <typeinfo>

using namespace std;

int main()
{
  cout << typeid(2+3).name() <<' '<<2+3<<endl; // int + int = int
 
  /* double + int = double + double = double*/
  cout << typeid(2.1+3).name() <<' '<<2.1+3<<endl;
 
  cout <<typeid(true).name() <<' '<<true<<endl; // bool
 
  /* bool + int = int + int = int */
  cout <<typeid(true+3).name() <<' '<<true+3<<endl;
 
  /* double + float = double + double = double */
  cout <<typeid(2.3+3.8F).name()<<' '<<2.3+3.8F<<endl;

  /* float + char = float + int = float + float = float */
  cout <<typeid(2.1F+'a').name()<<' '<<2.1F+'a'<<endl; // 'a' to w ASCII 97
 
  /* unsigned int + int = unsigned int + unsigned int = unsigned int*/
  cout <<typeid(2U + 3).name()<<' '<< 2U + 3<<endl;
 
  /* unsigned int + long int = ? (albo long int albo unsigned long int) */
  cout <<typeid(2U + 3L).name()<<' '<< 2U + 3L<<endl;    

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.4

Sam kod został opatrzony komentarzami, więc nie trzeba go prawie tłumaczyć. Skupię się zatem tylko na najważniejszych kwestiach.

Zwróć uwagę, co się dzieje kiedy dodajemy zmienną typu char (konkretnie literę 'a') do zmiennej typu float. Okazuje się, że litera jako zmienna typu char ma pewną wartość przy awansowaniu jej do typu int. Musisz wiedzieć, że jest tak dla każdego znaku - każdy znak jest reprezentowany za pomocą pewnej wartości liczbowej w tzw. kodzie ASCII. Akurat wartością litery 'a' w tym kodzie jest 97.

Zauważ też, co się dzieje, kiedy przeprowadzamy operację (tutaj dodawanie) dla zmiennych, z których jedna jest typu unsigned int, a druga typu long int. Zgodnie z regułami dotyczącymi awansowania, rezultat może być albo typu long int albo typu unsigned long int. Na moim komputerze był to akurat typ unsigned long int.

Pozostała część programu dzięki komentarzom powinna być dla Ciebie zrozumiała. Przejdźmy zatem do zdobywania dalszej wiedzy.

Rzutowanie

Jeśli dobrze przeanalizujesz to, co Ci przedstawiłem w tej lekcji, uświadomisz sobie, że wszystkie przekształcenia typów jakie były do tej pory wykonywane, były wykonywane automatycznie przez kompilator. Tobie pozostawało zrozumieć jak to się odbywa i ewentualnie jeśli coś Ci się nie podobało, zmienić typ jednej lub kilku zmiennych.

Oczywiście automatyczne przekształcenia typów (zwane również niejawnymi) wykonywane przez kompilator są dla osoby piszącej programy bardzo wygodne. Nie zawsze bowiem typy zmiennych zadeklarowanych w programie są z takich czy innych powodów identyczne i gdyby nie niejawne przekształcenia typów, należałoby poświęcić znacznie więcej wysiłku, żeby poinformować kompilator, w jaki sposób ma dokonać przekształcenia.

Z drugiej jednak strony, niejawne konwersje mogą być bardzo niebezpieczne, jeśli osoba programująca nie jest ich świadoma lub ich nie rozumie. Dlatego też niekiedy znacznie rozsądniej jest posłużyć się rzutowaniem.

Rzutowanie to inaczej jawne przekształcenie typu. Zauważ, że ponieważ przekształcenie ma być jawne, to znaczy, że tym razem, to osoba pisząca program określa jakie przekształcenie ma zostać wykonane.

W tej lekcji przedstawię Ci jedynie proste rzutowania, bowiem do zrozumienia do czego mogą się przydać rzutowania bardziej zaawansowane, potrzebna jest zdecydowanie większa wiedza.

Ponadto chcę Ci uświadomić, że mimo że rzutowania mogą być w niektórych sytuacjach przydatne, to jednak powinny być również używane świadomie i rozważnie, bowiem stosując rzutowania, pomijane jest badanie zgodności typów i jeśli użyjemy pewnego "bardzo dziwnego" rzutowania, możemy sobie tylko utrudnić życie.

Rzutowanie w starym stylu

Mimo że kurs dotyczy języka C++, warto pamiętać, że sam język C++ jest następcą języka C. Dlatego też pewne elementy, które istniały w języku C, zostały przeniesione do języka C++. Tak właśnie jest ze sposobem rzutowania pochodzącym z języka C, które mimo że jest nadal poprawne w języku C++, nie jest jednak zalecane.

Schematycznie operację rzutowania możemy zapisać następująco:

typ (wyrazenie);

lub

(typ) wyrazenie;

Jak więc widzisz, sam sposób rzutowania nie jest zbyt trudny. Najpierw podajemy typ, jaki chcemy otrzymać (czyli np. int, float), a następnie wyrażenie, które chcemy do tego typu przekształcić. W najprostszym przypadku, wyrażenie może być po prostu nazwą zmiennej, a w bardziej skomplikowanym na przykład sumą dwóch zmiennych.

Mimo że oba te rzutowania są do siebie dość podobne i różnią się tak naprawdę tylko rozmieszczeniem nawiasów, to nie są identyczne. Różnicami jednak zajmować się nie będziemy, bowiem tak naprawdę tych rzutowań będziemy starali się unikać mimo że ich zapis jest bardzo prosty i łatwy do zapamiętania.

Czas na przykłady zastosowania rzutowania. Oto pierwszy przykładowy program. Program będzie demonstracją jak można sobie poradzić z tym, z czym na początku tej lekcji mieliśmy program, czyli ilorazem dwóch liczb typu całkowitego int:

#include <iostream>

using namespace std;

int main()
{
  int a=2, b=3, wynik1;
  float wynik2;
 
  cout <<"Iloraz liczb wynosi "<< (float) 2/3 <<endl; //(1)  
 
  cout <<"Iloraz liczb wynosi "<< a/ (double) b <<endl; //(2)
 
  wynik1= float (a/b);  // (3)
  cout <<"Iloraz liczb wynosi "<< wynik1 <<endl;
 
  wynik2= float (a/b); // (4)
  cout <<"Iloraz liczb wynosi "<< wynik2 <<endl;    
 
  wynik2= float (a)/b; // (5)
  cout <<"Iloraz liczb wynosi "<< wynik2 <<endl;  
 
  wynik2= (float) a/b; // (6)
  cout <<"Iloraz liczb wynosi "<< wynik2 <<endl;      

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.5

Mimo że program jest prosty, warto go skomentować. W miejscu // 1 dokonujemy rzutowania na typ float. Rzutowaniu w tym przypadku podlega wyłącznie pierwszy literał, czyli 2. Następnie wszystko dokonuje się już za pomocą niejawnych konwersji. Ponieważ pierwszy argument operatora dzielenia jest typu float, to drugi zostanie również do tego typu przekształcony i w rezultacie wynik przekształcenia będzie również typu float.

W miejscu // 2 dokonujemy rzutowania na typ double, tym razem drugiego argumentu przekształcenia. Następnie dokonywana jest niejawna konwersja pierwszego argumentu i wynik operacji będzie typu double.

W miejscu opatrzonym komentarzem // 3 dokonujemy przekształcenia całego wyrażenia do typu float. Ponieważ jednak zmienna wynik1 jest typu int, to za moment zostaje wykonane niejawne przekształcenie do typu int, a kompilator powinien tutaj wysłać ostrzeżenie.

Podobnie postępujemy w miejscu // 4. Całe wyrażenie zostaje przekształcone do typu int dzięki rzutowaniu. Ponieważ zmienna wynik2 jest również typu float, to nie muszą być wykonywane już żadne niejawne konwersje.

W tym miejscu pragnę zwrócić Twoją uwagę, że ani w // 3 ani w // 4 nie osiągamy tego, czego byśmy oczekiwali. Ponieważ dokonujemy przekształcenia całego wyrażenia do typu float, to najpierw zostanie przeprowadzone zwykłe dzielenie dwóch liczb typu int (i tu już sprawiamy, że liczby zmiennopozycyjnej nie uzyskamy), a dopiero później wynik tej operacji ulega rzutowaniu do typu int. Dodatkowo, w // 3, zmienna jest typu int, więc nawet gdybyśmy przypisali do niej wartość zmiennopozycyjną, nic by nam to nie dało.

W miejscach // 5 i // 6 dokonujemy rzutowania pierwszego argumentu operatora dzielenia do typu float (za pomocą dwóch zapisów). Drugi argument zostanie niejawnie przekształcony do typu float i w rezultacie wynik będzie również typu float. Operacje te przeprowadzamy tylko i wyłącznie dla zmiennej wynik2, bowiem dla zmiennej wynik1, nadal rezultat byłby oczywiście typu całkowitego.

Innym przykładem niech będzie wypisanie znaków w kodzie ASCII wraz z ich wartością liczbową w tym kodzie. Ponieważ niektóre ze znaków nie są raczej przeznaczone do wypisywania na ekranie, wypiszemy znaki o kodach 32-255. Oto przykładowy program:

#include <iostream>

using namespace std;

int main()
{
  for (unsigned int i=32; i<256; ++i)
  {        
      cout <<i<<". "
           << (char) i<<" ";
           
      if ((i+1)%4 == 0)
         cout <<endl;
  }      

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.6

Dzięki pętli oraz rzutowaniu typu int do typu char, udało nam się w programie poznać, jaka jest wartość kodu ASCII niemal wszystkich interesujących nas znaków (z wyjątkiem znaków specjalnych).

Oprócz tego, że konwersje są nam niekiedy przydatne do osiągnięcia pewnych celów (tak jak w dwóch poprzednich przykładach), to warto ich użyć w sytuacjach, kiedy wiemy, że niejawne przekształcenie, które zajdzie, będzie dla kompilatora podejrzane. Takim przekształceniem jest zazwyczaj przypisywanie wartości zmiennopozycyjnych do zmiennych całkowitych. Zagadnienie to przedstawia poniższy przykład:

#include <iostream>

using namespace std;

int main()
{
  double duza=543.324;
  int calkowita;

/*    calkowita = duza;    
  cout <<calkowita<<endl; */

 
  calkowita = (int) duza;    
  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.7

Jak widzisz część kodu jest opatrzona komentarzem. Jeśli usuniemy komentarz, to program się skompiluje, jednak zgłosi ostrzeżenie, że dokonujemy konwersji z typu double to typu int. Dlatego też dwie linijki niżej dokonujemy takiego samego przypisania, ale tym razem dodatkowo stosujemy rzutowanie. W ten sposób informujemy kompilator, że wiemy co robimy i że zdajemy sobie sprawę, że zmiennej typu całkowitego przypisujemy wartość zmiennej typu zmiennopozycyjnego.

Mimo że rzutowanie jest, jak widzisz, niekiedy przydatne i pomocne, ma swoje minusy. Ponieważ podczas rzutowania, dajemy kompilatorowi do zrozumienia, że wiemy co robimy, to najczęściej (choć nie zawsze) pomija on sprawdzanie poprawności typów i rezultaty niektórych operacji mogą być bardzo dziwne.

Wyobraźmy sobie następującą sytuację. Pewien początkujący programista miał program taki jak poniżej:

#include <iostream>

using namespace std;

int main()
{
  float duza = 453453.8912;
  int calkowita = duza;

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.8

Zachwycony jednak rzutowaniem i chcąc zostać dobrym programistą, postanowił, że uniknie ostrzeżenia kompilatora i użyje rzutowania, aby uniknąć budzącej wątpliwości linijki. Otrzymał zatem taki kod programu:

#include <iostream>

using namespace std;

int main()
{
  float duza = 453453.8912;
  int calkowita= (int) duza;

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.9

Pozbył się zatem ostrzeżenia i wszystko było wspaniale. Mógł pracować nad dalszą częścią swojego programu, oznaczoną tutaj przez symboliczny kod w komentarzu.

W trakcie pracy nad programem, okazało się, że lepiej, gdyby zmienna duza była przechowywana nie w postaci zmiennej typu double, ale w postaci tablicy znaków, bowiem ułatwiało to w znacznym stopniu pracę naszemu programiście. Dokonał zatem zmian w programie:

#include <iostream>

using namespace std;

int main()
{
  char duza[20] = "453453.8912";
  int calkowita= (int) duza;

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.10

Dzięki wprowadzonej zmianie, mógł kontynuować pracę nad dalszą częścią programu. Kiedy już program ukończył, napotkał na dziwną sprawę - wartość zmiennej calkowita otrzymana na końcu działania programu wydaje się być nieprawidłowa. Programista, sądząc, że popełnił gdzieś błąd w części programu, nad którą ostatnio pracował, spędził kilka godzin, aby odnaleźć błąd, jednak nic nie znalazł. W końcu zrezygnował z pisania programu albo też zaczął pisać program od początku, bowiem błędu nie udało mu się nigdy odnaleźć.

Brzmi strasznie - prawda? Z powodu jednego nieświadomego działania, cały projekt został porzucony albo musiał zostać napisany od początku? Mam nadzieję, że domyślasz się, co było źródłem kłopotów programisty. Okazuje się, że stała się nim właśnie operacja rzutowania, która miała początkowo sprawić, że nie będzie pojawiało się niepotrzebne ostrzeżenie podczas kompilacji programu.

Jak to się zatem stało? Ponieważ w pierwotnej wersji programu, zmienna duza była typu double, to przekształcenie typu double do typu int było dokonywane niejawnie przez kompilator. Pojawiało się ostrzeżenie, dla którego uniknięcia użyto rzutowania. Na tym etapie było nadal wszystko w absolutnym porządku.

Problem pojawił się, kiedy programista zmienił typ zmiennej duza z typu double do typu tablicowego. Okazało się, że tutaj pojawił się tak naprawdę problem. Czy wiesz jaki?

Otóż, jak wcześniej zaznaczyłem, podczas rzutowania, zazwyczaj nie są sprawdzane typy, które podlegają rzutowaniu, bowiem kompilator zakłada, że skoro używamy rzutowania, to jesteśmy świadomi tego, co robimy. W naszym przypadku też byliśmy początkowo pewni, że rzutowanie jest zupełnie sensowne (i tak właśnie było), jednak nie przewidzieliśmy, że kiedyś dokonamy zmiany typu zmiennej (zmiennej calkowita).

O ile rzutowanie typu double do typu int, było sensowne, o tyle o rzutowanie zmiennej, która jest typu tablicowego do typu całkowitego, raczej nam chodzić nie będzie. Nie pomyślał o tym też nasz programista.

Zwróć uwagę, co by się stało, gdyby programista zrezygnował z zastosowanego początkowo rzutowania. W końcowym etapie swojej pracy uzyskał by następujący kod:

#include <iostream>

using namespace std;

int main()
{
  char duza[20] = "453453.8912";
  int calkowita= duza;

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.11

Jeśli skompilujesz ten kod, to kompilacja się nie powiedzie, bowiem kompilator zaprotestuje: invalid conversion from 'char *' to int. Zatem gdyby programista nie przejmował się początkowym ostrzeżeniem kompilatora, nie napotkałby nigdy poważnych problemów, bowiem przy zmianie typu zmiennej i próbie kompilacji, dowiedziałby się o źródle potencjalnego błędu.

Po co nam zatem rzutowanie? Czy warto je stosować skoro można doprowadzić do tak poważnych problemów? Moim zdaniem tak, jednak podczas rzutowania, należy być pewnym jakich typów są zmienne, żeby nie doprowadzić do nieprzewidzianych sytuacji.

Gdyby nasz programista był nieco lepszym programistą (a tak naprawdę raczej programistą - kombinatorem), w momencie kiedy chciał pozbyć się ostrzeżenia generowanego przez kompilator, mógł napisać taki kod:

#include <iostream>

using namespace std;

int main()
{
  double duza = 453453.8912;
  int calkowita = (int) double (duza);

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.12

Wówczas po zmianie typu zmiennej duza na tablicę znaków, program nie uległby kompilacji. Kompilator zgłosiłby następujący błąd: pointer value used where a floating point value was expected.

Taki zapis jednak byłby raczej bardziej przypadkiem niż świadomym działaniem i takich zapisów w praktyce raczej się nie stosuje.

Co zatem należałoby zrobić, żeby móc skorzystać z dobrodziejstw rzutowania, a jednocześnie zabezpieczyć się przed nieprzewidzianymi sytuacjami takimi jak na przykład zmiana typów powodująca wręcz absurdalne wyniki?

Aby móc osiągnąć takie korzyści, należy porzucić rzutowanie w starym stylu języka C, a skorzystać z operatorów rzutowania, które pojawiły się dopiero w języku C++.

Istnieją 4 operatory służące do rzutowania, które pojawiły się w języku C++. Są to static_cast, dynamic_cast, const_cast oraz reinterpreter_cast.

Jako że na razie w Twoich programach nie będzie wymagane używanie wszystkich tych operatorów oraz że Twoja wiedza jest na razie niewystarczająca, aby wyjaśnić dokładne zastosowanie ich wszystkich, skupię się tylko na operatorze static_cast.

Operator static_cast

Operator static_cast jest tym operatorem, który powinien być przez Ciebie stosowany w przypadku rzutowania, jeśli rzutowanie będzie się odbywało dla typów prostych (czyli typów, które nie są wskaźnikami ani referencjami).

Schematycznie aby dokonać rzutowania z użyciem operatora static_cast piszemy:

static_cast<typ> (wyrazenie);

Jak widzisz, tym razem wykonanie rzutowania nie będzie tak wygodne (będzie się trzeba bardziej napisać), ale w nagrodę uzyskamy większą przejrzystość kodu - łatwiej nam będzie takie rzutowanie zauważyć i w bardzo podejrzanych sytuacjach kompilator odmówi kompilacji lub też wyśle ostrzeżenie.

Przykładowy program, który miałby służyć do usunięcia ostrzeżenia, gdy przypisujemy do zmiennej typu całkowitego wartość zmiennej typu zmiennopozycyjnego wyglądałby następująco:

#include <iostream>

using namespace std;

int main()
{
  double ulamkowa=710.8178;
  int calkowita;
 
  calkowita = static_cast<int> (ulamkowa);
  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.13

Jak widzisz, zmienia się tylko wywołanie w stosunku do rzutowania w starym stylu i tak naprawdę nie ma żadnych nowości, dlatego też możesz na własną rękę w ramach treningu zastosować operator dla wszystkich poprzednich programów.

Chcę tylko zwrócić Twoją uwagę, że gdyby przykładowy programista, użył zamiast rzutowania w starym stylu, rzutowanie za pomocą operatora static_cast, czyli gdyby jego program wyglądał następująco:

#include <iostream>

using namespace std;

int main()
{
  float duza = 453453.8912;
  int calkowita = static_cast<int> (duza);

  /* Tutaj 1000 linii programu,
     w tym rowniez operacje na zmiennej calkowita
  */


  cout <<calkowita<<endl;

  cout <<endl<<"Nacisnij ENTER aby zakonczyc"<<endl;
  getchar();
  return 0;  
}
program nr 32.14

to po zmianie typu zmiennej duza na typ tablicowy, program nie uległby kompilacji i pojawiłby się następujący błąd: invalid static_cast from type 'char[20]' to type 'int'. Programista uniknąłby dzięki temu całego zamieszania i zaoszczędził wiele czasu.

Mam nadzieję, że przekonuje Cię to, że mimo nieco mniej wygodnego zapisu, warto stosować właśnie operator static_cast do rzutowania zamiast stosowania rzutowania w starym stylu.

Podsumowanie

W tej lekcji przedstawiłem Ci bardzo ważne zagadnienie jakim są przekształcenia typów, zarówno te jawne, jak i niejawne. Mam nadzieję, że teraz rozumiesz już wszystkie operacje wykonywane na zmiennych i wiesz jak sprawić, aby uzyskiwane wyniki były takiego typu, jakiego sobie życzysz.

powrót