Programowanie w Visual C++ 2010

Spis treści

Biblioteka języka C

W naszych projektach często będziemy używać gotowych funkcji przygotowanych przez twórców języka C, prekursora C++. Stanowią one podstawę wielu programów i są znacznym ułatwieniem dla programistów. Łatwiej jest bowiem tworzyć aplikacje, korzystając z już istniejących, gotowych i dobrze przetestowanych rozwiązań, niż tworząc wszystkie od nowa. W niniejszym samouczku omówimy wybrane, najbardziej przydatne funkcji zawarte w tzw. bibliotece języka C.

Biblioteka to inaczej zbiór funkcji o określonym działaniu, gotowych do użycia w innych programach.

Omówimy pokrótce najważniejsze elementy następujących podbibliotek biblioteki języka C:

  1. <cmath> – funkcje matematyczne,
  2. <cstdlib> – generowanie liczb pseudolosowych,
  3. <cassert> – asercje,
  4. <ctime> – pomiar czasu.
Żeby wypożyczyć książkę w bibliotece, potrzebujesz karty członkowskiej – musisz zatem zgłosić chęć przynależenia do grona czytelników. Analogicznie, aby skorzystać z funkcji należącej do danej biblioteki, musisz zasygnalizować, że chcesz z niej korzystać. W tym celu należy użyć dyrektywy #include. Przykładowo, chcąc użyć w którymś z plików źródłowych (.cpp) swojego programu dowolnej funkcji z biblioteki <cmath>, musisz załączyć odpowiedni plik nagłówkowy:
#include <cmath>

cmath

...czyli to, co tygryski lubią najbardziej. ;) Jak sama nazwa wskazuje, <cmath> to biblioteka funkcji matematycznych. Po jej załączeniu możesz do woli pierwiastkować, potęgować, zaokrąglać liczby czy obliczać funkcje trygonometryczne. Jak się okaże, jest to całkiem przydatne narzędzie dla Młodego (Przyszłego) Matematyka. Zatem – do rzeczy!

Marzy Ci się pierwiastkowanie lub potęgowanie? Nie zapomnij o dyrektywie:
#include <cmath>

Funkcje trygonometryczne

Poniższe funkcje z biblioteki <cmath> wyznaczają wartości dobrze nam znanych, wybranych funkcji trygonometrycznych:

Funkcja Działanie
double sin(double a);
oblicza sinus kąta \(\alpha\) (danego w radianach)
double cos(double a);
oblicza cosinus kąta \(\alpha\) (danego w radianach)
double tan(double a);
oblicza tangens kąta \(\alpha\) (danego w radianach)

Każda z wymienionych funkcji jako argument przyjmuje zmienną rzeczywistą typu double oznaczającą kąt w radianach.

Przydatna może się zatem okazać funkcja przeliczająca stopnie na radiany. Nazwijmy ją deg2rad() (ang. degrees-to-radians).

double deg2rad(double a)
{
       return a * 3.141592653589793238462643/180.0;
}
Spróbuj napisać analogiczną funkcję
double rad2deg(double x)
przeliczającą radiany na stopnie. Do sprawdzenia jej poprawności możesz wykorzystać funkcję deg2rad().
Pamiętajmy, że działania na liczbach "rzeczywistych" na komputerze są obarczone pewnym błędem, związanym nie tylko z własnościami arytmetyki zmiennopozycyjnej ale i faktem, iż niektóre funkcje wyznaczają wartości w pewnym przybliżeniu. I tak, choć wiemy, iż \( \sin(\pi) = 0, \) to uruchomienie programu:
#include <cmath>
#include <iostream>
using namespace std;

int main()
{
   const double pi = 3.141592653589793238462643;
   cout << sin(pi) << endl;
   return 0;
}
spowoduje wyświetlenie wyniku 1.22465e-016, czyli tylko "prawie" zera (0.000000000000000122465)!

Nietrudno zauważyć, że w powyższym wykazie brakuje funkcji cotangens. Korzystając z definicji, można jednak ją natychmiast samodzielnie zaimplementować w razie potrzeby:

double cotan(double a)
{
       return 1.0/tan(a);
}

Biblioteka <cmath> zawiera również implementacje funkcji odwrotnych do trygonometrycznych. Zwracają one wynik w radianach. Dostęp do nich otrzymujemy poprzez dodanie przedrostka a- do nazw funkcji z powyższej tabeli (asin(), acos(), atan()).

