Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

Autor artykułu: mgr Jerzy Wałaszek
Zmodyfikowano 29.01.2024

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

Matura - programowanie w C++

Praca ze zmiennymi

SPIS TREŚCI

Modyfikacje

Zanim przejdziemy do głównego tematu tego podrozdziału, musimy zwrócić uwagę na ważną własność operatora przypisania (=) w języku C++. Zapamiętaj, że operatory zwracają wartość operacji, którą wykonują. Operator przypisania umieszcza w zmiennej wartość wyrażenia, np:

a = (b + 10) * 8;

Do zmiennej a trafi wynik obliczenia wyrażenia (b + 10) * 8. Jednocześnie wynik ten jest wartością tej operacji, czyli wartością operatora przypisania. Dzięki temu można wynik przypisania wykorzystać w dowolnym wyrażeniu, np:

c = (a = (b + 10) * 8) - 2;

Najpierw komputer wyliczy wartość wyrażenia (b + 10) * 8 i wynik wpisze do zmiennej a. Wynik ten jest wartością tej operacji przypisania, załóżmy, że jest to wartość x. W następnej kolejności komputer wyliczy (x - 2) i wynik wpisze do zmiennej c.

Osobiście nie polecam takich konstrukcji w programie, gdyż trudno je analizować. Lepiej jest użyć dwóch instrukcji:

a = (b + 10) * 8;
c = a - 2;

Czyż nie wygląda to lepiej? Jednakże tę własność operatora przypisania można wykorzystać w przypadku, gdy do kilku zmiennych należy wprowadzić tę samą wartość, np. zamiast:

a = 5; b = 5; c = 5; d = 5;

Można zastosować:

a = b = c = d = 5;

Najpierw komputer wykona ostatnie przypisanie i umieści w zmiennej d liczbę 5. Wartością tej operacji będzie 5. Wartość ta zostanie wykorzystana w kolejnym przypisaniu i do zmiennej c też trafi 5. To samo stanie się ze zmiennymi b i a. W każdej z nich znajdzie się liczba 5.

Modyfikacja polega na zmianie wartości zmiennej zależnie od tego, co w zmiennej się znajduje. Poznaliśmy już dwa operatory modyfikacji:

++zmienna lub zmienna++
--zmienna lub zmienna--

Operatory te odpowiednio zwiększają lub zmniejszają zawartość zmiennej o 1. Wartość operatora zależy od jego pozycji w stosunku do modyfikowanej zmiennej. Jeśli operator jest przed zmienną, to wartością operatora jest zmodyfikowana zawartość zmiennej:

++zmienna
--zmienna

Jeśli operator jest za zmienną, to jego wartością jest niezmodyfikowana jeszcze zawartość zmiennej:

zmienna++
zmienna--

Ma to znaczenie, gdy operator ++ lub -- zostanie użyty w wyrażeniu:

a = 5; b = ++a;

W a i b będzie liczba 6.

a = 5; b = a--;

W a będzie 6, w b będzie 5.

Innym przykładem modyfikacji jest np. dodanie do zmiennej jakiejś wartości, powiedzmy. 5. Wykonujemy to następująco:

a = a + 5;

W identyczny sposób możemy odjąć 5 od zmiennej:

a = a - 5;

pomnożyć zmienną przez 5:

a = a * 5;

Ogólnie jest to operacja modyfikacji typu:

zmienna = zmienna operator wyrażenie;

Tego typu operacje często występują w różnych algorytmach, dlatego w języku C++ istnieją odpowiednie operatory modyfikacji. Mają one postać:

oparator=

Jako operator może być użyty każdy operator dwuargumentowy:

Operator Operacja Przykład
+= a ← a + wyrażenie a += 5;
-= a ← a - wyrażenie a -= 5;
*= a ← a * wyrażenie a *= 5;
/= a ← a / wyrażenie a /=5;
%= a ← a % wyrażenie a %= 5;
||= a ← a || wyrażenie a ||= 5;
&&= a ← a && wyrażenie a &&= 5;

