Języki programowania Nowoczesne techniki programowania Wykład 6 Witold Dyrka witold.dyrka@pwr.wroc.pl 11/01/2012
Prawa autorskie Slajdy do dzisiejszego wykładu powstały w oparciu o: C++ Language Tutorial (Juan Soulié) http://www.cplusplus.com/doc/tutorial/ slajdy Bjarne Stroustrupa do kursu Foundations of Engineering II (C++) prowadzonego w Texas A&M University http://www.stroustrup.com/programming
Materiały Literatura Bjarne Stroustrup. Programowanie: Teoria i praktyka z wykorzystaniem C++. Helion (2010) Jerzy Grębosz. Symfonia C++ standard. Edition 2000 (2008) Dowolny podręcznik języka C++ w standardzie ISO 98 www.cplusplus.com - The C++ Reference Network (j.ang.) Środowisko programistyczne Microsoft Visual C++ (rekomendowane) Dowolne środowisko korzystające z GCC
Program wykładów 1. Pierwszy program. Obiekty, typy i wartości 2. Wykonywanie obliczeń. Błędy 3. Pisanie i wykańczanie programu 4. Techniki pisania funkcji i klas 5. Strumienie wejścia/wyjścia 6. Wskaźniki i tablice 7. Systematyka technik i języków programowania* * nie zdążymy:-(
Zagadnienia Tablice tworzenie, inicjacja, przekazywanie do funkcji Wskaźniki operatory adresu i deferencji arytmetyka wskaźników Tworzenie obiektów i tablic w pamięci wolnej Tablica czy wektor?
Tablice Tablica jest szeregiem elementów tego samego typu umieszczonych w ciągłym bloku pamięci typ_elementu nazwa_tablicy[liczba_elementów] np. int wiek[7] wiek: 0 sizeof(int) 1 2 3 4 5 elementy tablicy są indeksowane od zera 6
Dostęp do elementów tablicy nazwa_tablicy [indeks_elementu] np. wiek: 23 30 56 71 25 32 45 0 1 2 3 4 5 6 cout << wiek[0]; cout << wiek[6]; cout << wiek[7]; cout << wiek[-1]; wiek[0] = 25; wiek[1] = wiek[6]; wiek[7] = 26; // 23 // 45 //?? wartość nieokreślona // jednak kompilator nie zgłosi błędu // może powodować błąd czasu wykonania // bzdura, która się kompiluje // podstawiamy wartość 25 na pierwszą pozycję w tabeli // podstawiamy wartość 2-gą wartością z pozycji 7-mej // zapisujemy 26 poza obszarem zarezerwowanym // dla tablicy wiek możemy nadpisać inną zmienną // jednak kompilator nie zgłosi błędu // może powodować błąd czasu wykonania
Tworzenie tablicy Standardowa tablica ma z góry określoną liczbę elementów, na które kompilator rezerwuje pamięć rozmiar tablicy musi być określony przez literał stały void f(int n) { int tablica_calk[60]; } // ok const int rozmiar = 20; char tablica_znak[rozmiar]; // ok double tablica_rzeczyw[n]; // błąd kompilacji int liczba_el = 10; double tablica[liczba_el]; // błąd kompilacji //...
Inicjacja tablicy w zakresie globalnym: automatyczna inicjacją wartością domyślną dla typu elementów int wiek[5]; int main() { cout << wiek[2]; } // wyświetli: 0 w zakresie lokalnym (np. w funkcji) brak inicjacji int main() { int wiek[5]; cout << wiek[2]; } // wyświetli: nie-wiadomo-co!
Lista inicjacyjna tablicy int wiek[7] = { 23, 30, 56, 71, 25, 32, 45 }; wiek: 23 30 56 71 25 32 45 71 25 0 0 int wiek[7] = { 23, 30, 56, 71, 25 }; wiek: 23 30 56 int wiek[] = { 23, 30, 56, 71, 25, 32 }; wiek: 23 30 56 71 25 32 0 0 0 0 0 int wiek[6] = { }; wiek: 0
Lista inicjacyjna tablicy znaków char imie[6] = { 'W', 'i', 't', 'o', 'l', 'd' }; imie: 'W' 'i' 't' char imie[6] = ''Witold''; 'o' 'l' 'd' // błąd kompilacji! Wyraz nie mieści się w tablicy char imie[] = ''Witold''; imie: 'W' 'i' 't' 'o' 'l' 'd' 0 // C-string, czyli // ciąg ASCIIZ //... imie = ''Janusz''; // błąd kompilacji! W ten sposób można tylko inicjować tablice
Tablice wielowymiarowe Tablice wielowymiarowe są jakby tablicami tablic const int wymiar_n = 10; const int wymiar_m = 20; int main() { int macierz[wymiar_n][wymiar_m]; for (n=0;n<wymiar_n;n++) for (m=0;m<wymiar_m;m++) { macierz[n][m]=(n+1)*(m+1); } double wiek[100][365][24][60][60]; return 0; } // liczba wymiarów nie jest ograniczona, // ale pamięć tak: standardowo // ta tablica zajmie ponad 25 gigabajtów
Przekazywanie tablic jako parametrów funkcji Nie da się przekazać tablicy przez wartość Przekazujemy adres bloku pamięci zajętego przez tablicę void drukuj(double tablica[]) adres tablicy określa jej nazwa: double tablica1[50]; drukuj(tablica1); tablica nie pamięta swojego rozmiaru trzeba go przekazać osobno: void drukuj (double tablica[], int rozmiar) { for (int i=0; i<rozmiar; i++) cout << tablica[i] << endl; } int main () { double pierwsza[] = {1.2, 3.4, 5.6}; double druga[] = {2.1, 4.3, 6.5, 8.7, 10.9}; drukuj(pierwsza, 3); drukuj(druga, 5); return 0; }
Przekazywanie tablic wielowymiarowych jako parametrów funkcji Kompilator musi wiedzieć czy blok pamięci o pojemności np. 200 elementów ma traktować jako tablicę 10x10x2 czy 5x5x4? pamięć jest liniowa wielowymiarowość tablic jest tylko interpretacją kompilatora! dlatego: void drukuj2(double tablica2[ ][10][2]); void drukuj2(double tablica2[ ][5][4]); to dwie różne funkcje podczas, gdy: void drukuj(double tablica[10]); void drukuj(double tablica[5]); są takie same i kompilator traktuje je tak: drukuj(double tablica[ ]);
Przykład: kopiowanie tablic znaków Tablica nie zna swojego rozmiaru void f(int pi[ ], int n, char pc[ ]) // ostrzeżenie: to nie jest bezpieczny kod, podajemy go jako przykład // nigdy nie zakładaj, że podany rozmiar jest poprawny { char buf1[200]; strcpy(buf1,pc); // kopiuje znaki z pc do buf1 // strcpy kończy działanie gdy napotka znak '\0' // zakładamy, że pc zawiera mniej niż 200 znaków strncpy(buf1,pc,200); // kopiuje 200 znaków z pc do buf1 // jeśli pc jest krótsze, wypełnia buf1 do końca znakami '\0' int buf2[300]; // nie można było utworzyć char buf2[n]; n jest zmienną if (300 < n) throw runtime_error("brak miejsca"); for (int i=0; i<n; ++i) buf2[i] = pi[i]; // zakładamy, że pi naprawdę zawiera // n int-ów. Mamy problem jeśli ma mniej... }
Adresy tablic a referencje Przekazywanie tablicy jako argumentu funkcji jest podobne do przekazywania argumentu przez referencję w obu przypadkach funkcja może modyfikować argument int modyfikuj_liczbe (int& a); int modyfikuj_tablice (int tab[]); Czy nazwa tablicy jest referencją? NIE referencja jest inną nazwą obiektu znajdującego się pod tym samym adresem, np. człowiek jan_kowalski; człowiek& mój_sąsiad = jan_kowalski nazwa tablicy jest niemodyfikowalnym oznaczeniem adresu pamięci
Odkryliśmy nowy byt: wskaźnik (ang. pointer) Wskaźnik jest adresem pamięci nazwa tablicy jest specjalnym przypadkiem wskaźnika Wskaźnik definiujemy następująco: typ_obiektu* nazwa_wskaźnika; wskaźnik wskazuje na obiekt określonego typu, np. double* liczba może wskazywać na liczbę double, a nie na string dzięki temu wiemy jak korzystać ze wskazywanego obiektu, np. to na co wskazuje double* można dodawać, a nie konkatenować
Adresy: operator & int a; char ac[20]; void f(int n) { int b; int* p = &b; p = &a; } pc: p: a: ac: // wskaźnik na pojedynczą zmienną lokalną // wskaźnik na pojedynczą zmienną globalną char* pc = ac; // wskaźnik na pierwszy element tablicy // możemy tak napisać, bo nazwa tablicy // jest rodzajem wskaźnika, ustawionym 1-szy element pc = &ac[0]; // równoważne pc = ac pc = &ac[n]; // wskaźnik na n-ty element tablicy (numerując od 0) // uwaga! zakres nie jest sprawdzany //
Dereferencja czyli wyłuskanie: operator * int a; char ac[20]; void f(int n) { int b; int* p = &b; char* pc = ac; } // wskaźnik na pojedynczą zmienną lokalną // wskaźnik na pierwszy element tablicy int b2 = *p; // wyłuskanie wartości typu int spod adresu p char c = *ac; // wyłuskanie wartości typu char spod adresu ac // (jest to pierwszy element tablicy ac) // to samo co: char c = ac[0]; c = *(ac+n); // wyłuskanie wartości n-tego elementu tablicy ac // to samo co: c = ac[n]; //
Wskaźniki Wskaźniki są adresami pamięci można je traktować jak pewne wartości całkowite pierwszy bajt pamięci ma adres 0, kolejny 1 itd. 0 1 2 p2 *p2 2^20-1 7 Wartości wskaźników można wyświetlić (ale rzadko się to przydaje) char c1 = 'c'; char* p1 = &c1; int i1 = 7; int* p2 = &i1; cout << "p1==" << p1 << " *p1==" << *p1 << "\n"; // p1==??? *p1==c cout << "p2==" << p2 << " *p2==" << *p2 << "\n"; // p2==??? *p2=7
źródło: http://en.wikipedia.org/wiki/pointer_%28dog_breed%29 Wskaźnik, czyli ang. pointer
Wskaźnik a referencja Referencja to automatycznie wyłuskiwany niemodyfikowalny wskaźnik, mówiąc prościej: przezwisko obiektu podstawienie do wskaźnika zmienia jego wartość podstawienie do referencji zmienia obiekt, do którego się odnosi nie można przestawić referencji na inny obiekt int a = 10; int* p = &a; // potrzebujesz & aby otrzymać adres zmiennej (wskaźnik na nią) *p = 7; // przypisanie wartości 7 do zmiennej a poprzez wskaźnik p // potrzebujesz * (lub [ ]) aby dostać się do tego, na co wskazuje wskaźnik int x1 = *p; // inicjuje zmienną x1 wartością zmiennej a poprzez wskaźnik p int& r = a; r = 9; int x2 = r; // r jest synonimem a // przypisanie wartości 9 do zmiennej a poprzez referencję r // inicjuje zmienną x2 wartością zmiennej a poprzez referencję r p = &x1; r = &x1; // możesz przestawić wskaźnik tak by odnosił się do innego obiektu // błąd: nie możesz przestawić referencji by odnosiła się do innego obiektu
Tablice i wskaźniki void f(int pi[ ]) // równoważne void f(int* pi) { int a[ ] = { 1, 2, 3, 4 }; int b[ ] = a; // błąd: kopiowanie nie jest zdefiniowane dla tablic. b = pi; // błąd: kopiowanie nie jest zdefiniowane dla tablic. // Nazwa tablicy jest niemodyfikowalnym wskaźnikiem pi = a; // dobrze, ale to NIE kopiuje tablicy: // teraz pi wskazuje na 1-szy element a int* p = a; // p wskazuje na pierwszy element a int* q = pi; // q wskazuje na pierwszy element a } pi: najpierw potem a: p: q: 1 2 3 4
Ostrożnie z tablicami i wskaźnikami char* f() { char ch[20]; char* p = &ch[90]; // *p = 'a'; char* q; *q = 'b'; return &ch[10]; // odczytaliśmy adres spoza zarezerwowanej dla ch pamięci // nie wiemy więc co teraz nadpisujemy // zapomnieliśmy zainicjować // nie wiemy co teraz nadpisujemy // O-o!: przecież ch zniknie gdy wyjdziemy z funkcji // a my zostaniemy z niesławnym wiszącym wskaźnikiem } void g() { char* pp = f(); // *pp = 'c'; // nie wiemy więc co teraz nadpisujemy // bo ch z funkcji f już dawno nie ma } Stroustrup/Programming Apr'10 24
Arytmetyka wskaźników char* mychar; short *myshort; long *mylong; // mychar++; myshort++; mylong++; // typ char zajmuje 1 bajt // typ short zajmuje 2 bajty // typ long zajmuje 4 bajty // Przesuwamy się w pamięci o: // 1 bajt // 2 bajty // 4 bajty źródło: http://www.cplusplus.com/doc/tutorial/pointers/ Tak samo działa zwiększanie wskaźnika o dowolną liczbę pozycji, np. mylong+=4 przesunie wskaźnik o 16 bajtów. Nie ma niejawnych konwersji pomiędzy wskaźnikami na różne typy chociaż istnieją niejawne konwersje pomiędzy typami: mychar = myshort; mylong = myshort; *mychar = *myshort; *mylong = *myshort; // błąd! // błąd! // ok // ok
Arytmetyka wskaźników (2) Co będzie wynikiem działania następujących poleceń? Zasady: operator ++ ma wyższy priorytet niż * preinkrementacja ++x zwiększa x przed użyciem postinkrementacja x++ zwiększa x po użyciu int *p; // cout << *p++ ; cout << *++p ; cout << (*p)++; cout << ++*p; // wyłuskaj obiekt, zwiększ wskaźnik, wyświetl obiekt // zwiększ wskaźnik, wyłuskaj obiekt, wyświetl obiekt // wyłuskaj obiekt, wyświetl obiekt, zwiększ obiekt, // wyłuskaj obiekt, zwiększ obiekt, wyświetl obiekt
Tworzenie tablic o rozmiarze określanym w czasie wykonania Często w trakcie pisania programu nie wiemy ile dokładnie pamięci zarezerwować na tablicę Taką tablicę możemy umieścić w pamięci wolnej void funkcja() { int wymiar_n, wymiar_m; cout << ''Podaj wymiary macierzy 2D: ''; cin >> wymiar_n, wymiar_m; int *macierz = new int[wymiar_n][wymiar_m]; // Do tworzenia obiektów // w pamięci wolnej służy służy operator new, // który zawsze zwraca wskaźnik for (n=0;n<wymiar_n;n++) for (m=0;m<wymiar_m;m++) { macierz[n][m]=(n+1)*(m+1); } } delete[ ] macierz; // Po zakończeniu pracy z tablicą w pamięci wolnej // należy ją usunąć operatorem delete[] // W przeciwnym przypadku mamy wyciek pamięci`
Tworzenie dowolnych obiektów w pamięci dynamicznej Alokuj używając new new alokuje obiekt w pamięci wolnej, czasami inicjuje go i zwraca wskaźnik do niego: int* pi = new int; // inicjacja domyślna (nieokreślona dla int) Date* pd = new Date(2012, Date::jan, 11); // inicjacja jawna używamy konstruktor double* pd = new double[10]; // alokacja tablicy (niezainicjowanej ) new rzuca wyjątek bad_alloc() jeśli nie uda się utworzyć obiektu De-alokuj używając delete i delete[ ] delete (dla pojedynczych obiektów) i delete[ ] (dla tablic) zwalnia pamięć zaalokowaną przez new delete pi; // dealokuje pojedyńczy obiekt delete pd; // dealokuje pojedyńczy obiekt, wywołuje destruktor obiektu delete[ ] pd; // dealokuje tablicę, wywołuje destruktor dla każdego elementu tablicy De-alokacja wskaźnika o adresie 0 (czyli null) niczego nie robi char* p = 0; delete p; // nieszkodliwe
Pamięć komputera tak jak widzi ją program: Statyczna alokacja pamięci Kod programu zmienne statyczne, zmienne globalne Pamięć statyczna nienazwane obiekty alokowane dynamicznie Dynamiczna alokacja pamięci Pamięć wolna (sterta, ang. heap) Stos (ang. stack) zmienne automatyczne tzn. zadeklarowane lokalnie, parametry funkcji
Gdzie żyją obiekty int glob[10]; int* some_fct(int n) { int loc[20]; int* wsk = new int[n]; // return wsk; } // tablica globalna żyje zawsze // tablica lokalna żyje do końca zakresu // tablica na stercie żyje aż nie usunięta przez delete[ ] void f() { int* pp = some_fct(17); // delete[ ] pp; // dealokacja tablicy w wolnej pamięci, zaalokowanej przez some_fct() } łatwo zapomnieć usunąć obiekt/tablicę zaalokowaną w pamięci wolnej unikaj new / delete (lub delete[ ]) jeśli możesz
Wektor jest sprytną tablicą Vector jest typem danych zdefiniowanym w bibliotece standardowej C++ vector<t> przechowuje sekwencję wartości typu T w zasadzie można powiedzieć, że wektor jest sprytną tablicą zna size() swój rozmiar i może go zmieniać, współpracuje z algorytmami biblioteki standardowej v: 5 elementy v: v[0] v[1] v[2] 1 4 2 v[3] 3 v[4] 5
vector czy tablica? vector<double> v_liczby; vector<string> v_slowa; vector<reading> v_odczyty; const int rozmiar = 10; double t_liczby[rozmiar]; string t_slowa[rozmiar]; Reading t_odczyty[rozmiar]; const int rozmiar = 10; vector<double> v_liczby2(rozmiar); for (int i=0; i<v_liczby2.size(); ++i) v_liczby2[i] = i*i; for (int i=0; i<rozmiar; ++i) t_liczby[i] = i*i; for (int i=0; i<v_liczby2.size(); ++i) cout << v_liczby2[i] << endl; for (int i=0; i<rozmiar; ++i) cout << t_liczby[i] << endl; Różnica: wektor zna swój rozmiar rozmiar tablicy musimy zapamiętać
vector czy tablica? #include <iostream> #include <string> #include <vector> #include <algorithm> using namespace std; #include <iostream> #include <string> #include <cmath> #include <cstring> using namespace std; while (cin>>s && s!= "quit") words.push_back(s); const int rozmiar=10; string words[rozmiar]; string s; int i=0; while (i<rozmiar && cin>>s && s!= "quit") words[i++] = s; sort(words.begin(), words.end()); qsort(words, rozmiar, sizeof(string), compare); for (int i=0; i<words.size(); ++i) cout<<words[i]<< "\n"; for (i=0; i<rozmiar; ++i) cout<<words[i]<< "\n"; vector<string> words; string s; Różnica: wektor współpracuje z wygodnymi algorytmami biblioteki standardowej int compare (const void * a, const void * b) { return strcmp(((string*)a)->c_str(),((string*)b)->c_str()); } Tablica - nie
Po co zajmować się tablicami? W języku C są tylko tablice w C nie ma wektorów istnieje bardzo dużo kodu w C systemy operacyjne, systemy wbudowane bardzo dużo kodu C++ zostało napisane w stylu C programując w C++ na pewno się z nim spotkasz Tablica jest najprostszą reprezentacją pamięci musisz więc co-nieco o nich wiedzieć ktoś kto napisał kontener vector wykorzystał tablicę Zasada: unikaj tablic, gdy tylko możesz są najczęstszym źródłem błędów w C i C++ są wśród najczęstszych źródeł luk bezpieczeństwa
vector czy tablica? Tam, blisko sprzętu, życie jest proste i brutalne musisz zaprogramować wszystko sam nie pomoże Ci kontrola typów błędy w czasie działania poznaje się po tym, że dane uległy uszkodzeniu lub program się wysypał O nie, chcemy się wyrwać stamtąd tak szybko jak się da chcemy być produktywni i pisać niezawodne programy chcemy używać języka odpowiedniego dla ludzi Dlatego używamy wektorów itp. ale chcemy też poznać techniki tworzenia nowych klas służących do pracy ze strukturami danych
Wskaźniki, tablice i vector Uwaga Używając wskaźników i tablic dotykasz sprzętu przy minimalnym wsparciu języka Stąd przy korzystaniu z nich łatwo o poważny i trudny do znalezienia błąd Należy z nich korzystać tylko wtedy kiedy naprawdę są potrzebne vector jest jednym ze rozwiązań by zachowując prawie całą elastyczność i wydajność tablic skorzystać z większego wsparcia języka (czytaj: mniej błędów, szybsze odrobaczanie)
Dziś najważniejsze było to, że... Tablice i wskaźniki są najprostszą reprezentacją pamięci w C/C++ są bardzo efektywne ale i bardzo błędogenne należy ich unikać gdy to tylko możliwe dobrym zastępstwem dla tablicy jest vector Pozostało jeszcze tyle do nauczenia, np. korzystanie z pamięci wolnej tworzenie typów użytkownika korzystających z pamięci wolnej programowanie zorientowane obiektowo (np. dziedziczenie) programowanie uogólnione (szablony, kontenery, iteratory)
A w przyszłym semestrze Dalsze spotkania z programowaniem na kursie Informatyka język wysokiego poziomu w środowisku Matlab rozwiązywanie problemów numerycznych graficzny interfejs użytkownika programowanie zorientowane obiektowo Ale zanim to nastąpi: kolokwium zaliczeniowe: pon., 16.01.2012, godz.19-21, sala 1.30/C-13 kolokwium poprawkowe: śr. 25.01.2012, godz. 13-15 (w sesji), sala 1.30/C-13
vector<double> v = { 1, 2, 3.456, 99.99 }; template<class E> class vector { public: vector (std::initializer_list<e> s) // initializer-list constructor { reserve(s.size()); // get the right amount of space uninitialized_copy(s.begin(), s.end(), elem); // initialize elements (in elem[0:s.size())) sz = s.size(); // set vector size } Listy inicjacyjne C++11 //... as before }; vector<double> v1(7); v1 = 9; vector<double> v2 = 9; vector<double> v1{7}; v1 = {9}; vector<double> v2 = {9}; // ok: v1 has 7 elements // error: no conversion from int to vector // error: no conversion from int to vector // ok: v1 has 1 element (with its value 7) // ok v1 now has 1 element (with its value 9) // ok: v2 has 1 element (with its value 9) int x0 {7.3}; // error: narrowing int x1 = {7.3}; // error: narrowing; the = is optional double d = 7; int x2{d}; char x3{7}; vector<int> vi = { 1, 2.3, 4, 5.6 }; // error: narrowing (double to int) // ok: even though 7 is an int, this is not narrowing // error: double to int narrowing http://www2.research.att.com/~bs/c++0xfaq.html#init-list