Funkcja Działanie
double asin(double x);
oblicza arcus sinus \(x\in[-1,1];\)
wynik w przedziale \([-\pi/2,\pi/2]\)
double acos(double x);
oblicza arcus cosinus \(x\in[-1,1];\)
wynik w przedziale \([0,\pi]\)
double atan(double x);
oblicza arcus tangens \(x;\)
wynik w przedziale \([-\pi/2,\pi/2]\)
double atan2(double y,
             double x);
oblicza arcus tangens \(y/x;\)
wynik w przedziale \([-\pi,\pi]\)
Zastanów się, dlaczego twórcy biblioteki <cmath> zdecydowali się zaimplementować aż dwie funkcje wyznaczające wartość arcusa tangensa. W jakich przypadkach można je zastosować?

Funkcje potęgowe

W bibliotece <cmath> zostały także zaimplementowane dwie funkcje potęgowe.

Funkcja Działanie
double pow(double x,
           double y);
oblicza wartość \(x^y\)
double sqrt(double x);
oblicza wartość \(\sqrt{x}\)
Z operacją potęgowania często kojarzony jest operator ‘ ˆ ’. Tak jest w istocie na przykład w języku R bądź Pascal. Jednakże w C++ spełnia on funkcję bitowego operatora różnicy symetryczynej (alternatywy wykluczającej). Staraj się o tym nie zapominać.

Oczywiście pamiętamy, że \[ \root{n}\of{x}=x^\frac{1}{n}. \]

Zaimplementuj funkcję
double root(double x, double n);
obliczającą pierwiastek n-tego stopnia z liczby x.
Zwróćmy uwagę, że może wydawać się kuszące wyznaczanie pierwiastka kwadratowego liczby \(x\) za pomocą wywołania pow(x, 0.5); Nie jest to jednak najlepszy pomysł – pow() jest funkcją ogólniejszą niż sqrt(), przez co możemy spodziewać, że będzie wykonywać się wolniej. Tak jest w istocie. Zbadamy to zjawisko eksperymentalnie w dalszej części niniejszego samouczka. Podobnie rzecz ma się przy podnoszeniu liczby do kwadratu – lepiej skorzystać wtedy z operatora *.

Funkcje logarytmiczne i wykładnicza

Funkcja Działanie
double log(double x);
           
zwraca logarytm naturalny z liczby x
double log10(double x);
           
oblicza logarytm dziesiętny z liczby x
double exp(double x);
           
zwraca wartość stałej e podniesionej do potęgi x

Zauważmy, że funkcja exp() (ang. expotential function) jest odwrotna do funkcji log(). Z kolei funkcję odwrotną do log10() uzyskamy, korzystając z już nam znanej funkcji pow():

double ten2n(double x)
{
       return pow(10, x);
}

Jak zapewne zauważyłeś/aś, nie została wymieniona funkcja obliczająca logarytm o dowolnej podstawie. Potrafisz sobie jednak poradzić z tym problemem. Wskazówką jest dobrze wszystkim znana tożsamość zmiany podstawy logarytmu: \[ \log_a x=\frac{\log_b x}{\log_b a}. \]

Zaimplementuj funkcję
double log_a(double a, double x)
obliczającą logarytm o podstawie a z liczby x.

Od podłogi aż po sufit, czyli co tu się jeszcze mieści...

Oto garść pozostałych funkcji z biblioteki <cmath>, z którymi zaprzyjaźnimy się w tym samouczku:

Funkcja Działanie
double ceil(double x);
            
zaokrągla w górę liczbę x (sufit)
double floor(double x);
            
zaokrągla w dół liczbę x (podłoga)
double fabs(double x);
            
zwraca wartość bezwzględną liczby x

Funkcja ceil()(ang. ceiling – sufit) zwraca najmniejszą liczbę całkowitą większą bądź równą wartości argumentu, natomiast funkcja floor()(ang. floor – podłoga) – największą liczbę całkowitą nie większą niż argument.

Zauważmy, że wartość funkcji floor(x) jest równa wartości będącej wynikiem rzutowania zmiennej x na typ int. Jaka jest zatem różnica pomiędzy wynikami poniższych operacji?
cout << floor(5.22);
cout << (int)5.22;
Otóż wynikowe wartości różnią się typem. Pierwsza z wypisanych liczb jest typu double, natomiast druga z nich to liczba typu int.
Spróbuj dla ćwiczenia samodzielnie napisać funkcję
double MyAbs(double x)
zwracającą wartość bezwzględną argumentu.

