c++ b.sobczak [ PL ], WYKLAD3, Temat:


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 - 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:

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 (...);

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 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:

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

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

3



Wyszukiwarka