Koło informatyczne

Przyspieszony kurs języka C++

 

Wyrażenia

Wyrażenia są używane do przetwarzania danych i trudno się bez nich obejść w programowaniu. Wyrażenie składa się z operandów i operatorów. Operandy są to przetwarzane dane, a operatory są to symbole operacji nad tymi danymi. Niektóre operatory już znasz:

 
+   dodawanie
-   odejmowanie
*   mnożenie
/   dzielenie

 

Na pozór wydaje się, że nic nas tutaj nie może zaskoczyć. Czy aby na pewno? Uruchom poniższy program:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    cout << 2/3 << endl;
    return 0;
}
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    cout << 2*2/3 << endl;
    return 0;
}

 

Na zdrowy rozum, pierwszy program powinien dać wynik 0.6666..., a drugi 1.3333...

Niestety, otrzymujemy za pierwszym razem wynik 0, a za drugim razem wynik 1. O co tutaj chodzi? Zapamiętaj regułę: Jeśli oba argumenty operatora są liczbami całkowitymi, to wynik jest również całkowity. Wykonywaliśmy działanie dzielenia dwóch liczb całkowitych: 2/3. Skoro tak, to komputer wykonał dzielenie całkowitoliczbowe i wynikiem jest liczba całkowita 0, ponieważ tyle razy 3 mieści się w 2. Za drugim razem wykonywane było działanie 2 x 2 / 3. Operacje mnożenia i dzielenia posiadają ten sam priorytet (czyli ważność w wyrażeniu) i dlatego będą wykonywane od strony lewej do prawej. Zatem komputer najpierw pomnoży 2 x 2 i otrzyma wynik 4. Następnie wynik 4 podzieli przez 3. Ponieważ 4 i 3 są liczbami całkowitymi, dzielenie również będzie całkowite i otrzymamy wynik 1.

Czy teraz jest to zrozumiałe?

Co zatem zrobić? Należy jeden z operandów przekształcić na typ zmiennoprzecinkowy, wtedy operacja będzie zmiennoprzecinkowa. Jeśli są to stałe, to wystarczy dopisać część ułamkową 0 przy jednym z operandów:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    cout << 2/3.0 << endl;
    return 0;
}
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    cout << 2*2/3.0 << endl;
    return 0;
}

 

Co jednak zrobić, jeśli operandy są zmiennymi? Uruchom poniższy program:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    int a,b;
    double x;

    a = 2;
    b = 3.0;
    x = a/b;
    cout << x << endl;
    return 0;
}

 

Znów dostajemy 0, chociaż do b wprowadziliśmy liczbę zmiennoprzecinkową 3.0! Tutaj z kolei zaszła sytuacja odwrotna. Ponieważ b jest zmienną całkowitą, to komputer przekształcił liczbę zmiennoprzecinkową 3.0 na jej odpowiednik całkowity, czyli liczbę 3. Dzielenie znów jest całkowitoliczbowe, ponieważ oba operandy są zmiennymi całkowitymi. Cóż zatem zrobić? Na przykład możemy postąpić tak:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    int a,b;
    double x;

    a = 2;
    b = 3.0;
    x = 1.0*a/b;
    cout << x << endl;
    return 0;
}

 

Pierwszą zmienną a  mnożymy przez liczbę zmiennoprzecinkową 1.0. W wyniku otrzymamy wartość a, lecz teraz będzie to liczba zmiennoprzecinkowa. Dlaczego? Pamiętasz, co powiedzieliśmy przed chwilą? Wynik jest zmiennoprzecinkowy, jeśli jeden z operandów jest liczbą zmiennoprzecinkową, a taką u nas jest liczba 1.0. Zatem dzielenie będzie zmiennoprzecinkowe.

Mnożenie przez jeden nie zmienia wartości, jednak wymaga czasu. Innym sposobem jest jawne przekształcenie operandu całkowitego w operand zmiennoprzecinkowy. W tym przypadku stosujemy tzw. rzutowanie (ang. casting). Przed operandem umieszczamy nazwę typu w nawiasach:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    int a,b;
    double x;

    a = 2;
    b = 3.0;
    x = (double)a/b;
    cout << x << endl;
    return 0;
}

 

Ale bądź ostrożny! Dlaczego poniższy program znów daje złe wyniki pomimo rzutowania?

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    int a,b;
    double x;

    a = 2;
    b = 3.0;
    x = (double)(a/b);
    cout << x << endl;
    return 0;
}

 