cassert

Biblioteka <cassert> zawiera funkcję:

void assert(int expression);

Stosuje się ją, by przerwać działanie programu, jeśli warunek expression nie zachodzi (tzn. jest równy 0, false). W ten sposób zapewniamy zachodzenie tego warunku dla reszty programu znajdującej się po użyciu asercji. Jest ona bardzo przydatna na etapie testowania programów.

Nagłe zakończenie programu w przypadku programów komercyjnych jest bardzo nieelegancką (i często wręcz niedopuszczalną) metodą radzenia sobie z niespodziewanymi sytuacjami, np. zerwaniem połączenia z internetem. W takich przypadkach implementuje się różnego rodzaju mechanizmy pozwalające na racjonalne reagowanie na niekorzystne wydarzenia (np. za pomocą tzw. wyjątków, które poznamy na II semestrze). Jednakże istnieje pewna grupa błędów, które z założenia nigdy nie powinny się wydarzyć, np. te spowodowane pomyłką programisty. W takich przypadkach, zanim program trafi do użytkownika końcowego (gdy programiści jeszcze testują program przed oficjalnym wydaniem), stosowanie asercji jest bardzo wygodne.

Dobrze będzie prześledzić jakiś przykład. Jak wiemy, w dziedzinie liczb rzeczywistych funkcja pierwiastek jest nieokreślona dla liczb ujemnych. Oto funkcja, która nie dopuszcza pierwiastkowania w takich sytuacjach.

double mySqrt(double x)
{
   assert(x>=0);
   return sqrt(x);
}

Korzystając z mySqrt() mamy pewność, że wartość zwrócona jest poprawną liczbą rzeczywistą, a nie – być może – NaN (nie-liczbą).

Pamiętaj, żeby jako argument dla assert() nie podawać wyrażenia przypisującego wartość lub zmieniającego stan programu, np.
assert(x=0);
Doprowadzi to do nieustannego wykrywania błędu dla dowolnych wartości zmiennej x, a co za tym idzie – uniemożliwi wykonanie programu.

ctime

Biblioteka <ctime> zawiera funkcje związane z obsługą czasu i daty. Jedną z nich jest:

clock()

Funkcja ta zwraca liczbę całkowitą reprezentującą liczbę "tyknięć" wirtualnego zegara, jakie upłynęły od uruchomienia programu.

Chcąc zatem obliczyć, ile sekund minęło od uruchomienia programu, należy tę wartość zwracaną przez clock() podzielić przez liczbę "tyknięć", jakie zegar wykonuje w ciągu jednej sekundy, przechowywanych w stałej CLOCKS_PER_SEC. Funkcja obliczająca czas działania programu w sekundach będzie więc wyglądała następująco:

double upTime()
{
       return (double)clock()/(double)CLOCKS_PER_SEC;
}
 

Jednym z ważniejszych zastosowań funkcji clock() jest obliczenie czasu trwania poszczególnych fragmentów programu. Sprawdźmy, jak długo będzie działał program wypisujący na konsolę sto kropek:

int c = clock();
for(int i=0; i<100; i++)
        cout<<".";
cout<<endl;
cout<<(double)(clock()-c)/CLOCKS_PER_SEC<<endl;
 

Okazuje się, że bardzo krótko:

Przykładowy zrzut ekranu

Dzięki temu możemy także porównywać wydajność różnego rodzaju operacji. Przypomnijmy, że zastanawialiśmy się wcześniej, w jaki sposób lepiej jest wyznaczać wartość pierwiastka z danej liczby rzeczywistej. Sprawdźmy!

#include <ctime>
#include <cmath>
#include <iostream>
using namespace std;


/* Które działanie szybsze? pow(x,0.5), czy sqrt(x)?
 * Żeby czymś zająć komputer, będziemy sumować pierwiastki z różnych
 * wartości. Zwróćmy uwagę, że wynik będzie zależny od szybkości komputera,
 * możesz uzyskać inne wyniki niż Twój kolega/koleżanka.
 * Co więcej, aby pomiar był wiarogodny, należy uruchomić program kilkakrotnie
 * i uśrednić uzyskane wyniki (np. z 3 bądź 5 prób).
 */

