Rozważmy niewinnie wyglądający fragment programu:
       double x = 1.5, y;
       int    k =  10;
           // ...
       y = x + k;
Jaka funkcja wykonująca dodawanie będzie wywołana,
aby wykonać polecenie '
x+k' z ostatniej linii?
Zmienna
k
 jest typu
int, zmienna
x
 zaś
typu
double. Mają one zatem zupełnie inną strukturę
w pamięci komputera. Aby je prawidłowo dodać, należy oczywiście
tę strukturę uwzględnić i zdecydować, jaki z kolei ma być
typ wyniku. Czy istnieje zatem jakaś funkcja realizująca
takie dodawanie? A gdyby
k
 była typu
unsigned short?
Dodawanie liczby zapisanej w postaci
double
 do liczby
zapisanej w postaci
unsigned short
 jest oczywiście zupełnie
czym innym niż dodawanie
double'a do
int'a.
Musiałaby zatem istnieć ogromna liczba funkcji realizujących to samo
zadanie, ale inaczej, w zależności od typów argumentów. To samo
dotyczy wywołań funkcji. Dostępna po dołączeniu pliku
nagłówkowego
cmath
 (albo
math.h) funkcja
sin
 ma jeden parametr typu
double
 (lub
long).
Czy oznacza to, że wywołanie
sin(1) jest błędem, czy
może istnieje inna wersja tej funkcji z parametrem
int?
Tymi właśnie problemami teraz się zajmiemy.
Załóżmy, że w programie występuje wyrażenie z operatorem dwuargumentowym (dodawanie, mnożenie, ...). Przed wykonaniem operacji dokonywane są na wartościach argumentów konwersje standardowe, których celem jest:
Wyjaśnijmy przebieg poszukiwania tego wspólnego dla wartości obu argumentów typu. Zauważmy, że konwersje są, jeśli to tylko możliwe, promocjami, to znaczy typ „węższy” jest awansowany do typu „szerszego” tak, żeby nie utracić dokładności. W tym sensie np. typ int jest „węższy” od double, bo każda wartość całkowita może być zapisania w formie double bez utraty dokładności, ale nie odwrotnie. Z drugiej strony, nie dla wszystkich pokrewnych typów można zdefiniować taką relację zawierania: zbiory wartości typu int i typu unsigned int są tak samo liczne (232 elementów), ale żaden nie zawiera się w drugim. Wrócimy do tego problemu w dalszym ciągu.
Zatem:
Na przykład, zgodnie z tymi zasadami, wyrażenie w trzeciej linii fragmentu
       int i = 1, j = 2, k = 3, m;
       // ...
       m = (j > i) + (k > j);
jest prawidłowe: wartości typu
bool, jakimi są wartości
wyrażeń
(j > i) i 
(k > j), zostaną
niejawnie przekształcone do wartości całkowitych
(jedynek, bo obie relacje są w naszym przykładzie prawdziwe).
Zatem operator dodawania „zobaczy” po obu stronach dwie jedynki i
zmienna
m
 uzyska wartość 2.
Często popełniany jest błąd przy dzieleniu:
pamiętajmy, że dzielenie liczb całkowitych zawsze,
zgodnie z powyższymi zasadami, daje wynik całkowity, a więc
części ułamkowej nie ma. Początkującym często wydaje się,
że jeśli przypisują wynik do zmiennej typu
double,
to w jakiś tajemniczy sposób część ułamkowa zostanie
„odzyskana”: tak nie będzie, jej tam po prostu nie ma, nie została
policzona!
Tak więc, jeśli aktualną wartością
zmiennej
k
 typu
int
 jest 7, to wartością
wyrażenia '
k/2' jest dokładnie 3. Jeśli chodziło
nam raczej o 
3 , to wystarczy dopisać kropkę
przy dwójce: wartością '
k/2.' jest 3.5, bo '2.'
(z kropką) jest interpretowane jako literał wartości
typu
double, a zatem i wartość
k
 zostanie
