Temat:
Operatory
1. Operatory arytmetyczne
Operatory arytmetyczne służą do tworzenia wyrażeń arytmetycznych. W poniższej tabeli zestawiono operatory arytmetyczne:
Operator |
Działanie |
Przykład |
+ |
dodawanie |
a = b + c; |
- |
odejmowanie |
a = b - c; |
* |
mnożenie |
a = b * c; |
/ |
dzielenie |
a = b / c; |
% |
reszta z dzielenia (modulo) |
a = b % c; |
Wszystkie operatory za wyjątkiem operatora % można stosować zarówno do argumentów całkowitych, jak i zmiennoprzecinkowych. Operatory + i - można również stosować jako operatory jednoargumentowe. Jeśli przy dzieleniu liczb całkowitych iloraz zawiera część ułamkową, jest ona odrzucana.
Przykłady:
25 / 7 → 3;
25 / 7. → 3.571428
35. / 5 → 7.0
1 / 4 → 0
19 % 6 → 1
0 % 5 → 0
18 % 6 → 0
#include <iostream.h>
#include <conio.h>
main ()
{
int i;
clrscr ();
for (i = 0; i < 64; i = i+1)
{
if (i % 8)
cout << "\t"; // wyprowadzenie tabulatora
else
cout << "\n"; // przejście do nowej linii
cout << i; // wyświetlenie aktualnej wartości i
}
return 0;
}
2. Operatory relacji
Wszystkie operatory relacji są dwuargumentowe. Jeśli relacja jest prawdziwa, to jej wartością jest 1; w przeciwnym przypadku wartością relacji jest 0. Poniżej zestawiono operatory relacji:
Operator |
Działanie |
Przykład |
< |
mniejszy |
a < b |
<= |
mniejszy lub równy |
a <= b |
> |
większy |
a > b |
>= |
większy lub równy |
a >= b |
== |
równy |
a ==b |
!= |
nie równy |
a != b |
Argumenty operatorów relacji muszą być typu arytmetycznego lub wskaźnikowego.
3. Operatory logiczne
W poniższej tabeli zestawiono operatory logiczne:
Operator |
Działanie |
Przykład |
! |
negacja |
! a |
&& |
koniunkcja (iloczyn logiczny) |
a && b |
|| |
alternatywa (suma logiczna) |
a || b |
Wyrażenia połączone dwuargumentowymi operatorami logicznymi koniunkcji i alternatywy zawsze są wartościowane od strony lewej do prawej. Kompilator oblicza wartość wyrażenia dotąd, dopóki na pewno nie wie jaki będzie wynik. Oznacza to, że w wyrażeniu
( a == 0 ) && ( m == 5 ) && ( x > 23 )
kompilator będzie obliczał od lewej do prawej, a jeśli pierwszy czynnik koniunkcji nie będzie prawdziwy, dalsze obliczanie zostanie przerwane.
4. Operatory bitowe
Język C++ oferuje sześć bitowych operatorów logicznych, które interpretują argumenty operacji jako uporządkowany ciąg bitów. Każdy bit może przyjmować wartość 1 lub 0. Argumenty tych operacji muszą być całkowite, a więc typu char, short, int i long, zarówno bez znaku, jak i ze znakiem. Ze względu na różnice pomiędzy reprezentacjami liczb ze znakiem w różnych implementacjach, zaleca się używanie operandów bez znaku.
Poniżej przedstawiono zestawienie bitowych operatorów logicznych:
Operator |
Działanie |
Przykład |
& |
bitowa koniunkcja |
a = b & c; |
| |
bitowa alternatywa |
a = b | c; |
^ |
bitowa różnica symetryczna |
a = b ^ c; |
<< |
przesunięcie w lewo |
a = b << c; |
>> |
przesunięcie w prawo |
a = b >> c; |
~ |
bitowa negacja |
a = ~b |
Zilustrujmy działanie bitowych operatorów logicznych na następującym przykładzie:
int m = 0x0f0f;
int k = 0x0ff0;
int a, b, c, d;
a = m & k; // iloczyn bitowy
b = m | k; // suma bitowa
c = ~m; // negacja bitowa
d = m ^ k; // różnica symetryczna (XOR)
Dane wejściowe mają następujące wartości w układzie binarnym:
m 0000 1111 0000 1111
k 0000 1111 1111 0000
Stąd:
m & k 0000 1111 0000 0000 bitowa koniunkcja
m | k 0000 1111 1111 1111 bitowa alternatywa
~m 1111 0000 1111 0000 bitowa negacja
m ^ k 0000 0000 1111 1111 bitowa różnica symetryczna
Jaka jest różnica pomiędzy operatorami logicznymi a bitowymi? Wynikiem zwykłego operatora logicznego jest „prawda” lub „fałsz”, czyli słowo, w którym zapisana jest wartość 1 lub 0. Natomiast operatory bitowe działają na poszczególnych bitach argumentów. Wynikowy układ bitów jest liczbą, dlatego operatory bitowe przypominają operacje arytmetyczne.
Operator przesunięcia w lewo << jest to dwuargumentowy operator pracujący na argumentach typu całkowitego:
zmienna << ile_miejsc
Bierze on wzór bitów zapisany w danej zmiennej, przesuwa go o zadaną liczbę miejsc w lewo i jako wynik zwraca ten nowy wzór. Bity z prawego brzegu słowa uzupełnione zostają zerami. Bity z lewego brzegu zostają zgubione.
Przykład:
int a = 0x40f2;
int w;
w = a << 3;
a 0100 0000 1111 0010
w 0000 0111 1001 0000
Przesunięcie w prawo >> jest to dwuargumentowy operator pracujący na argumentach typu całkowitego:
zmienna >> ile_miejsc
Bierze on wzór bitów zapisany w danej zmiennej, przesuwa go o żądaną liczbę miejsc w prawo i jako wynik zwraca ten nowy wzór. Bity z prawego brzegu wychodzące poza zakres słowa są gubione. Sposób uzupełniania bitów z lewej strony zależy od komputera.
Jeśli operator >> pracuje na danej typu unsigned lub signed, ale dana jest liczbą nieujemną, wówczas bity z lewego brzegu są uzupełniane zerami. Przykładowo:
unsigned int d = 0x0ff0;
unsigned int r;
r = d >> 3;
d 0000 1111 1111 0000
r 0000 0001 1111 1110
Jednakże, jeśli operator >> pracuje na danej typu signed i jest tam liczba ujemna, to wynik zależy od typu komputera. Przykładowo”
signed int d = 0xff00;
signed int r;
r = d >> 3;
d 1111 1111 0000 0000
r 0001 1111 1110 0000
r' 1111 1111 1110 0000 IBM PC
5. Pozostałe operatory przypisania
Instrukcja przypisania:
a = a << 3;
która powoduje przesunięcie wartości zmiennej a o 3 pozycje w lewo, a następnie przypisanie wyniku do a.
Instrukcję tę można przepisać w następującej postaci:
a <<= 3;
Pokazaną wyżej postać operatora przypisanie stosuje się w przypadku, gdy lewa strona instrukcji przypisania jest powtarzana po prawej. Ponieważ podobne przypadki występują w programach bardzo często, w języku C++ wprowadzono całą rodzinę dwu- i trzy-znakowych operatorów przypisania, zestawionych w poniższej tablicy:
Operator |
Zapis skrócony |
Zapis rozwinięty |
+= |
a += b; |
a = a + b; |
-= |
a -= b; |
a = a - b; |
*= |
a *= b; |
a = a * b; |
/= |
a /= b; |
a = a / b; |
%= |
a %= b; |
a = a % b; |
<<= |
a <<= b; |
a = a << b; |
>>= |
a >>= b; |
a = a >> b; |
&= |
a &= b; |
a = a & b; |
|= |
a |= b; |
a = a | b; |
^= |
a ^= b; |
a = a ^ b; |
Analogia ta nie jest jednak zupełna: jeśli a jest wyrażeniem, to w zapisie skróconym jest ono obliczane tylko raz, natomiast w zapisie rozwiniętym 2 razy.
6. Wyrażenie warunkowe
Jest to wyrażenie, które w zależności od spełnienia lub niespełnienia warunku przyjmuje jedną z dwóch postaci:
( warunek ) ? wartość1 : wartość2
Przykładowo:
( i > 5) ? 15 : 20
Jeśli warunek jest spełniony, to wyrażenie przyjmuje wartość 15, natomiast jeśli warunek nie jest spełniony, to wyrażenie przyjmuje wartość 20.
Jest to bardzo wygodna konstrukcja, ponieważ pozwala zapakować ją do wnętrza innych instrukcji, np:
c = ( x > y ) ? 17 : 56;
Oto prosty przykład:
#include <iostream.h>
#include <conio.h>
main ()
{
int a;
clrscr ();
cout << "Musisz odpowiedzieć TAK lub NIE \n"
<< "jeśli TAK, to napisz 1 \n"
<< "jeśli NIE, to napisz 0 \n"
<< "Rozumiesz? Odpowiedz: ";
cin >> a;
cout << "Odpowiedziałeś: "
<< (a? "TAK" : "NIE")
<< " prawda?" << endl;
return 0;
}
7. Operator sizeof
Operator sizeof pozwala nam rozpoznać zachowania kompilatora i komputera, z którymi przyszło nam pracować. Jest to ważne z dwóch powodów:
Te same typy obiektów (np. zmiennych) mogą mieć w różnych implementacjach różne wielkości.
C++ pozwala użytkownikowi na definiowanie własnych typów obiektów. Często ważne jest, by wiedzieć ile pamięci zajmuje zdefiniowany obiekt.
Operator sizeof ma następującą składnię:
sizeof (nazwa_typu)
albo
sizeof (nazwa_obiektu)
Oto przykład zastosowania:
#include <iostream.h>
#include <conio.h>
main ()
{
int mm;
clrscr ();
cout << "Godzina prawdy. W tym komputerze "
<< "poszczególne typy\n"
<< "mają następujące rozmiary w bajtach: \n";
cout << "typ char: \t" << sizeof(char) << endl;
cout << "typ int: \t" << sizeof(int) << endl;
cout << "typ short: \t" << sizeof(short) << endl;
cout << "typ long: \t" << sizeof(long) << endl;
cout << "typ float: \t" << sizeof(float) << endl;
cout << "typ double: \t" << sizeof(double) << endl;
cout << "typ long double: \t" << sizeof(long double) << endl;
cout << "Nasz obiekt lokalny mm ma rozmiar: "
<< sizeof(mm) << endl;
return 0;
}
8. Operator rzutowania
Operator rzutowania umożliwia przekształcenie typu obiektu. Działa on w ten sposób, że bierze obiekt jakiegoś typu i jako wynik zwraca obiekt innego typu. Operator ten może mieć jedną z dwóch postaci:
(nazwa_typu) obiekt
lub
nazwa_typu (obiekt)
Przykład:
int a = 0xffff;
char b;
b = (char) a;
9. Operator - przecinek
Jeśli kilka wyrażeń stoi obok siebie oddzielone przecinkiem, to ta całość jest także wyrażeniem, którego wartością jest wartość wyrażenia znajdujące się z prawej strony. Zatem wartością wyrażenia:
(2 + 4, a * 4, 3 < 6, 77 + 2) jest 79.
Poszczególne wyrażenia obliczane są od lewej do prawej.
10. Priorytety operatorów
W poniższej tabeli przedstawiono priorytety operatorów w kolejności od najwyższego do najniższego:
Priorytet |
Operator |
Działanie |
15 |
[ ] ( ) ( ) |
element tablicy wywołanie funkcji nawias w wyrażeniach |
14 |
sizeof ++ -- ! - + ( ) |
rozmiar obiektu lub typu inkrementacja dekrementacja negacja jednoargumentowy minus jednoargumentowy plus konwersja typu (rzutowanie) |
13 |
* / % |
mnożenie dzielenie modulo |
12 |
+ - |
dodawanie odejmowanie |
11 |
<< >> |
przesunięcie w lewo przesunięcie w prawo |
10 |
< <= > >= |
mniejsze nie większe większe nie mniejsze |
9 |
== != |
równe nie równe |
8 |
& |
iloczyn bitowy |
7 |
^ |
bitowa różnica symetryczna |
6 |
| |
bitowa suma |
5 |
&& |
koniunkcja |
4 |
|| |
alternatywa |
3 |
?: |
arytmetyczne if |
2 |
= *= /= %= += -= <<= >>= &= |= ^= |
zwykłe przypisanie mnóż i przypisz dziel i przypisz modulo i przypisz dodaj i przypisz odejmij i przypisz przesuń w lewo i przypisz przesuń w prawo i przypisz koniunkcja i przypisz alternatywa i przypisz xor i przypisz |
1 |
, |
przecinek |
Funkcje
Jedną z najważniejszych cech nowoczesnych języków programowania jest to, że można w nich posługiwać się podprogramami.
Jeśli napiszemy podprogram realizujący operację obliczenia pola koła na podstawie zadanego promienia - to tak, jakbyśmy język programowania wyposażyli w nową instrukcję umiejącą właśnie to obliczać.
Podprogram, który jako wynik zwraca pojedynczą wartość, nazywamy funkcją. W języki C++ wszystkie podprogramy nazywane są funkcjami.
Rozpatrzmy następujący przykład:
#include <iostream.h>
#include <conio.h>
int kukulka (int ile); //1
/*********************************************************************/
main ()
{
int m = 20;
clrscr ();
cout << "Zaczynamy" << endl;
m = kukulka (5); //2
cout << "\nNa koniec m = " << m; //3
return 0;
}
int kukulka (int ile) //4
{ //5
int i;
for (i = 0; i < ile; i++)
{
cout << "Ku-ku! ";
}
return 77; //6
} //7
Funkcja ma swoją nazwę, która ją identyfikuje. Wszelkie nazwy - przed pierwszym odwołaniem się do nich - muszą być zadeklarowane. W tym miejscu programu widzimy deklarację funkcji, która mówi kompilatorowi, że kukulka jest funkcją wywoływaną z argumentem typu int, a zwracającą jako wynik wartość typu int. Przed odwołaniem się do nazwy wymagana jest jej deklaracja. Deklaracja, ale niekoniecznie definicja. Sama funkcja może być zdefiniowana później, nawet w zupełnie innym pliku. Zdefiniować funkcję, to znaczy poprostu napisać jej treść.
Wywołanie funkcji, to napisanie jej nazwy wraz z listą argumentów przesyłanych do funkcji, ujętych w nawiasy okrągłe. Ponieważ funkcja ma zwrócić wartość, przypisujemy ją do zmiennej m.
Na dowód tego, że funkcja zwróciła jakąś wartość i że nastąpiło przypisanie tej wartości do obiektu m - wypisujemy go w tym miejscu na ekran.
Tu się zaczyna definicja funkcji. Dwie klamry 5 i 7 określają obszar ciała funkcji, czyli jej treść.
6. Jest to moment, w którym funkcja kończy swoją pracę i wraca do miejsca skąd została wywołana. Obok słowa return znajduje się wartość, którą zdecydowaliśmy zwrócić jako wynik wykonania tej funkcji.
Oto kilka przykładów deklaracji funkcji:
float kwadrat (int bok);
void fun (int stopien, char znak, int nachlenie);
int przypadek (void);
char znak_x( );
void funk (...);
kwadrat jest funkcją, wywoływaną z jednym argumentem typu int, która zwraca wartość typu float;
fun jest funkcją wywoływaną z 3 argumentami typu: int, char, int, która nie zwraca żadnej wartości. Słowo void służy tu właśnie do zaznaczenia tego faktu;
przypadek jest funkcją, która wywoływana jest bez żadnego argumentu, a która zwraca wartość typu int;
znak_x jest to funkcja, która wywoływana jest bez żadnych argumentów, a która zwraca wartość typu char;
funk jest to funkcja, którą wywołuje się z bliżej nieokreślonymi jeszcze argumentami, a która nie zwraca żadnej wartości.
Deklaracja f( ) oznacza w C++ brak jakichkolwiek argumentów, czyli to samo co f (void).
Nazwy argumentów umieszczone w nawiasach przedstawionych deklaracji są nieistotne dla kompilatora i można je pominąć. Nazwy ale nie typy argumentów. Dlatego deklarację funkcji:
void fun (int stopien, char znak, int nachylenie);
można napisać także jako:
void fun (int, char, int);
To dlatego, że w deklaracji powiadamiamy kompilator o liczbie i typie argumentów. Ich nazwy nie są w tym momencie istotne.
1. Zwracanie wyniku przez funkcje
W przykładowym programie funkcja była wywoływana z argumentem i zwracała jakąś wartość. Przyjrzyjmy się bliżej mechanizmowi przekazywania wyników.
Oto przykład programu liczącego potęgi danej liczby:
#include <iostream.h>
#include <conio.h>
long potega (int stopien, long liczba);
/***************************************************************/
main ()
{
int pocz, koniec;
clrscr ();
cout << "Program na obliczenie potęgi liczb całkowitych\n"
<< "z zadanego przedziału \n"
<< "Podaj początek przedziału: ";
cin >> pocz;
cout << "Podaj koniec przedziału: " ;
cin >> koniec;
cout << endl;
// pętla wyświetlająca wyniki z danego przedziału
for (int i = pocz; i <= koniec; i++)
{
cout << i
<< " do kwadratu = "
<< potega (2, i)
<< " a do sześcianu = "
<< potega (3, i)
<< endl;
}
return 0;
}
/*****************************************************************/
long potega (int stopien, long liczba)
{
long wynik = liczba;
for (int i =1; i < stopien; i++)
{
wynik = wynik * liczba;
}
return wynik; //1
}
/*****************************************************************/
//1 Zwracanie wartości funkcji odbywa się przez instrukcję return. Stawia się poprostu przy niej żądaną wartość. Przykładowo:
return wynik;
return (wynik + 6);
return 12.34;
Jeśli po słowie return stoi wyrażenie, to najpierw obliczana wartość tego wyrażenia, a następnie wynik jest przedmiotem zwrotu.
Należy zwrócić uwagę, że zadeklarowaliśmy funkcje potega typu long. Czy jest błędem napisanie instrukcji return 12.34? Nie zawsze. Nastąpi bowiem próba niejawnej konwersji typu. W naszym przypadku będzie to konwersja typu zmiennoprzecinkowego na typ long, w wyniku której funkcja zwróci wartość 12. Nie zawsze jednak taka konwersja może się odbyć.
Jeśli funkcja została zadeklarowana jako zwracająca typ void, to próba użycia jej w wyrażeniu spowoduje błąd, który zostanie zasygnalizowany. Również, gdybyśmy wewnątrz definicji takiej funkcji obok słowa return napisali wyrażenie, to kompilator wykryje błąd.
Odwrotnie, jeśli zadeklarowaliśmy, że funkcja ma coś zwracać, a przy słowie return stoi sam średnik, kompilator uzna to za błąd.
Jeśli w obrębie funkcji definiujemy jakieś zmienne, to są one przechowywane w podręcznej pamięci nazywanej stosem.
2. Przesyłanie argumentów do funkcji przez wartość
Rozpatrzmy program:
#include <iostream.h>
#include <conio.h>
void alarm (int stopien, int wyjscie);
/************************************************************/
main()
{
int a, m;
clrscr ();
alarm (1, 10);
cout << "\nPodaj stopień zagrożenia: ";
cin >> a;
cout << "Podaj numer wyjścia: ";
cin >> m;
cout << endl;
alarm (a, m);
return 0;
}
/***********************************************************/
void alarm (int stopien, int wyjscie)
{
cout << "Alarm " << stopien
<< " stopnia\n"
<< "Skierować się do wyjścia nr "
<< wyjscie << endl;
}
stopien, wyjscie - parametry formalne
1, 10, a, m - parametry aktualne.
Argumenty przesłane do funkcji w rozpatrywanym przykładzie są tylko kopiami. Jakiekolwiek działanie na nich nie dotyczy oryginału.
Rozpatrzmy program:
#include <iostream.h>
#include <conio.h>
void zwieksz (int formalny);
main ()
{
int aktu = 2;
clrscr ();
cout << "Przed wywołaniem funkcji, aktu = " << aktu << endl;
zwieksz (aktu);
cout << "Po wywołaniu funkcji, aktu = " << aktu << endl;
return 0;
}
/**********************************************************/
void zwieksz (int formalny)
{
formalny += 1000; // zwiększenie liczby o 1000
cout << "Funkcja zwieksz modyfikuje argument formalny\n\t"
<< " i teraz argument formalny = "
<< formalny << endl;
}
Po wykonaniu programu otrzymamy:
Przed wywołaniem funkcji, aktu = 2
Funkcja zwieksz modyfikuje argument formalny
i teraz argument formalny = 1002
Po wywołaniu funkcji, aktu = 2
Należy uświadomić bardzo ważną rzecz: do funkcji przesyłamy tylko wartość liczbową zmiennej aktu (parametru formalnego). Wartość ta służy do inicjalizacji parametru formalnego, czyli zmiennej lokalnej tworzonej przez funkcję na stosie. Jest to więc zrobienie kopii w obrębie funkcji.
Funkcja pracuje na tej kopii. Oznacza to, że w naszym przykładzie dodanie 1000 nie nastąpiło do komórki pamięci, gdzie tkwi aktu, ale do tej zmiennej lokalnej na stosie, gdzie mieści się kopia (o nazwie formalny). Po opuszczeniu funkcji ten fragment stosu jest niszczony, znika więc kopia.
3. Przesyłanie argumentów przez referencję
Język programowania C++ umożliwia również przesyłanie argumentów przez referencje. Oto przykład:
#include <iostream.h>
#include <conio.h>
void zer ( int wart, int &ref); //1
/************************************************************/
main ()
{
int a = 44,
b = 77;
clrscr ();
cout << "Przed wywołaniem funkcji: zer \n";
cout << "a = " << a << ", b = " << b << endl;
zer (a, b); //2
cout << "Po powrocie z funkcji: zer \n";
cout << "a = " << a << ", b = " << b << endl; //7
return 0;
}
/************************************************************/
void zer (int wart, int &ref)
{
cout << "\tW funkcji zer przed zerowaniem \n";
cout << "\twart = " << wart << ", ref = "
<< ref << endl; //3
wart = 0;
ref = 0; //4
cout << "\tW funkcji zer po zerowaniu \n";
cout << "\twart = " << wart << ", ref = "
<< ref << endl; //5
} //6
//1 Funkcja zer zależy od dwóch parametrów: parametru wart przesyłanego przez wartość i parametru ref przesyłanego przez referencję;
//2 Funkcja zer w bloku głównym jest wywoływana z parametrami aktualnymi a i b;
//3 Wewnątrz funkcji zer wypisywane są wartości parametrów formalnych wart i ref.
//4 Następnie w bloku funkcji następuje zmiana wartości parametrów wart i ref.
//5 Następuje wypisywanie wartości parametrów wart i ref.
//6 Działanie funkcji zostaje zakończone. Ponieważ funkcja jest typu void, nie musimy na końcu bloku funkcji pisać instrukcji return ( możemy napisać return;).
//7 Po powrocie z funkcji, będąc w bloku głównym main wypisujemy na ekranie wartości zmiennych a i b. Zmienna, którą funkcja odebrała przez referencje została zmodyfikowana.
Zmiana wartości zmiennej b nastąpiła dlatego, że do funkcji zamiast liczby 77 (wartość zmiennej b) został wysłany adres zmiennej b w pamięci komputera. Ten adres funkcja odebrała i na stosie stworzyła referencję, czyli komórce pamięci o przesłanym adresie nadano nazwę ref.
W //4 do obiektu o nazwie ref wpisano zero. Skoro ref jest nazwą obiektu b, to znaczy, ze odbyło się to na obiekcie b.
Ponieważ po zakończeniu działania funkcji zmienne lokalne zostają niszczone, zobaczmy, co zostało zlikwidowane:
będąca na stosie kopia zmiennej a (kopia początkowo miała wartość 44, a potem 0);
drugi argument przesyłany był przez referencję, więc na stosie mieliśmy zanotowany adres tego obiektu, który nazwaliśmy ref. Ten adres został zlikwidowany.
Wniosek: przesłanie argumentów przez referencję pozwala funkcji na modyfikowanie zmiennych znajdujących się poza tą funkcją.
Jak już było powiedziane wcześniej, każda nazwa przed odniesieniem się do niej musi zostać zadeklarowana. Dotyczy to również funkcji.
4. Argument domniemany
Zadeklarowaliśmy funkcję w sposób następujący:
void temperatura (float stopnie, int skala = 0);
Oznacza to, że parametr skala ma wartość domniemaną zero. Wówczas można wywołać funkcję w sposób następujący:
temperatura (66.7); Drugi parametr ma wartość domniemaną 0.
O tym, że argument jest domniemany, informujemy kompilator raz, w deklaracji funkcji. Jeśli definicja funkcji występuje później, to w definicji już się tego nie powtarza.
Od tej pory wolno funkcję wywołać także z jednym parametrem. Stary sposób z dwoma parametrami też jest dopuszczalny, wtedy kompilator nie musi nic domniemywać.
Jeśli chcemy, by funkcja miała kilka argumentów domniemanych, to argumenty takie muszą być na końcu listy:
int multi (int x, float m, int a = 4, float y = 6.55, int k = 10);
Ostatnie argumenty jako domniemane, mogą być więc w niektórych przypadkach opuszczone. Oto przykłady wywołań tej funkcji:
multi (2, 3.14); // a = 4, y = 6.55, k = 10
multi(2, 3.14, 7); //a = 7, y = 6.55, k = 10
multi(2, 3.14, 7, 0.3); // a = 7, y = 0.3, k = 10
multi (2, 3.14, 7, 0,3, 5); //a = 7, y = 0.3, k = 5
Nie jest możliwe opuszczenie domniemanego argumentu a lub y, a umieszczenie argumentu k. Zatem wywołanie typu:
multi (2, 3.14, , 5);
jest traktowane jako błąd.
5. Nienazwany argument
Jeśli zdefiniujemy funkcję w sposób następujący:
void ton (int wysokosc)
{
}
to kompilator będzie nas ostrzegał, że argument formalny wysokosc nie został nigdzie użyty.
Otóż zamiast wyrzucać cały argument z definicji funkcji, wyrzucamy tylko jego nazwę, a typ argumentu pozostaje:
void ton ( int)
{
}
Można również zastosować następującą definicję:
void ton ( int /* wysokosc */ )
{
}
6. Przypomnienie o zakresie ważności nazw deklarowanych wewnątrz funkcji
Zakres ważności nazw deklarowanych w obrębie funkcji ogranicza się tylko do bloku tej funkcji. Nie można więc spoza funkcji za pomocą danej nazwy próbować dotrzeć do zmiennej będącej w obrębie funkcji.
Nazwą deklarowaną w obrębie funkcji jest tez etykieta. Nie można więc do niej skoczyć instrukcją goto spoza tej funkcji.
Skoro etykieta jest lokalna dla funkcji, dlatego w dwóch różnych funkcjach mogą istnieć bezkonfliktowo identyczne etykiety.
7. Wybór zakresu ważności nazwy i czas życia obiektu
Przez sposób, w jaki definiujemy obiekt, można decydować o zakresie ważności jego nazwy i o czasie jego życia. Poniżej omówimy kilka możliwych sposobów definiowania obiektów.
7.1. Obiekty globalne
Obiekt zdefiniowany na zewnątrz wszystkich funkcji ma zasięg globalny. Oznacza to, że jest on dostępny wewnątrz wszystkich funkcji znajdujących się w tym pliku. Z jednym zastrzeżeniem - jest znany dopiero od linijki, w której nastąpiła jego deklaracja, w dół do końca programu.
Oczywiście praktyka jest taka, że deklaracje umieszcza się na samym początku pliku, dzięki czemu obiekt jest dostępny wszystkim funkcjom z tego pliku.
Rozpatrzmy program:
#include <iostream.h>
#include <conio.h>
int liczba;
void fff(void);
/************************************************************/
main ()
{
int i;
clrscr ();
liczba = 10;
i = 4;
cout << "Wartości: liczba = " << liczba
<< " i = " << i;
fff ();
return 0;
}
/***********************************************************/
void fff(void)
{
int x;
x = 5;
liczba--;
// i = 4; // błąd!
cout << " suma = " << (x + liczba) << endl;
}
/**********************************************************/
7.2. Obiekty automatyczne
W naszym przykładzie zmienne lokalne i oraz x są tzw. zmiennymi automatycznymi. W momencie, gdy kończymy blok, w którym zmienne zostały powołane do życia, automatycznie przestają istnieć. Obiekty automatyczne komputer przechowuje na stosie.
Jeśli po raz drugi wejdziemy do danego bloku, to zmienne takie zostana powołane do życia po raz drugi. Nie ma żadnej gwarancji, że znajdą się w tych samych miejscach w pamięci, co poprzednio. Opuszczając blok, znowu zostana zlikwidowane.
Uwaga:
Należy pamiętać, że zmienne automatyczne nie są zerowane w chwili definicji. Jeśli nie zainicjowaliśmy ich jakąś wartością, to przechowują one wartości przypadkowe. Dzieje się tak dlatego, że zmienne automatyczne przechowywane są na stosie. Przydziela się im wymagany dla danego obiektu obszar - i nic więcej. Nie inicjalizuje się tego obszaru.
Zmienne globalne zakładane są w normalnym obszarze pamięci. Ten obszar przed uruchomieniem programu jest zerowany, zatem zmienna globalna, jeśli jej nie zainicjowaliśmy ma wartość zero.
Z obiektem automatycznym wiąże się słowo kluczowe auto stawiane przed definicją obiektu wewnątrz jakiegoś bloku. Jest ono jednak rzadko używane, gdyż obiekty tak definiowane są automatyczne przez domniemanie. Zatem, jeśli w bloku funkcji definiujemy obiekt:
auto int m;
to jest to równoważne definicji int m;
7.3. Obiekty lokalne statyczne
Zmienne lokalne dla jakiejś funkcji powoływane są do życia w momencie ich definicji, a gdy kończy się wykonywanie tej funkcji, przestają istnieć. Wywołanie ponowne tej funkcji powoduje ponowne utworzenie takiej zmiennej itd. Czasem taka sytuacja nas zadawala, czasem jednak chcielibyśmy, aby zmienna lokalna dla danej funkcji nie ginęła bez śladu, tylko przy ponownym wejściu do tej funkcji miała taką wartość, jak przy ostatnim opuszczeniu tej funkcji.
Załóżmy, że mamy utworzyć funkcję, która będzie wypisywała na ekranie monitora, ile razy ją do tej pory wywołaliśmy. Musi więc mieć ona jakiś licznik w środku o czasie życia rozciągającym się na cały czas wykonywania programu. Czyli taki czas życia, jak maja zmienne globalne. Z drugiej strony chcemy również, żeby ta zmienna była znana tylko lokalnie przez tę funkcję. Bez tego ostatniego warunku wystarczyłoby mieć zmienną globalną.
Oto przykład:
#include <iostream.h>
#include <conio.h>
/*-------------------------------------------------------------*/
void czerwona(void);
void biala(void);
/*-------------------------------------------------------------*/
main ()
{
clrscr ();
czerwona ();
czerwona ();
biala ();
czerwona ();
biala ();
return 0;
}
/*------------------------------------------------------------*/
void czerwona (void)
{
static int ktory_raz;
ktory_raz = ktory_raz + 1;
cout << "Funkcja czerwona wywołana " << ktory_raz
<< " raz(y)\n";
}
/*-----------------------------------------------------------*/
void biala (void)
{
static int ktory_raz = 100;
ktory_raz = ktory_raz + 1;
cout << "Funkcja biala wywołana " << ktory_raz
<< " raz(y)\n";
}
Po wykonaniu programu otrzymamy na ekranie:
Funkcja czerwona wywołana 1 raz(y)
Funkcja czerwona wywołana 2 raz(y)
Funkcja biała wywołana 101 raz(y)
Funkcja czerwona wywołana 3 raz(y)
Funkcja biała wywołana 102 raz(y)
#include <iostream.h>
#include <conio.h>
/*-------------------------------------------------------------*/
void czerwona(void);
void biala(void);
/*-------------------------------------------------------------*/
main ()
{
clrscr ();
czerwona ();
czerwona ();
biala ();
czerwona ();
biala ();
return 0;
}
/*------------------------------------------------------------*/
void czerwona (void)
{
int ktory_raz;
ktory_raz = ktory_raz + 1;
cout << "Funkcja czerwona wywołana " << ktory_raz
<< " raz(y)\n";
}
/*-----------------------------------------------------------*/
void biala (void)
{
int ktory_raz = 100;
ktory_raz = ktory_raz + 1;
cout << "Funkcja biala wywołana " << ktory_raz
<< " raz(y)\n";
}
Obiekty statyczne - jako obiekty przypominające czasem życia obiekty globalne, nie są przechowywane na stosie, lecz w normalnej pamięci. Wstępnie są inicjalizowane zerami.
8. Funkcje w programie składającym się z kilku plików
Program napisany w jednym pliku można podzielić na kilka tylko w miejscu między definicjami funkcji. Załóżmy, że podzieliliśmy program na pliki A i B.
Należy pamiętać o tym, że po to, by funkcje z pliku B miały dostęp do jakichkolwiek zmiennych globalnych z pliku A - trzeba w pliku B umieścić deklaracje tych zmiennych (deklaracje - a nie definicje). Definicje są zrealizowane w pliku A.
Jeśli więc w pliku A mamy następujące zmienne globalne:
int n;
float x;
char z;
to aby móc z nazw n, x, z korzystać w pliku B musimy tam zamieścić deklaracje:
extern int n;
extern float x;
extern char z;
Jeśli chcemy w pliku B wywołać jakąś funkcję z pliku A, to także musimy umieścić w pliku B jej deklarację. W przypadku deklaracji nazwy funkcji nie ma potrzeby pisania słowa extern - jest ono przyjmowane przez domniemanie.
W sumie więc zanim w pliku B pojawią się jego funkcje, najpierw muszą wystąpić deklaracje zmiennych i funkcji z pliku A. Nie wszystkich - tych, które będą w pliku B używane.
Czasem jest wygodne umieścić te wszystkie deklaracje w osobnym pliku - tzw. pliku nagłówkowym, który po prostu bezpośrednio przed procesem kompilacji jest włączany do pliku B. To automatyczne wstawianie do pliku wykonuje dyrektywa preprocesora:
#include ″naglowek.h″
Dyrektywa ta bezpośrednio przed rozpoczęciem pracy kompilatora wstawia do pliku inny plik o nazwie ″naglowek.h″, znajdujący się w bieżącym katalogu. Rozszerzenie .h jest zazwyczaj rozszerzeniem dawanym plikom nagłówkowym.
Oto przykład programu, który podzielony został na 3 pliki:
/********************* plik prog32a.cpp ****************************/
#include <iostream.h>
#include <conio.h>
#include "nagl.h"
#include "prog32b.cpp"
int ile_prymusow = 3;
main ()
{
clrscr ();
cout << "Początek programu\n";
Grupa_3 ();
Grupa_4 ();
cout << "Koniec programu \n";
return 0;
}
/**********************************************************/
void Grupa_1 ()
{
cout << "Jestem w grupie nr 1 ------- \n";
cout << "Jest tu " << ile_prymusow
<< " prymusów, oraz " << ile_spadochroniarzy
<< " spadochroniarzy \n";
}
/*********************************************************/
void Grupa_2 ()
{
cout << "Jestem w grupie Nr 2 ------- \n";
cout << "Jest tu " << ile_prymusow
<< " prymusów, oraz " << ile_spadochroniarzy
<< " spadochroniarzy \n";
}
/*********************************************************/
/******************* plik prog32b.cpp ***********************/
/*********************************************************/
#include <iostream.h>
#include <conio.h>
#include "nagl.h"
int ile_spadochroniarzy = 2;
/**********************************************************/
void Grupa_3 ()
{
cout << "Jestem w grupie Nr 3 ********************** \n";
cout << "Jest tu " << ile_prymusow
<< " prymusów, oraz " << ile_spadochroniarzy
<< " spadochroniarzy \n";
Grupa_1 ();
}
/*********************************************************/
void Grupa_4 (void)
{
cout << "Jestem w grupie Nr 4 ********************* \n";
cout << "Jest tu " << ile_prymusow
<< " prymusów, oraz " << ile_spadochroniarzy
<< " spadochroniarzy \n";
Grupa_2 ();
}
/*********************************************************/
/********************** plik nagl.h **************/
extern int ile_prymusow;
extern int ile_spadochronioarzy;
void Grupa_1 ();
void Grupa_2 ();
void Grupa_3 ();
void Grupa4 ();
9. Nazwy statyczne globalne
Jeśli w deklaracji nazwy globalnej napiszemy słowo static, to oznacza, że nie chcemy, by nazwa ta była znana w innych plikach (modułach) składających się na nasz program. Chodzi tu o nazwy, które są zadeklarowane globalnie, czyli na zewnątrz wszystkich funkcji.
Za sprawą słowa static nazwa jest nadal globalna, ale może być znana tylko w tym jednym pliku. Dotyczy to nie tylko nazw obiektów, ale również nazw funkcji.
Powodem by nazwę globalną funkcji czy obiektu zaopatrzyć w słowo static jest najczęściej chęć by inne moduły (pliki) programu, które danej funkcji czy zmiennej nigdy nie mają używać - nie musiały dbać o unikalność nazw.
10. Funkcje biblioteczne
Programowanie w języku C++ to w zasadzie żadna sztuka, wystarczy opanować kilkanaście instrukcji, wiedzieć, jakie są operatory i jeszcze parę drobnych rzeczy. Jest jednak coś, co będzie zawsze potrzebne - opis funkcji bibliotecznych.
Funkcje biblioteczne nie są częścią języka C++. Są to poprostu funkcje, które kiedyś zostały napisane i okazały się tak dobre i przydatne, że zrobiono z nich bibliotekę standardową. Biblioteka ta stała się tak popularna wśród programistów, że każdy producent kompilatora C++ musi ją także dostarczyć.
W poniższych tabelach zestawiono najczęściej używane funkcje biblioteczne.
Funkcje obsługi ekranu i klawiatury |
||
Nazwa funkcji Składnia |
Biblioteka |
Znaczenie |
clreol void clreol (void); |
conio.h |
kasowanie linii, w której znajduje się kursor poczynając od pozycji kursora do końca |
clrscr void clrscr (void); |
conio.h |
kasowanie ekranu |
delline void delline (void); |
conio.h |
usunięcie linii, w której znajduje kursor |
gotoxy void gotoxy (int x, int y); |
conio.h |
pozycjonowanie kursora |
highvideo void highvideo (void); |
conio.h |
zwiększenie jaskrawości koloru znaku |
insline void insline (void); |
conio.h |
wstawienie nowego wiersza w miejscu aktualnego położenia kursora |
lowvideo void lowvideo (void); |
conio.h |
zmniejszenie jaskrawości koloru znaku |
normvideo void normvideo (void); |
conio.h |
ustalenie koloru tła i znaku, jakie obowiązywały na początku programu (białe znaki na czarnym tle) |
textattr void textattr (int atr); |
conio.h |
ustawienie atrybutów znaku |
textbackground void textbackground (void kolor); |
conio.h |
ustawienie koloru tła |
textcolor void textcolor (void kolor); |
conio.h |
ustawienie koloru znaku |
wherex int wherex (void) |
conio.h |
określenie aktualnej współrzędnej X kursora |
wherey int wherey (void) |
conio.h |
określenie aktualnej współrzędnej Y kursora |
window void window (int xlg, int ylg, int xpd, int ypd ); |
conio.h |
zdefiniowanie okna tekstowego |
Poniżej przytoczono również wybrane standardowe funkcje matematyczne.
Nazwa funkcji Składnia |
Biblioteka |
Znaczenie |
abs int abs (int x); |
stdlib.h |
wartość bezwzględna argumentu całkowitego |
acos double acos (double x); |
math.h |
arccos x |
asin double asin (double x); |
math.h |
arcsin x |
atan double atan (double x); |
math.h |
arctg x |
atof double atof (const char *s); |
math.h |
konwersja łańcucha znaków na liczbę zmiennopozycyjną |
atoi int atoi (const char *s); |
math.h |
konwersja łańcucha znaków na liczbę całkowitą |
cos double cos (double x); |
math.h |
cos x |
cosh double cosh (double x); |
math.h |
cosh x |
div div_t div (int ilo, int resz); |
stdlib.h |
dzielenie dwóch liczb całkowitych; jako wynik otrzymuje się iloraz oraz resztę z dzielenia |
exp double exp (double x); |
math.h |
funkcja wykładnicza ex |
log double log (double x); |
math.h |
logarytm naturalny |
log10 double log10 (double x) |
math.h |
logarytm dziesiętny |
pow double pow (double x, double y); |
math.h |
xy |
pow10 double pow (int p); |
math.h |
10p |
random int random (int N); |
stdlib.h |
generowanie liczb losowych z zakresu od 0 do N-1 |
randomize void randomize (void) |
stdlib.h time.h |
inicjalizacja generatora liczb losowych |
sin double sin (double x) |
math.h |
sin x |
sinh double sinh (double x); |
math.h |
sinh x |
sqrt double sqrt (double x); |
math.h |
pierwiastek kwadratowy z x |
tan double tan (double x) |
math.h |
tg x |
tanh double tanh (double x); |
math.h |
tgh x |
15