int main()
{
   int n = 10000000; // dużo razy liczymy, oj dużo...
   double x = 2.0;
   double s;


   s = 0.0;
   int t0pow = clock(); // czas start!
   for (int i=0; i<n; ++i)
      s += pow(x+s, 0.5);
   int t1pow = clock(); // czas stop!

   cout << "pow(,0.5) - " << double(t1pow-t0pow)/CLOCKS_PER_SEC;
   cout << " s." << endl;


   s = 0.0;
   int t0sqrt = clock(); // czas start!
   for (int i=0; i<n; ++i)
      s += sqrt(x+s);
   int t1sqrt = clock(); // czas stop!

   cout << "sqrt()    - " << double(t1sqrt-t0sqrt)/CLOCKS_PER_SEC;
   cout << " s." << endl;

   return 0;
}
 

Na moim komputerze wynik jest następujący (zgodnie z intuicją):

pow(,0.5) - 0.83 s.
sqrt()    - 0.168 s.
Inną funkcją odmierzającą czas jest time(). Wywołujemy ją najczęściej tak:
int czas = time(0);
W ten sposób zwrócony zostaje aktualny znacznik czasu systemowego. Jeśli spróbujesz go wyświetlić, nie będzie przypominał "ludzkiej" daty. Pod tą zagadkową wartością kryje się liczba sekund, które upłynęły od 1 stycznia 1970 roku od godziny 00:00. Jest to data historyczna, znana jako początek epoki Unixa. Funkcja time() przyda się nam za chwilę.
Korzystając z wyżej wymienionych funkcji, nie zapomnij o dodaniu biblioteki <ctime>:
#include <ctime>

cstdlib

<cstdlib> jest biblioteką udostępniającą funkcje dotyczące dynamicznej alokacji pamięci (w stylu języka C), konwersji typów, itd. Większość z nich nie będzie nam potrzebna (bądź w języku C++ istnieją lepsze sposoby dokonywania tego typu operacji). Nas najbardziej interesować będzie zagadnienie generowania liczb pseudolosowych.

Aby wykorzystać funkcje opisane w tym paragrafie, załącz bibliotekę <cstdlib>, używając dyrektywy
#include <cstdlib>

Generowanie liczb pseudolosowych

W tej sekcji mamy aż dwie funkcje: :)

Funkcja Działanie
int rand();
generuje całkowitą liczbę pseudolosową z przedziału [0; RAND_MAX]
void srand(unsigned int
           seed);
inicjuje generator liczb pseudolosowych
RAND_MAX jest stałą z biblioteki <cstdlib> określającą maksymalną wartość zwracaną przez rand() . Wartość ta jest zależna m.in. od kompilatora. Na przykład, na moim komputerze wynosi 32767.

W większości przypadków wartość zwracaną przez funkcję rand() będziemy przekształcać, tak by uzyskać liczby pseudolosowe z zadanych przedziałów. Przyjrzyjmy się dwóm poniższym funkcjom.

double randDouble(double a, double b)
{
   assert(b>a);
   double w = (double)rand()/(double)RAND_MAX; // liczba z przedziału [0,1]
   return w*(b-a)+a; // a teraz z [a,b]
}
double randInt(int a, int b)
{
   double w = (double)rand()/(double)(RAND_MAX+1); // liczba z przedziału [0,1)
   w = w*(b-a+1)+a; // a teraz z [a,b+1)
   return (int)(w); // funkcja podłoga!
}

Wywołanie funkcji randDouble(a,b) zwraca liczbę pseudolosową rzeczywistą z rozkładu jednostajnego na przedziale [a,b], tzn. takiego rozkładu, dla którego (intuicyjnie) prawdopodobieństwo wylosowania każdej liczby jest takie samo. Podobnie z wywołaniem randInt(a,b) z tym, że w tym przypadku "losujemy" liczby ze zbioru {a,a+1,...,b}.

W przypadku chęci losowania liczb np. ze zbioru {0,1,2,3,4,5} rzecz jasna kuszącym sposobem jest próba wywołania:
int liczba = rand() % 6;
Jednakże, można udowodnić, że tak "wylosowana" liczba jest w pewnym sensie mniej "losowa" niż wartość uzyskana przez wywołanie randInt(0,5).
Jeżeli jeszcze tego nie zrobiłeś, skopiuj nasze generatory liczb do swojego testowego programu, wzbogać go o wyświetlanie wygenerowanej liczby i spróbuj kilkakrotnie ten program uruchomić.