Operatory te również można stosować w wyrażeniach. Ich wartością jest to, co zostaje umieszczone po modyfikacji w zmiennej. Na przykład:

a = 5;
b = (a += 4) + 2;

W a będzie 9, w b będzie 11. Odradzam stosowania tego typu konstrukcji, ponieważ trudno je analizować i mogą dawać niespodziewane wyniki.

Do zapamiętania:


Na początek:  podrozdziału   strony 

Zmienne zmiennoprzecinkowe

Liczby zmiennoprzecinkowe są liczbami o określonej dokładności (tzw. precyzji). Oznacza to, iż pewnych wartości liczba zmiennoprzecinkowa nie jest w stanie przedstawić dokładnie, jedynie z pewnym przybliżeniem. Uruchom poniższy program:
// Zmienne fp
//-----------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{

    float  x = 123456789;
    double y = 123456789;

    cout << fixed << setprecision(2);

    cout << "float : " << x << endl
         << "double: " << y << endl << endl;

    return 0;
}
float : 123456792.00
double: 123456789.00

Co tutaj się dzieje? Tworzymy dwie zmienne zmiennoprzecinkowe: x typu float (32 bity, precyzja 7 cyfr) oraz y typu double (64 bity, precyzja 15 cyfr). Do obu zmiennych wpisujemy liczbę o tej samej wartości: 123456789. Następnie wyświetlamy te liczby w oknie konsoli. Jak widzisz zmienna x zapamiętała liczbę niedokładnie - pierwsze 7 cyfr jest w porządku, ale dwie następne już nie są. Typ float ma precyzję 7 cyfr znaczących (najstarszych). Jeśli w liczbie jest więcej cyfr, to tylko pierwsze 7 jest dokładnych. Pozostałe już nie są gwarantowane. Pamiętasz nasz przykład z liczb zmiennoprzecinkowych, gdy próbowaliśmy w naszym "szkolnym systemie fp" zapisać liczbę 9? Nie dało się, liczba została zaokrąglona do 8, ponieważ jej bity nie mieściły się w bitach mantysy. Tutaj mamy to samo zjawisko. Komputer zaokrąglił liczbę 123456789 do takiej, którą można było zapamiętać w zmiennej typu float, czyli do 123456792. Liczba typu double została zapamiętana dokładnie, ponieważ typ ten ma podwojoną precyzję w stosunku do typu float (ang. double = podwójny).

Nie oznacza to, iż typ double likwiduje problem niedokładności, on tylko odsuwa go dalej. Uruchom poniższy program:

// Zmienne fp
//-----------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    double      y = 123456789123456789;
    long double z = 123456789123456789;

    cout << fixed << setprecision(0);

    cout << "double     : " << y << endl
         << "long double: " << z << endl << endl;

    return 0;
}
double     : 123456789123456784
long double: 123456789123456789

Typ double gwarantuje 15 cyfr znaczących, pozostałe mogą już być zaburzone. W naszym przykładzie, dopiero ostatnia cyfra zmieniła się z 9 na 4. Typ long doble jest typem wykorzystywanym wewnętrznie przez mikroprocesor do wykonywania obliczeń zmiennoprzecinkowych. Później ich wynik jest zamieniany na typ float lub double. Typ long double precyzję 20 cyfr znaczących. Dzięki temu błędy obliczeniowe są mniejsze. Typ double jest wystarczająco dokładny dla typowych obliczeń naukowych i inżynierskich.


Z uwagi na ograniczoną precyzję wynik obliczeń może być błędny. Uruchom poniższy program:

// Zmienne fp
//-----------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    float suma = 100000000;
    int i;

    cout << fixed << setprecision(0);

    cout << "Przed sumowaniem" << endl
         << "suma = " << suma << endl << endl;

    for(i = 0; i < 1000000; i++) suma++;

    cout << "Po sumowaniu" << endl
         << "suma = " << suma << endl << endl;

    return 0;
}
Przed sumowaniem
suma = 100000000