Dochodzimy tutaj do kolejnej własności wyrażeń – nawiasów. Operatory posiadają tzw. priorytet, czyli ważność. Operatory o wyższym priorytecie zostaną wykonane przed operatorami o priorytecie niższym. Na przykład wyrażenie 2 + 2 * 2 daje wynik 6 a nie 8, ponieważ operator mnożenia * posiada wyższy priorytet od operatora dodawania. Zatem komputer najpierw wymnoży dwa ostatnie operandy i otrzyma wynik 4. Teraz pierwszy operand zostanie dodany do wyniku mnożenia i otrzymamy 6.

Jeśli naszą intencją było wykonać najpierw dodawanie, a później sumę pomnożyć przez 2, to powinniśmy byli zastosować nawiasy. Wyrażenia ujęte w nawiasy są wyliczane najpierw. Zatem nasze wyrażenie (2 + 2) * 2 da teraz wynik 8.

To wyjaśnia, dlaczego (double)(a/b) w naszym programie dało wynik 0. Po prostu komputer najpierw wykonał dzielenie w nawiasie, a tam oba operandy są przecież liczbami całkowitymi. W wyniku otrzymano 0, które zostało przekształcone w 0.0.

Dodawanie i odejmowanie również potrafi czasami wymknąć się spod kontroli nieuważnego programisty. Szczególnie, gdy mieszamy ze sobą typy int i unsigned. Uruchom poniższy program:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    unsigned a,b,c;
    int d;

    a = 2000000000;
    b = 1000000000;
    c = a + b;
    d = a + b;
    cout << c << endl
         << d << endl;
    return 0;
}

 

Otrzymujemy dwa wyniki. Jeden spodziewany 3000000000 i drugi dziwny -1294967296. Tutaj błędem jest przekroczenie dopuszczalnego zakresu. Zmienne typu unsigned są liczbami w naturalnym kodzie binarnym. Dla 32 bitów zmienne te mogą przechowywać liczby z zakresu od 0 do 232 - 1, czyli 0...4294967295. Wynik pierwszego dodawania trafia do zmiennej c, która jest typu unsigned. Wynik ten mieści się w zakresie tego typu.

Zmienne typu int są 32 bitowymi liczbami w kodzie U2. Jak pamiętamy, w U2 najstarszy bit ma wagę przeciwną. Pozwala to zapisywać liczby ujemne. 32 bitowa liczba U2 może przyjmować wartości z zakresu od -231 do 231 - 1, czyli -2147483648...2147483647. Wynik drugiego dodawania trafia do zmiennej typu int. Jest on zbyt duży, dlatego zostanie źle zinterpretowany.

W zapisie dwójkowym liczba 3000000000 ma postać 10110010110100000101111000000000. Składa się zatem z 32 bitów i najstarszy bit ma wartość 1. Jeśli zinterpretujemy te bity jako liczbę U2, a tak dzieje się w zmiennej d, to musi być ona ujemna z uwagi na wartość najstarszego bitu. Dlatego otrzymujemy wynik -1294967296, który jest interpretacją U2 wartości NBC.

Podobny błąd może wystąpić przy przekształcaniu typu int na unsigned. Uruchom poniższy program:

 
Code::Blocks
// Dziwne zachowanie się operatorów
// Koło informatyczne
// (C)2014 I LO w Tarnowie
//------------------------------

#include <iostream>

using namespace std;

int main()
{
    int a,b,c;
    unsigned d;

    a = 1;
    b = 2;
    c = a - b;
    d = a - b;
    cout << c << endl
         << d << endl;
    return 0;
}

 

Tutaj odejmujemy liczby 1 i 2. Pierwszy wynik jest poprawny. Drugi nie. Znów powodem jest interpretacja bitów. W kodzie U2 liczba -1, będąca wynikiem odejmowania, posiada zapis dwójkowy 11111111111111111111111111111111. Jak widzisz, składa się ona z samych jedynek. W naturalnym kodzie binarnym zostanie to jednak zinterpretowane jako 4294967295.

 

Poniżej przedstawiamy podstawowe operatory arytmetyczne w kolejności priorytetów od najwyższego do najniższego. Więcej operatorów poznasz w dalszej części kursu.

 
-
  minus oznaczający wartość ujemną
* / %
  mnożenie, dzielenie, reszta z dzielenia
+ -
  dodawanie, odejmowanie

 

Podsumowując, zapamiętaj:

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe