Wstęp do programowania obiektowego Przekazywanie parametrów do funkcji w C++ Metody i funkcje operatorowe Strumienie: standardowe, plikowe, napisowe 1
PRZEKAZYWANIE PARAMETRÓW DO FUNKCJI W C++ 2
W C++ dostępne są dwa tryby przekazywania parametrów do funkcji: Przez wartość Przez referencję 3
PRZEKAZYWANIE PARAMETRÓW DO FUNKCJI W C++ PRZEZ WARTOŚĆ 4
Przykład: przekazywanie przez wartość #include <stdio.h> int kwadrat (int x){ return x*x; } int main(){ int i = 3; printf( Kwadrat %d to %d, i, kwadrat(i)); return 0; } 5
Parametry formalne i aktualne #include <stdio.h> int kwadrat (int x) { return x*x; } Parametr formalny Definicja funkcji int main() { int i = 3; printf( Kwadrat %d to %d, i, kwadrat(i)); return 0; } Wywołanie funkcji Parametr aktualny 6
Przekazywanie parametrów do funkcji przez wartość #include <stdio.h> int kwadrat (int x) { return x*x; } Parametr formalny Definicja funkcji int main() Wywołanie funkcji { int i = 3; printf( Kwadrat %d to %d, i, kwadrat(i)); return 0; } Parametr aktualny 1. Tworzona jest nowa zmienna lokalna (parametr formalny) o nazwie jak w nagłówku funkcji (tutaj x). 2. Wyliczana jest wartość parametru aktualnego; może to być zmienna, stała lub wyrażenie (tutaj jest to zmienna o wartości 3). 3. Wartość parametru aktualnego jest kopiowana do parametru formalnego. 4. Parametr formalny jest używany jak zwykła zmienna lokalna. 5. Po zakończeniu funkcji nie ma kopiowania wartości do parametru aktualnego (nawet jeżeli jest on zmienną ). Operacje na parametrach formalnych nie mają wpływu na parametry aktualne. 7
Parametr aktualny jako wyrażenie #include <stdio.h> int kwadrat (int x) { return x*x; } int main() { int i = 3; int j = 5; printf( Kwadrat sumy %d i %d to %d, i, j, kwadrat(i+j)); return 0; } Parametr aktualny 8
Kilka parametrów #include <stdio.h> float funkcja (int a, float b) { return 5*a + b; } int main() { int i = 2; float j = 3.14; printf( Wartość funkcji to %f, kwadrat(i,j)); return 0; } 9
Przekazywanie wskaźnika jako parametr Aby nie kopiować niepotrzebnie dużych ilości danych (np. przekazując przez wartość strukturę lub obiekt o dużej objętości) lub też umożliwić funkcji modyfikację zmiennej przekazuje się przez wartość wskaźnik. Przekazanie (skopiowanie) adresu (jednej liczby całkowitej) to prosta operacja i umożliwia pełny dostęp do wskazywanego obiektu. Sam wskaźnik nie jest modyfikowany (kopiowany jest adres), ale może być modyfikowany wskazywany przez niego obiekt (czy też zwykła zmienna). 10
Przekazywanie wskaźnika: przykład #include <stdio.h> void zwiększojeden(int* a) //parametr formalny to wskaźnik { (*a)++; //nawias dla pewności } int main() { int i = 2; zwiększojeden(&i); //parametr aktualny to adres zmiennej i printf( Wartość i to %d, i); // wypisze 3 return 0; } Do parametru formalnego a (wskaźnika) jest zapisywana wartość adresu zmiennej i, więc operacje na obiekcie wskazywanym przez a odbywają się tak naprawdę na zmiennej i. C/C++ nie sprawdza na jakiej pamięci operujemy (czy jest ona np. zarezerwowana na inne zmienne), stąd możliwe są różne błędy związane z dostępem przez wskaźnik do cudzej pamięci. 11
PRZEKAZYWANIE PARAMETRÓW DO FUNKCJI W C++ PRZEZ REFERENCJĘ 12
Cechy referencji w C++ Referencja w C++ jest to wskaźnik o ograniczonych możliwościach (nie można używać arytmetyki wskaźników, uproszczony dostęp do wskazywanego elementu bez * ) Referencję deklarujemy i inicjujemy następująco: <typ danych> & <nazwa> = <istniejąca_zmienna<; np. int &ref_r = r; Referencja musi być zainicjalizowana w momencie utworzenia. Referencji po przypisaniu nie można przestawić na inną zmienną. 13
Przykład użycia referencji int i=0; int &ref_i=i; cout << i; // wypisuje 0 ref_i = 1; cout << i; // wypisuje 1 cout << ref_i; // wypisuje 1 14
To samo za pomocą wskaźnika int i=0; int *wsk_i; // deklaracja wsk. wsk_i = &i; // inicjacja wsk. cout << i; // wypisuje 0 *wsk_i = 1; cout << i; // wypisuje 1 cout << *wsk_i; // wypisuje 1 15
W nagłówku funkcji używamy & (ampersandu) pomiędzy nazwą typu oraz nazwą parametru formalnego Parametr aktualny musi być zmienną (nie może być wyrażeniem ani stałą) Parametr formalny jest aliasem (alternatywną nazwą) zmiennej będącej parametrem aktualnym. Jeżeli chcemy uniemożliwić zmiany wartości zmiennej to używamy słowa kluczowego const przed parametrem formalnym. 16
Przekazywanie parametrów przez referencję #include <stdio.h> void zwiększojeden(int& a) //parametr formalny to referencja { a++; } int main() { int i = 2; zwiększojeden(i); //parametr aktualny to zmienna i printf( Wartość i to %d, i); // wypisze 3 return 0; } Parametr formalny a to referencja do zmiennej i, więc operacje na obiekcie wskazywanym przez a odbywają się tak naprawdę na zmiennej i. Funkcję wywołujemy jak dla zwykłej zmiennej Wewnątrz funkcji operujemy referencją jak zwykłą zmienną (bez gwiazdki) 17
METODY OPERATOROWE 18
Operatory znaki symbolizujące wykonanie powszechnie znanych (lub domyślnych) operacji Operandy argumenty na których te operacje się wykonuje 19
Operatory w C++ (1/3) Artymetyczne: a=b a+b a-b a*b a/b a++ ++a a-- --a a%b Porównania: a==b a!=b a>b a<b a>=b a<=b 20
Operatory w C++ (2/3) Logiczne:!a a&&b a b Bitowe: ~a a&b a b a^b a<<b a>>b Działanie z podstawieniem: a+=b a-=b a*=b a/=b a%=b a&=b a =b a^=b a<<=b a>>=b 21
Operatory w C++ (3/3) Zakresu i wskaźnikowe: a[b] *a &a a->b a.b a->*b a.*b Inne:, a?b:c a::b sizeof() typeid() new new[] delete delete[] <wywołanie funkcji> <rzutowania typu> 22
Funkcje operatorowe Funkcje operatorowe pozwalają na zdefiniowanie działania tych operatorów w odniesieniu do argumentów obiektowych. Służy do tego funkcja operatorowa, której nazwa ma postać: operator<symbol operatora> Przykładowe nazwy: operator+ operator<< 23
Ograniczenia: nie można zmienić: * liczby argumentów * priorytetu operatora * sposobu wiązania (lewostronnie, prawostronnie) nie można przedefiniowywać niektórych operatorów:..*?: :: sizeof typeid 24
Przykład 1: operator dodawania liczb zespolonych Definiujemy metodę: Zespolona operator+ (const Zespolona & z) { return Zespolona(re+z.re, im+z.im); } Przykład wywołania (z użyciem symbolu operatora): Zespolona z, z1(1,0), z2(0,1); z = z1 + z2; Ale można też wywoływać pełną nazwą ( standardowo ): z = z1.operator+(z2); 25
Metoda operatorowa dwuargumentowa Składnia: <typ wyniku> operator <symbol operatora>(<argument typu obiektowego>) Funkcja operatorowa dwuargumentowa należąca do klasy (metoda operatorowa) jest wywoływana na rzecz obiektu, który jest po lewej od operatora (dane tego obiektu są dostępne przez this, albo bezpośrednio przez nazwę). Drugi argument jest przekazywany bezpośrednio (przez referencję lub wskaźnik). 26
Przykład 2: operator mnożenia liczb zespolonych Zespolona operator* (const Zespolona& z) { return Zespolona(re*z.re-im*z.im, re*z.im+im*z.re); } Wywołanie operatorem: z = z1 * z2; Wywołanie w postaci pełnej: z = z1.operator*(z2); 27
Metoda operatorowa jednoargumentowa Składnia: <typ wyniku> operator <symbol operatora> (void) Funkcja operatorowa jednoargumentowa należąca do klasy (metoda operatorowa) jest zwykle wywoływana na rzecz obiektu po prawej od operatora (dane tego obiektu są dostępne przez this, albo bezpośrednio przez nazwę). Przykład: definicja operatora minus (zmiana znaku liczby) Zespolona operator- (void) { return Zespolona(-re, -im); } Użycie: Zespolona z, z2(0,1); z=-z2; Dla porównania: operator odejmowania dwóch liczb zespolonych: Zespolona operator- (const Zespolona& z) { return Zespolona(re-z.re, im-z.im); } z = z1 - z2; 28
Inne języki obiektowe W wielu językach obiektowych nie ma możliwości przedefiniowywania operatorów, ze względu m.in. na możliwe niejasności jakie to powoduje (operator jakiej klasy się wywoła, jakie są priorytety rozpatrywania, z której strony się łączą itp.). Zamiast metod operatorowych używa się zwykłych metod. Definicja operacji dodawania liczb zespolonych bez użycia operatorów: Zespolona dodaj(const Zespolona& z) { return Zespolona(re+z.re, im+z.im); } Wywołanie dla takiej definicji: z = z1.dodaj(z2); 29
FUNKCJE OPERATOROWE 30
Jak zdefiniować operator, który pozwala na zapis: int x=5; Zespolona z, z1(5,3); z = x * z1; gdzie x jest zmienną typu float, tzn. lewym argumentem operatora jest zmienna typu standardowego? Wiemy, że operatory dwuargumentowe wywołują się dla swojego lewego argumentu, ale nie mamy możliwości zmiany działania wbudowanego typu float. 31
Funkcja operatorowa Rozwiązaniem jest zdefiniowanie funkcji operatorowej (poza klasą, ale w obszarze widoczności wywołania). Lista argumentów będzie tym razem pełna (nie mamy tym razem za darmo obiektu dla którego wołamy funkcję czyli this). Składnia nagłówka funkcji operatorowej jednoargumentowej: <typ wyniku> operator <symbol operatora> (<typ argumentu>) Składnia nagłówka funkcji operatorowej dwuargumentowej: <typ wyniku> operator <symbol op.> (<typ lewego arg.>, <typ prawego arg.>) 32
Czyli definiujemy np. poza main() i klasą Zespolona następującą funkcję: Zespolona operator* (float a, const Zespolona& z) { return Zespolona(a*z.re, a*z.im); } Jeżeli w takiej funkcji używane są pola prywatne klasy Zespolona, funkcję należy zaprzyjaźnić z klasą. Wtedy możemy używać wspomnianego kodu: int x=5; Zespolona z, z1(5,3); z = x * z1; 33
Podobna sytuacja występuje przy definiowaniu operatorów wprowadzania lub wyprowadzania do strumienia, np.: cout << z; 34
STRUMIENIE 35
Strumienie Strumień - ciąg bajtów o nieokreślonej długości W języku C++ wyróżniamy trzy typy strumieni: strumienie wejściowe (wczytują dane), strumienie wyjściowe (wypisują dane), strumienie uniwersalne, umożliwiające zarówno wczytywanie, jak i wypisywanie danych. Strumieniami posługujemy się zwykle do operacji wejścia/wyjścia (ekran, klawiatura, pliki), np. pobieraliśmy dane z klawiatury lub pliku, wypisywaliśmy dane na ekran, zapisywanie danych do pliku. Można też strumieniami przetwarzać dane, np. napisy. 36
Do wczytywania danych ze strumienia wejścia służy operator >>, a wysyłania danych do strumienia wyjścia służy operator << Tak naprawdę wszystkie dane, które strumień wypisuje na ekran muszą zostać sformatowane, ale w wielu przypadkach (dla zmiennych typów standardowych) dzieje się to automatycznie. 37
Strumienie vs funkcje Dla przykładu, aby wypisać zmienną x typu float, w C++ dołączamy bibliotekę iostream.h i piszemy: std::cout << x; Jeżeli użyjemy przestrzeni nazw std (using namespace std;) to możemy jeszcze krócej: cout << x; W języku C musieliśmy dołączyć bibliotekę stdio.h i wypisywać za pomocą funkcji (precyzując formatowanie): printf("%f", x); 38
Możemy też łączyć strumienie w "kaskady": cout <<"x ma wartość "; cout <<x; cout <<".\n"; cout << "x ma wartość " << x << ".\n"; 39
Strumienie predefiniowane Strumienie predefiniowane to strumienie już stworzone i gotowe do korzystania. Strumienie te dziedziczą po klasie ostream dla strumieni wyjścia i istream dla wejścia (są to obiekty!). Dołączenie pliku nagłówkowego iostream sprawia, że mamy od początku otwarte 3 predefiniowane strumienie: std::cin - standardowe wejście std::cout - standardowe wyjście std::cerr - wyjście dla komunikatów błędu 40
#include <iostream> #include <string> using namespace std; int main(){ string x; cout << "Podaj swoje imię:"; cin >> x; cout << x << endl; return 0; } Operator >> "wyciąga" pojedyncze słowo oddzielone białymi znakami oraz zapisuje je do zmiennej x (tutaj obiekt klasy string). 41
Możemy stworzyć bufor na tekst (c-string czyli tablica charów) oraz wypełnić go znakami, Metoda getline() klasy cin pobiera wskaźnik na stworzony wcześniej bufor oraz jego rozmiar. char tekst[100]; cout << "Podaj imię i nazwisko:"; cin.getline(tekst, 100); cout << x << std::endl; 42
STRUMIENIE NAPISOWE 43
Strumienie napisów Wyróżniamy jeszcze jeden rodzaj strumieni - stringstream. Dzięki niemu jesteśmy w stanie operować na napisach tak, jak na zwykłym strumieniu. Wyobraźmy sobie sytuację, gdy musimy zamienić liczbę całkowitą na napis. Język C umożliwiał nam dokonywanie takich operacji za pomocą funkcji sprintf() bądź funkcji itoa(). 44
Przykład stosowania strumieni napisowych w C++ #include <iostream> #include <sstream> // plik nagłówkowy do stringstream using namespace std; int main() { long x; string napis; stringstream ss; cout << "Podaj dowolna liczbę całkowitą: "; cin >> x; ss << x; // Do strumienia 'wysyłamy' podaną liczbę napis = ss.str(); // Zamieniamy strumień na napis cout << "Długość napisu wynosi " << napis.length() << " znaków."<< endl; return 0; } 45
STRUMIENIE PLIKOWE 46
Strumienie plikowe Za pomocą strumieni możemy czytać i zapisywać do plików. Przykładowy program czyta po jednym znaku ze standardowego wejścia i zapisuje go do pliku tekst.txt, dopóki użytkownik nie wciśnie <Enter> #include <iostream> #include <fstream> //plik nagłówkowy fstream do strumieni plikowych int main(){ char a; // mini-bufor std::ofstream f ("tekst.txt"); // tworzymy strumień wyjściowy f, podłączamy go do pliku tekst.txt (param. konstruktora) std::cout << "Podaj tekst do zapisu:"; do { a = std::cin.get(); // wczytujemy pojedynczy znak ze standardowego wejścia, f << a; // zapisujemy znak z mini-bufora do pliku } while (a!= '\n'); // dopóki użytkownik nie wciśnie <Enter> return 0; } 47
Takie podejście (pliki traktowane jako strumienie) jest powszechnie stosowane w innych językach obiektowych (m.in. Java, C#, Python). 48