Po sumowaniu
suma = 100000000

Program tworzy dwie zmienne, suma typu float oraz i typu int. W zmiennej suma umieszczona zostaje liczba 100 milionów. Program wypisuje zawartość zmiennej suma i rozpoczyna pętlę wykonującą milion obiegów. W każdym obiegu do zmiennej suma zostaje dodane 1. Zatem po zakończeniu pętli w suma powinna się znaleźć liczba 101 milionów. Po zakończeniu pętli program wypisuje zawartość zmiennej suma i co się okazuje? Dalej jest w niej liczba 100 milionów, czyli nic się nie zmieniło. Dlaczego?

Powodem jest właśnie precyzja typu float. Popatrzmy, dodajemy liczby:

100.000.000 + 1 = 100.000.001

Niestety, wynik takiego dodawania jest poza precyzją liczb typu float i nic się nie zmieni. Po dodaniu 1 do zmiennej suma w zmiennej wciąż będzie ta sama wartość. Wykonanie tego dodawania milion razy też nic nie zmienia - 1 jest zbyt małą liczbą, aby zmienić sumę.

Poeksperymentuj z tym programem, np. zmień typ float na double, zmień dodawaną wartość z 1 na 100, 1000, 10000. Wyciągnij wnioski.


Istnieją liczby, których nie da się przedstawić dokładnie w dwójkowym kodzie fp, gdyż posiadają nieskończone rozwinięcie dwójkowe. Na przykład liczba 1/10. W systemie dwójkowym ma ona postać:

1/10 = 0,0001100110011...(2)

Jest to liczba okresowa. Podobną własność mają w systemie dziesiętnym ułamki 1/3, 1/6, 1/7, 1/9... Obliczenia seryjne z liczbą 1/10 mogą dawać wyniki przybliżone. Uruchom poniższy program:

// Zmienne fp
//-----------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    double x;

    cout << fixed << setprecision(2);

    cout << "START" << endl << endl;

    for(x = 0; x != 1; x += 0.1)
        cout << x << endl;

    cout << endl
         << "STOP" << endl << endl;

    return 0;
}

W programie zostaje utworzona zmienna x typu double. Następnie w pętli komputer nadaje jej wartość 0, po czym w kolejnych obiegach zwiększa tą zawartość o 1/10 i wyświetla wynik w oknie konsoli. Obiegi pętla wykonuje, jeśli x jest różne od 1. Analizujemy:

Obieg x przed
obiegiem
Operacja w obiegu
 1
  0
x ← x + 1/10 = 1/10
 2
 1/10
x ← x + 1/10 = 2/10
 3
 2/10
x ← x + 1/10 = 3/10
...
  ...
...
 8
 7/10
x ← x + 1/10 = 8/10
 9
 8/10
x ← x + 1/10 = 9/10
10
 9/10
x ← x + 1/10 = 10/10 = 1
11
  1
Tu powinien nastąpić
KONIEC PĘTLI

Z naszej analizy wynika, iż po 10 obiegach w zmiennej x znajdzie się wartość 1, zatem w obiegu 11 warunek kontynuacji pętli x != 1 będzie fałszywy i pętla powinna się zakończyć. Jednakże po uruchomieniu programu, pętla działa w nieskończoność. Dlaczego? Ponieważ dodawana wartość 0.1 nie jest dokładnie równa 1/10. W systemie dwójkowym liczba ta posiada nieskończone rozwinięcie: 0,0001100110011...(2), zatem zbudowana jest z nieskończonej liczby cyfr ułamkowych (podobnie jak w naszym systemie 1/3 = 0,33333333...). Mantysa liczby typu double ma ograniczoną liczbę bitów, nie może zatem zapamiętać ich nieskończonej ilości. Liczba zostanie zaokrąglona, czyli nie będzie dokładnie równa 0.1. Po wykonaniu 10 obiegów w zmiennej x znajdzie się liczba bliska 1, jednak nie dokładnie równa 1. Komputer jest bardzo precyzyjny. Porównuje x z wartością dokładnie równą 1 i oczywiście stwierdza nierówność, a więc warunek kontynuacji x != 1 będzie wciąż prawdziwy i komputer wykona kolejny obieg, a wartość w x ucieknie nam od 1 i pętla będzie wykonywana w nieskończoność.