przekształcona do typu
double, a co za tym idzie i wynik
będzie również tego właśnie typu.
, to wystarczy dopisać kropkę
przy dwójce: wartością '
k/2.' jest 3.5, bo '2.'
(z kropką) jest interpretowane jako literał wartości
typu
double, a zatem i wartość
k
 zostanie
przekształcona do typu
double, a co za tym idzie i wynik
będzie również tego właśnie typu.
Do standardowych niejawnych konwersji, które mogą być wykonane bez naszej wiedzy (i, niestety, zgody...), należy też:
Brzmi to skomplikowanie, bo też, niestety, jest to skomplikowane. W dodatku jest też niebezpieczne: w C/C++ dozwolone są niejawne konwersje, na skutek których traci się informację (zwykle kompilatory wyświetlają wtedy jakieś ostrzeżenia).
Pamiętać przy tym należy, że niejawne konwersje wcale nie
gwarantują, że otrzymana po przekształceniu wartość jest
z naszego punktu widzenia sensowna, to znaczy zgodna w oczekiwanym
przez nas sensie z wartością przed przekształceniem.
Na przykład rozpatrzmy program:
      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int main()
      5.  {
      6.      int      k   = -2;
      7.      unsigned uns =  1;
      8.  
      9.      int      x = k + uns;
     10.      unsigned y = k + uns;
     11.  
     12.      cout << "x   = " << x      << endl;
     13.      cout << "y   = " << y      << endl;
     14.      cout << "y+1 = " << y + 1  << endl;
     15.  
     16.  
     17.      signed   char c = 255;
     18.      unsigned char d = 255;
     19.  
     20.      cout << "c+1 = " << c + 1  << endl;
     21.      cout << "d+1 = " << d + 1  << endl;
     22.      d = d + 1;
     23.      cout << "d   = " << (int)d << endl;
     24.  }
Program ten drukuje:
    x   = -1
    y   = 4294967295
    y+1 = 0
    c+1 = 0
    d+1 = 256
    d   = 0
Wartość zmiennej
x
 to zgodnie z oczekiwaniem -1.
Ale drukowanie wartości zmiennej
y
 daje
4294967295 (nie jest to
taka sobie przypadkowa liczba; jej wartość to 232 - 1).
Po dodaniu
do tej liczby jedynki otrzymujemy dokładnie zero (linia 14
programu drukuje 'y + 1 = 0'). Wydawałoby się, że
zmienne
c
 i 
d
 mają te same wartości, więc i po
dodaniu jedynki otrzymamy to samo. Ale linia 20 drukuje
'c + 1 = 0', natomiast linia 21 drukuje 'd + 1 = 256'.
Jednak gdy wartość '
d + 1' przypiszemy znów
do zmiennej
d
 i wydrukujemy jej wartość
jako
int
 (linia 23), dostaniemy 0.
Jak widać, szczególnie łatwo jest pogubić się przy
konwersjach od typów ze znakiem do typów bez znaku
i odwrotnie — związane jest to z faktem, o którym wspominaliśmy,
że
zbiory wartości takich typów nie pozostają do siebie w relacji
zawierania: nie można powiedzieć, który jest „węższy”, a który
„szerszy”.
Najczęściej jednak nie musimy aż tak dokładnie analizować tego typu wyrażeń, jeśli trzymamy się podstawowych typów i staramy się pisać kod w sposób czytelny i prosty. Trzeba tylko pamiętać, że typy całkowite węższe od int zawsze promowane są co najmniej do typu int. Dotyczy to w szczególności wartości znakowych (typu char). Użyte jako argument operatorów lub argument w wywołaniu funkcji są przekształcane do wartości całkowitej typu int równej kodowi ASCII danego znaku. Na przykład po przypisaniu
int k = 3 + 'a';wartość k wynosi 100, bo kod ASCII małej litery 'a' jest 97.
Można z tej własności skorzystać do napisania
prostej funkcji odczytującej ciąg znaków aż do napotkania
nie-cyfry i interpretującej ten napis jako
dodatnią liczbę całowitą:
      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  int konwert(char* nap) {
      5.      int w = 0, i = 0, c;
      6.      while (c = nap[i++], c >= '0' && c <= '9')
      7.          w = 10*w + c - '0';
      8.      return w;
      9.  }
     10.  
     11.  int main() {
     12.      char tab1[] = "123a";
     13.      char tab2[] = "456 1";
     14.      char tab3[] = " 56";
     15.  
     16.      cout << "tab1 -> " << konwert(tab1) << endl;
     17.      cout << "tab2 -> " << konwert(tab2) << endl;
     18.      cout << "tab3 -> " << konwert(tab3) << endl;
     19.  }