Ojej, coś jest chyba nie tak! Przy każdym uruchomieniu programu "losowana" liczba ma taką samą wartość! Spokojnie, sytuacja jest pod kontrolą. Algorytm losujący naszą liczbę jest tylko deterministycznym wzorem matematycznym, a co za tym idzie   ma określony i przewidywalny sposób działania.

Więcej o podstawowych własnościach komputerowych generatorów liczb pseudolosowych (i dlaczego nie powinniśmy ich nazywać losowymi) dowiesz się na odpowiednim wykładzie.

Co zatem uczynić, aby generowanie bardziej przypominało prawdziwe losowanie, tzn. wyglądało na nieprzewidywalne? Z pomocą przychodzi nam kolejna funkcja:

void srand(unsigned int seed);

Parametr seed (od ang. ziarno) to liczba inicjująca generator liczb pseudolosowych. Na jej podstawie rand() dokonuje swych obliczeń. Jak nietrudno się domyślić lub zauważyć na drodze doświadczeń, dla każdej możliwej wartości ziarna wygenerowany zostanie inny ciąg.

Teraz już wiemy, że jeśli przy każdym uruchomieniu programu pragniemy uzyskać inne zachowanie się generatora liczb pseudolosowych, powinniśmy dostarczać różne argumenty dla funkcji srand(). Co w takim razie zmienia się pomiędzy poszczególnymi uruchomieniami programu? Jak zapewne się domyślasz, chodzi o czas systemowy. :) Spróbujmy w takim razie pouruchamiać "naprawiony" generator (tzn. taki, który inicjujemy aktualną datą):

srand(time(0));
cout << rand() << endl;

Teraz wszystko działa tak, jak powinno – za każdym razem otrzymujemy "nieprzewidywalną" liczbę (a przynajmniej takie nasz program stwarza pozory).

Pamiętaj, że wykorzystując time(0), korzystasz z biblioteki <ctime>.
Zwróć uwagę, że lepiej jest nie korzystać z funkcji clock() do inicjowania generatora. Jest bowiem całkiem prawdopodobne, iż program przy kolejnych uruchomieniach będzie wykonywał się w dokładnie takim samym tempie.
Kiedy najdzie Cię ochota, by pograć w "Chińczyka", a wszystkie kostki gdzieś się zawieruszą, może przydać Ci się symulator rzutu kostką sześcienną. Spróbuj go zaimplementować.
Dzięki liczbom pseudolosowym możesz zasymulować zachowanie nie tylko kostki do gry planszowej. Znajdują one szerokie zastosowanie w wielu innych grach – służą do generowania unikalnych map w grach strategicznych, losowego rozmieszczania przeciwników w grach zręcznościowych czy tworzenia efektów deszczu, ognia bądź kałuży krwi. Symulacje stanowią także nieodłoczny element wielu badań z dziedziny matematyki, np. w statystyce.

Podsumowanie

Z tak potężnym arsenałem funkcji możesz już wiele zdziałać. Ostatni rzut oka na wykaz poznanych funkcji i czas ruszać do boju:

Nazwa Wartość zwracana
<cmath> double sin(double a) sinus kąta a [rad.]
double cos(double a) cosinus kąta a [rad.]
double tan(double a) tangens kąta a [rad.]
double asin(double x) arcus sinus x
double acos(double x) arcus cosinus x
double atan(double x) arcus tangens x
double atan2(double y, double x) arcus tangens y/x
double pow(double x, double y) \( x^y \)
double sqrt(double x) \( \sqrt{x} \)
double log(double x) \( \ln x \)
double log10(double x) \( \log_{10} x \)
double exp(double x) \( e^x \)
double ceil(double x) \( \lceil x \rceil \)
double floor(double x) \( \lfloor x \rfloor \)
double abs(double x) \( \left| x \right| \)
<cassert> void assert(int expr) brak (asercja – sprawdza, czy warunek expr jest prawdziwy)
<ctime> clock_t clock() liczba "tyknięć" zegara od uruchomienia programu
time_t time(time_t* t) aktualny czas systemowy
<cstdlib> void srand(unsigned int s) inicjuje generator liczb pseudolosowych
int rand() generuje liczbę pseudolosową
Zarówno dokładniejsze opisy funkcji wymienionych, jak i opisy pozostałych funkcji z powyższych bibliotek znajdziesz w angielskojęzycznej dokumentacji technicznej języka C++.
CC By 3.0
Copyright © 2011-2016 by Jowita Hącia [Last update: 2017-02-01 17:17:05]
This work is licensed under a Creative Commons Attribution 3.0 Unported License