Z tego prostego przykładu wynika bardzo ważny wniosek: liczb zmiennoprzecinkowych będących wynikiem obliczeń nie należy przyrównywać do wartości dokładnych, ponieważ w trakcie tych obliczeń mogą pojawić się błędy zaokrągleń powodujące niedokładności. Niedokładności te nie są duże, ale są i trzeba się z nimi liczyć.

Jak zatem sobie poradzić w takich przypadkach? Zamiast porównywania x z wartością dokładną, sprawdzamy, czy różnica pomiędzy x i wartością dokładną jest dostatecznie bliska zeru. Ponieważ różnica ta może być dodatnia lub ujemna, bierzemy jej wartość bezwzględną. Następnie zakładamy pewne przybliżenie zera (oznaczane najczęściej grecką literką epsilon ε) i sprawdzamy, czy wartość bezwzględna różnicy jest mniejsza od ε. Jeśli tak, to przyjmujemy, iż x jest równe wartości dokładnej z dokładnością do epsilon. Jeśli nie, to x nie jest równe wartości dokładnej:

Do wyliczenia wartości bezwzględnej potrzebujemy dostępu do funkcji matematycznych, musimy zatem dołączyć do naszego programu plik z definicjami tych funkcji o nazwie cmath. Funkcja obliczająca wartość bezwzględną ma nazwę fabs( ). Nasz program będzie teraz wyglądał następująco (przeanalizuj go dokładnie):

// Zmienne fp
//-----------

#include <iostream>
#include <iomanip>
#include <cmath>

using namespace std;

int main()
{
    double x, eps;

    eps = 0.0000001; // Przybliżenie

    cout << fixed << setprecision(2);

    cout << "START" << endl << endl;

    for(x = 0; fabs(x - 1) > eps; x += 0.1)
        cout << x << endl;

    cout << endl
         << "STOP" << endl << endl;

    return 0;
}
START

0.00
0.10
0.20
0.30
0.40
0.50
0.60
0.70
0.80
0.90

STOP

Teraz program działa zgodnie z oczekiwaniem. Co zmieniliśmy?

  1. Dołączyliśmy nowy plik nagłówkowy:
    #include <cmath>
  2. Dodaliśmy nową zmienną eps (epsilon) i nadaliśmy jej wartość:
    eps = 0.0000001;
  3. Zmieniliśmy w pętli warunek kontynuacji z:
    x != 1;
    na równoważny:
    fabs(x - 1) > eps;

Zadanie:

Napisz program obliczający pierwiastki równania kwadratowego ax2 + bx + c = 0. Wykorzystaj podany wzór sprawdzania, czy liczba zmiennoprzecinkowa jest równa wartości dokładnej. Pierwiastek kwadratowy jest funkcją matematyczną o nazwie sqr( ) wymagającą dołączenia pliku nagłówkowego cmath (plik ten wystarczy dołączyć jeden raz na początku programu korzystającego z funkcji matematycznych).

Do zapamiętania:


Na początek:  podrozdziału   strony 

Literały

Stała (ang. constant) to wartość, której w programie nie można zmienić. Ze stałymi spotkaliśmy się wielokrotnie w programach, to np. liczby bezpośrednie: 15, 107.88... itp. Stałe w takiej postaci noszą nazwę literałów (ang. literals), czyli stałych zbudowanych ze znaków. Wartość literału jest określona bezpośrednio w nim samym i nie może być zmieniona. Literały mogą być całkowite lub zmiennoprzecinkowe (innymi zajmiemy się później).