Wynikiem jest
    tab1 -> 123
    tab2 -> 456
    tab3 -> 0
Funkcja
konwert
 pobiera jako argument tablicę
znaków w postaci wskaźnika do pierwszego jej elementu.
Następnie, w pętli
while
 przetwarzane są kolejne znaki
tego napisu. Wewnątrz nawiasów okrągłych, gdzie jest miejsce
na wyrażenie logiczne sterujące pętlą, mamy tu wyrażenie
przecinkowe. Wyrażenie będące lewym argumentem wczytuje kolejny
znak napisu do zmiennej
c, po czym zwiększa aktualną
wartość indeksu. Wartością całego wyrażenia przecinkowego
jest wartość prawego argumentu, a tu mamy sprawdzenie, czy wczytany
znak jest cyfrą. Co jest bowiem np. wartością wyrażenia
c >= '0'? Przed dokonaniem porównania obie strony
tej nierówności są przkształcane do typu
int. Zatem wartością
zmiennej znakowej
c
 będzie kod ASCII wczytanego znaku.
Podobnie wartością
'0'
 (z apostrofami!) będzie
po konwersji do typu
int
 kod ASCII znaku '0' — jest to
liczba 48, ale wcale nie musimy o tym wiedzieć. Podkreślmy jeszcze raz,
bo jest to źródłem wielu błędów: wartością liczbową zmiennej znakowej
'0'
 jest kod ASCII znaku zero (czyli 48), a nie liczba
zero. Literałem znaku o kodzie ASCII równym zero jest '
\0', łącznie
z apostrofami i odwrotnym ukośnikiem.
Kody ASCII kolejnych cyfr, 0, ..., 9, są kolejnymi liczbami całkowitymi (zeru odpowiada 48, jedynce — 49 itd., ale na szczęście nie musimy tych kodów pamiętać). Tak więc warunek c >= '0' && c <= '9' sprawdza, czy kod ASCII znaku c jest jednocześnie większy lub równy od kodu znaku '0' i mniejszy lub równy od kodu znaku '9', czyli czy jest to znak odpowiadający cyfrze. Jeśli nie, pętla zostanie przerwana.
Powtórnie wykorzystujemy ten mechanizm w następnej linii: wyrażenie c-'0' ma wartość liczbową równą liczbie reprezentowanej przez znak c. Jeśli np. c jest znakiem '4', to '4'-'0' jest tym samym co 52 - 48, czyli 4 (tym razem liczbowo cztery). Zatem, jeśli kolejny wczytany znak jest cyfrą, to dotychczasowa wartość zmiennej w jest mnożona przez dziesięć, co odpowiada przesunięciu cyfr o jedną pozycję w lewo, i dodawana jest na pozycji jedności wartość liczbowa odpowiadająca wczytanej cyfrze. Pętla kończy się, gdy kolejnym znakiem nie jest cyfra. Dlatego tab2 zostanie zinterpretowana jako 456; wczytywanie zostanie zakończone, gdy napotkany zostanie odstęp pomiędzy cyfrą 6 a 1. Dla tab3 otrzymamy 0, bo pętla nie wykona ani jednego obrotu: jako pierwszy znak zostanie bowiem wczytany odstęp, a więc nie-cyfra.
T.R. Werner, 21 lutego 2016; 20:17