Informacja o programowaniu w c++ Dr Maciej Bobrowski mate@mif.pg.gda.pl 27 kwietnia 2016
CZEGO NAM NA POTRZEBA Napisanie programu: Edytor, na przykład: vi, vim, joe, xedit, pico, notatnik (na przykład vim 1.cpp, zapisanie pod nazwą 1.cpp) Kompilacja: Kompilator, na przykład: g++, cpp, CC, c++ skompilowanie: c++ 1.cpp, powstanie pliku o nazwie a.out albo: c++ -o 1 1.cpp, powstanie pliku 1 Uruchomienie: Program musi być dla nas wykonywalny (atrybuty wykonywalności), musimy mieć możliwość obejrzenia wyników działania programu. /sciezka_dostepu/a.out, na przykład:./a.out
PIERWSZY PROGRAM // biblioteki #include <cstdlib> #include <iostream> // poniżej potrzebne do tego aby nie używać // za każdym razem: std::cout <<... // a zamiast tego po prostu: cout <<... using namespace std; 10 //funkcja main, tak jak w języku C int main(int argc, char *argv[]) { /* instrukcje funkcji main */ cout << endl << " oto moj pierwszy program w c++" << endl << endl; return 0; 20
DRUGI PROGRAM już z klasą #include <iostream> using namespace std; //definicja klasy class T{ public: void show(char znak) {cout << znak; void show(int liczba) {cout << liczba; ; 10 int main(void){ T t1; //tworzenie obiektu t1.show( a ); // wywolanie funkcji show(char znak) wyswietlajacej znak cout << "\n"; t1.show(10); // wywolanie funkcji show(int liczba) wyswietlajacej liczbe 20 cout << "\n"; return 1;
Dostęp do składników klasy Składnik private: Jest dostępny tylko dla funkcji składowych klasy oraz dla funkcji zaprzyjaźnionych z tą klasą. Składnik protected: Zasady dostępu są takie same jak dla składników private ale dodatkowo jeszcze dla klas wywodzących się od te klasy (dla potomków (klasy można dziedziczyć)). Składnik public: Dostępny bez ograniczeń. Zwykle są to funkcje składowe, gdyż za ich pomocą wykonuje się operacje na danych prywatnych. Domyślnie, jeśli nie wystąpi żadna z wyżej wymienionych etykiet, wówczas składniki mają flagę private.
Przyklad #include <iostream> #include <cstring> using namespace std; //definicja klasy class osoba{ private: char nazwisko[80]; int wiek; 10 public: //deklaracja funkcji składowej klasy (na razie brak definicji) void pamietaj(char* napis, int lata); //definicja funkcji składowej wewnątrz definicji klasy void wypisz(){ cout << "\t" << nazwisko << ", lat: " << wiek << endl; ; //deinicja funkcji składowej klasy poza definicją samej klasy void osoba::pamietaj(char* napis, int lata){ strcpy(nazwisko,napis); wiek=lata; 20 int main(void){ osoba student, profesor, pilot; 30
profesor.pamietaj("albert Einstein",55); student.pamietaj("jane Doe",20); pilot.pamietaj("neil Armstrong",37); cout << " Po wpisanu danych do obiektow\n\n"; profesor.wypisz(); student.wypisz(); pilot.wypisz(); cout << "\n"; 40 return 1;
Konstruktor Przyklad klasy przechowującej liczbę class numer{ private: int liczba; public: void schowaj(int x) { liczba = x; int zwroc (){ return liczba; ; Normalnie (bez klasy) mielibyśmy int skrytka = 5; Zaś przy pomocy klasy numer musimy postąpić tak: numer MojaSkrytka; MojaSkrytka.schowaj(5); Jak widać nasza klasa nie jest taka wygodna. Zamiast jednoczsnej inicjalizacji w momencie powołania obiektu do życia w pamięci oraz nadania mu wartości, musimy najpierw zdefiniować obiekt naszego nowego typu, a dopiero później nadać wartość komponentowi poprzez wywołanie funkcji składowej. Czy nie można tego zrobić w jednej instrukcji, czyli tak jak w metodzie tradycyjnej? Można.
Konstruktor, przykład class numer{ private: int liczba; public: numer (int x) { liczba = x; // < konstruktor int zwroc (){ return liczba; ; A zatem konstruktor jest funkcją, która ma taką samą nazwę jak nazwa klasy. Funkcja taka nic nie zwraca, nawet typu void. Nie ma tam też zatem instrukcji return. Jak posługujemy się konstruktorem? numer A1 = numer(100); albo numer A2(30); Uwaga. Konstruktor nie konstruuje obiektu, a jedynie nadaje mu wartość początkową. Konstruktor sam nie przydziela pamięci na obiekt, On może ją jedynie zainicjalizować.
Destruktor Kiedy wewnątrz funkcji definiowaliśmy obiekt automatyczny, np. typu int wówczas gdy funkcja kończyła pracę to obiekt przestawał istnieć. Podobnie jest z destruktorem. Destruktor uruchamiany jest autom. Destruktor nazywa się tak samo jak nazwa klasy, jednak z dodatkową tyldą na początku jego nazwy. Podobnie jak konstruktor - nie zwraca on nic. Uwaga Destruktor nie usuwa obiektów, on tylko sprząta. #include <iostream> #include <cstring> using namespace std; class numer{ private: int liczba; public: numer (int x) { liczba = x; // < konstruktor int zwroc (){ return liczba; 10 numer(){ cout << " jestesmy wewnatrz destruktora\n\n"; ; int main(void){ //przyklady z konstruktorami numer A1(100); numer A2(1000); numer A3 = numer(1000000); //przyklady z destruktorem 20 A1. numer(); A2. numer(); A3. numer(); return 1;
Dynamiczna alokacja pamięci - operatory new oraz delete Operator new służy do utworzenia w pamięci obiektu, zaś operator delete do skasowania go z pamięci. Obiekty tak tworzone nie mają nazwy, ale posługujemy się nimi za pomocą wskaźników. int* wsk1; wsk1 = new int; Tak utworzony obiekt nie ma nazwy, ale pokazuje na niego wskaźnik. Obiekt typu int pokazywany przez wsk1 nie ma też na razie żadnej zadanej przez nas wartości. Jednak, ponieważ zarezerwowano dla niego odpowiednią komórkę pamięci, to taki obiekt ma jakąś wartość, pochodzącą z obszaru pamięci, który zarezerowaliśmy. Musimy więc sami zadbać o to aby go na przykład wyzerować lub nadać mu jakąś sensowaną wartość. Instrukcja delete wsk1 usunie dostęp do obiektu za pomocą wskaźnika wsk1. Od tej chwili nie ma do niego dostępu. Mówimy o kasowaniu obiektu a tak naprawdę chodzi o zwolnienie dostępu do pamięci o odpowiednim adresie. Można też nadać wartość początkową już w momencie tworzenia obiektu : int* wsk1 = new int(100); Można też ustawić wskaźnik w konkretnym miejscu pamięci, które już wcześniej było pokazywane przez jakiś inny wskaźnik (poniżej nazywany adres): wsk = adres new int;
Dynamiczna alokacja pamięci - przykład #include <iostream> using namespace std; //deklaracja funkcji produkującej obiekty char* producent(void); int main(){ char *w1, *w2, *w3, *w4; 10 w1 = producent(); w2 = producent(); w3 = producent(); w4 = producent(); *w1 = A ; *w2 = B ; *w3 = C ; cout << "oto wartosci z 3 wskaznikow: " << *w1 << *w2 << *w3 << endl 20 << " oraz smiec w czwartym: " << *w4 << endl; return 0; //definicja funkcji produkującej obiekt dynamicznie i zwracajacej wskaznik. char* producent(void){ char *w; cout << " wlasnie produkuje obiekt\n"; w = new char; 30 return w;
Wynik działania mate cpp >./a.out wlasnie produkuje obiekt wlasnie produkuje obiekt wlasnie produkuje obiekt wlasnie produkuje obiekt oto wartosci z 3 wskaznikow: ABC oraz smiec w czwartym: mate cpp >
Dynamiczna alokacja pamięci - tablice jednowymiarowe Najpierw deklarujemy wskaźnik a następnie rezerwujemy w pamięci obszar dla danych. int* tablica = new int[3]; tablica[0] = 11; tablica[1] = 12; tablica[2] = 13; delete [] tablica; Dzięki użyciu tablicy dynamicznej użytkownik ma możliwość decydowania o rozmiarze tablicy podczas działania programu: int ile = 10; int* tablica1 = new int[ile]; ile = 100; int* tablica2 = new int[ile]; delete [] tablica;
Dynamiczna alokacja pamięci - tablice dwuwymiarowe Tablica dynamiczna dwuwymiarowa to tak naprawdę tablica wskaźników do poszczególnych wymiarów. Jaka płynie z tego korzyść? Podczas deklaracji tablicy mamy pełną kontrolę nad wielkością poszczególnych wymiarów, statycznie nie da się osiągnąć takich efektów: int wymiar = 3; //tablica na wskazniki int** tablica = new int* [wymiar]; // generowanie poszczegolnych wymiarów for (int i = 0; i<wymiar; i++){ tablica[i] = new int[wymiar]; // zwalnianie pamieci for (int i = 0; i<wymiar; i++){ delete [] tablica[i]; 10 delete [] tablica;
Przyklad dynamicznej alokacji tablicy Przy okazji - pamięć operacyjna to nie studnia bez dna #include <iostream> #include <stdlib.h> #include <new> using namespace std; //deklaracja funkcji void funkcja alert(); long k; int main(void){ 10 set new handler(funkcja alert); //ponizej nieskonczona petla alokujaca pamiec for (k=0; ;k++){ new int; return 1; 20 void funkcja alert(){ cout << "\n zabraklo pamieci przy k = " << k << "! \n" << endl; exit(1);
Porównanie funkcji malloc()/free() z operatorami new/delete malloc() oraz free() są nadal dostępne w c++. malloc() zwraca wskaźnik i operator new również, a więc w obydwu przypadkach trzeba proacować na obiektach przy pomocy wskaźników. Podczas automatycznego tworzenia obiektu konstruktorem, można posłużyć się operatorem new, wówczas pamięć jest dynamicznie alokowana, a obiekt tworzy się banalnie prosto. Podobnie, użycie delete w destruktorze automatycznie usunie obiekt z pamięci (tzn. zostanie zwolniona pamięć przez nasz program). malloc() zwraca wskaźnik do void czyli do nieokreślonego typu danych. Zaś new zwraca wskaźnik do typu, który właśnie jest tworzony. Jest to znacznie bezpieczniejsze. new oraz delete nie są zależne od modelu pamięci komputera/systemu operacyjnego. Na przykład na komputerach klasy IBM PC z kompilatorem Borland C++ przy modelu pamięci small należało używać malloc() oraz free(), jednak przy modelu pamięci huge należało używać farmalloc() oraz farfree().
Przeładowanie operatorów W programowaniu obiektowym powinniśmy mieć możliwość używania typowych operatorów + - * / itd. w przypadku obiektów naszych klas tak jak w przypadku typów wbudowanych. int a=3, b=5, c; c=a+b; Tymczasem dla typów abstrakcyjnych, definiowanych przez użytkownika nie jest to takie proste. NMa przykład dla typu definiującego liczby zespolone (klasa complex) complex a(10,20); complex b(5,3); complex w; w = dodaj(a,b); Jak widać nie jest to naturalne i takie wygodne jak dla typów wbudowanych. Chcielibyśmy mieć w = a+b; Czy jest to możliwe? Tak. jest to możliwe. Trzeba przeładować operator +. Wykonuje się to pisząc odpowiednią funkcję o takiejn właśnie nazwie, czyli o nazwie operator+.
Przeładowanie operatorów //definicja funkcji przeladowujacej operator dodawania complex operator+(complex A, complex B){ complex suma; suma.real = A.real + B.real; suma.imag = A.imag + B.imag; return suma; 10 //definicja funkcji dodajacej liczby zespolone //bez przeladowania operatora dodawania. complex dodaj(complex A, complex B){ complex suma; suma.real = A.real + B.real; suma.imag = A.imag + B.imag; return suma; 20 Jak widać, obydwie funkcje robią dokładnie to samo, ale jedna z nich ma nazwę operator+ i wykonuje dodawanie gdy w programie znajdzie się odpowiednia linijka kodu z symbolem dodawania oraz dwoma obiektami typu complex po obu stronach operatora.
Przeładowanie operatorów - przykład #include <iostream> using namespace std; class complex { private: double real; double imag; public: //konstruktor 10 complex (double re, double im) : real(re), imag(im) { //wypisnie liczby zespolonej void wypisz(){ cout << "(" << real << "," << imag << ")"; //przeladowany operator dodawania complex operator+(complex B){ complex suma(0.0,0.0); suma.real = real + B.real; suma.imag = imag + B.imag; 20 return suma; ; int main(){ complex X(10.0,20.0); complex Y(5.0,3.0); complex Z(0.0,0.0); cout << endl << endl << " liczba zespolona X: "; 30 X.wypisz(); cout << endl;
cout << endl << " liczba zespolona Y: "; Y.wypisz(); cout << endl; //obliczenie sumy liczb zespolonych //przy pomocy przeladowanego operatora dodawania Z = X + Y; cout << endl << " X + Y = "; Z.wypisz(); cout << endl << endl; 40 return 0;