Literały całkowite dziesiętne

To normalne liczby całkowite: 12, -135, 1999, 29876...

Literały ósemkowe

W wyrażeniach języka C++ można stosować liczby ósemkowe, czyli liczby całkowite o podstawie 8. Liczba ósemkowa może składać się z cyfr od 0 do 7 i musi być poprzedzona cyfrą 0. Komputer automatycznie obliczy jej wartość i zastosuje ją w wyrażeniu. Przykłady literałów ósemkowych:

05 (= 510), 012 (= 1010), 0773 (= 50710), 0100000 (= 3276810)...

Literał 08 jest niepoprawny, ponieważ cyfra 8 nie należy do systemu ósemkowego.

Literały szesnastkowe

Literał szesnastkowy oznacza liczbę w systemie szesnastkowym. Musi się rozpoczynać od przedrostka 0x lub 0X. Liczba szesnastkowa może się składać z cyfr od 0 do 9 oraz liter a...f lub A...F. Przykłady literałów szesnastkowych:

0x5 (= 510),
0xA (= 1010),
0x1FB (= 50710),
0x8000 (= 3276810)...

Literały dwójkowe

Literał dwójkowy oznacza liczbę w systemie dwójkowym. Musi rozpoczynać się od przedrostka 0b lub 0B. W skład liczby dwójkowej mogą wchodzić tylko cyfry 0 i 1. Przykłady literałów dwójkowych:

0b101 (= 510),
0b1010 (= 1010),
0b111111011 (= 50710),
0b100000000000 (= 3276810)...

Pamiętaj, iż literał jest tylko sposobem zapisu danej wartości stałej i jego rodzaj nie wpływa na wynik obliczeń. Piszę o tym, ponieważ spotkałem się z uczniami, którzy myśleli, iż wpisanie literału np. szesnastkowego do zmiennej zmienia ją w zmienną szesnastkową (!). Mam nadzieję, iż kandydaci do matury z informatyki nie wpadają już na takie dziwne pomysły. Wewnętrznie komputer przechowuje wszystkie wartości w bitach i tylko w bitach. Jeśli użyjemy dowolnego literału w programie, to komputer i tak go zamieni wewnętrznie na wartość dwójkową. Literały są pomocą dla programisty, aby swobodnie mógł pracować np. z liczbami szesnastkowymi, jeśli ma taką potrzebę.

Uruchom poniższy program:

// Literały
//---------

#include <iostream>

using namespace std;

int main()
{
    cout << 999999 << endl   // literał dziesiętny
         << 03641077 << endl // literał ósemkowy
         << 0xF423F << endl  // literał szesnastkowy
         << 0b11110100001000111111 << endl // literał dwójkowy
         << endl << endl;

    return 0;
}
999999
999999
999999
999999

Komputer stara się dopasować literał do wyrażenia, w którym jest on używany, niemniej w pewnych sytuacjach może zajść potrzeba poinformowania komputera o typie literału. Do tego celu używamy przyrostków (umieszczamy je na końcu liczby w dowolnej kolejności):

LL/ll : liczba 64-bitowa (ang. long long)
U/u : liczba bez znaku w kodzie NBS (ang. unsigned)

// Literały
//---------

#include <iostream>

using namespace std;

int main()
{
    cout << 18446744073709551615LLU << endl
         << endl << endl;

    return 0;
}

Literały zmiennoprzecinkowe

Literał zmiennoprzecinkowy reprezentuje wartość zmiennoprzecinkową. Może występować w dwóch postaciach:

  • Ułamkowej: 1.5, -15.78, 3., .5
  • Naukowej: 2.5E3 ( = 2,5 × 103 = 2500), 1E9 (1000000000)

W postaci ułamkowej kropka oznacza przecinek dziesiętny (w krajach anglosaskich stosuje się w tym celu kropkę: ang. decimal point). Kropka rozdziela część całkowitą od ułamkowej: 12.45, -3.2...

Jeśli liczba jest całkowita, to za kropką nie trzeba umieszczać cyfr: np. 9. = (9,0).

Jeśli część całkowita wynosi 0, to przed kropką nie trzeba umieszczać cyfr (chociaż dla przejrzystości zaleca się umieścić 0): .45 (0,45).

W postaci naukowej literał składa się z dwóch części: mantysy i wykładnika: 2.22E-4 (2,22 × 10-4 = 0,000222). Mantysa jest liczbą ułamkową. Wykładnik jest liczbą całkowitą. Mantysa i wykładnik są rozdzielone literą E/e (ang. exponent).

Uruchom poniższy program:

// Literały
//---------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    cout << setprecision(4);

    cout << fixed;
    cout << setw(21) << .5 << endl
         << setw(21) << 5. << endl
         << setw(21) << .5e3 << endl
         << setw(21) << 5.555555e15
         << endl << endl;

    cout << scientific;
    cout << setw(21) << .5 << endl
         << setw(21) << 5. << endl
         << setw(21) << .5e3 << endl
         << setw(21) << 5.555555e15
         << endl << endl;

    return 0;
}
               0.5000
               5.0000
             500.0000
5555555000000000.0000

           5.0000e-01
           5.0000e+00
           5.0000e+02
           5.5556e+15

Literały zmiennoprzecinkowe są standardowo typu double (64-bity, 15 cyfr znaczących). Jeśli potrzebujesz innych typów, to musisz użyć przyrostków na końcu literału:

F/f : literał typu float (32-bity, 7 cyfr znaczących)
L/l : literał typu long double (80-bitów, 20 cyfr znaczących)

Uruchom program:

// Literały
//---------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    cout << setprecision(2) << fixed;

    cout << "Literał double     : " << 1234567890123456789.12 << endl
         << "Literał float      : " << 1234567890123456789.12F << endl
         << "Literał long double: " << 1234567890123456789.12L << endl
         << endl << endl;

    return 0;
}
Literał double     : 1234567890123456768.00
Literał float      : 1234567939550609408.00
Literał long double: 1234567890123456789.12

Do zapamiętania


Na początek:  podrozdziału   strony 

Zmienne stałe

Zmienna stała (ang. constant variable) to zmienna (czyli obiekt w pamięci), do której wprowadzono wartość, lecz wartości tej nie można w programie zmieniać. Po  co takie coś jest potrzebne? Zrozumiesz, gdy poznasz zmienne złożone. Zmienną stałą definiujemy tak samo jak zwykłą zmienną, tylko definicję poprzedzamy słówkiem kluczowym const. Wartość zmiennej stałej nadajemy w trakcie jej definicji. Potem wartości tej nie można już zmieniać. Definicja zmiennej stałej wygląda zatem następująco:

const typ nazwa = wyrażenie;

Komputer wylicza wartość wyrażenia i wynik umieszcza w zmiennej. Następnie zmienna staje się obiektem tylko do odczytu (ang. read only), tzn. możemy dowolnie używać w programie wartości tej zmiennej, ale nie możemy jej modyfikować ani zmieniać. Istnieje zasada, aby nazwy zmiennych stałych pisać dużymi literami – w ten sposób odróżniają się one od zwykłych zmiennych.

Wpisz do edytora poniższy program i spróbuj go skompilować:

// Zmienne stałe
//--------------

#include <iostream>

using namespace std;

int main()
{
    const double PI = 3.14;
    cout << PI << endl << endl;

    PI = 3.14159236; // Lepsze PI

    return 0;
}

Kompilacja nie powiodła się, ponieważ w programie jest próba wpisania do chronionej zmiennej nowej wartości. Usuń błędną linię i skompiluj program ponownie. Teraz powinno być wszystko w porządku.

Do zapamiętania


Na początek:  podrozdziału   strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2024 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.

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

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.