LEKCJA 12. WskaĽniki i tablice w C i C++. ________________________________________________________________ W czasie tej lekcji: 1. Dowiesz się więcej o zastosowaniu wskaĽników. 2. Zrozumiesz, co maj± wspólnego wskaĽniki i tablice w języku C/C++. ________________________________________________________________ WSKA¬NIKI I TABLICE W C i C++. W języku C/C++ pomiędzy wskaĽnikami a tablicami istnieje bardzo ¶cisły zwi±zek. Do ponumerowania elementów w tablicy służ± tzw. INDEKSY. W języku C/C++ * KAŻDA OPERACJA korzystaj±ca z indeksów może zostać wykonana przy pomocy wskaĽników; * posługiwanie się wskaĽnikiem zamiast indeksu na ogół przyspiesza operację. Tablice, podobnie jak zmienne i funkcje wymagaj± przed użyciem DEKLARACJI. Upraszczaj±c problem - komputer musi wiedzieć ile miejsca zarezerwować w pamięci i w jaki sposób rozmie¶cić kolejne OBIEKTY, czyli kolejne elementy tablicy. [???] CO Z TYMI OBIEKTAMI ? ________________________________________________________________ OBIEKTEM w szerokim znaczeniu tego słowa jest każda liczba, znak, łańcuch znaków itp.. Takimi klasycznymi obiektami języki programowania operowały już od dawien dawna. Prawdziwe programowanie obiektowe w dzisiejszym, węższym znaczeniu rozpoczyna się jednak tam, gdzie obiektem może stać się także co¶ "nietypowego" - np. rysunek. Jest to jednak wła¶ciwy chyba moment, by zwrócić Ci uwagę, że z punktu widzenia komputera obiekt to co¶, co zajmuje pewien obszar pamięci i z czym wiadomo jak postępować. ________________________________________________________________ Deklaracja: int A[12]; oznacza: należy zarezerwować 12 KOLEJNYCH komórek pamięci dla 12 liczb całkowitych typu int (po 2 bajty każda). Jednowymiarowa tablica (wektor) będzie się nazywać "A", a jej kolejne elementy zostan± ponumerowane przy pomocy indeksu: - zwróć uwagę, że w C zaczynamy liczyć OD ZERA A NIE OD JEDYNKI; A[0], A[1], A[2], A[3], .... A[11]. Je¶li chcemy zadeklarować: - indeks i; - wskaĽnik, wskazuj±cy nam pocz±tek (pierwszy, czyli zerowy element) tablicy; - sam± tablicę; to takie deklaracje powinny wygl±dać następuj±co: int i; int *pA; int A[12]; Aby wskaĽnik wskazywał na pocz±tek tablicy A[12], musimy go jeszcze zainicjować: pA = &A[0]; Je¶li poszczególne elementy tablicy s± zawsze rozmieszczane KOLEJNO, to: *pA[0] oznacza: "wyłuskaj zawarto¶ć komórki pamięci wskazanej przez wskaĽnik", czyli inaczej - pobierz z pamięci pierwszy (zerowy!) element tablicy A[]. Je¶li deklaracja typów elementów tablicy i deklaracja typu wskaĽnika s± zgodne i poprawne, nie musimy się dalej martwić ile bajtów zajmuje dany obiekt - element tablicy. Zapisy: *pA[0];€€€€€€€€*pA;€€€€€€€€€€€A[0] *(pA[0]+1)€€€€€*(pA+1)€€€€€€€€A[1] *(pA[0]+2)€€€€€*(pA+2)€€€€€€€€A[2]€€€€€€itd. s± równoważne i oznaczaj± kolejne wyrazy tablicy A[]. Je¶li tablica jest dwu- lub trójwymiarowa, pocz±tek tablicy oznacza zapis: A[0][0]; A[0][0][0]; itd. Zwróć uwagę, że wskaĽnik do tablicy *pA oznacza praktycznie wskaĽnik do POCZˇTKOWEGO ELEMENTU TABLICY: *pA == *pA[0] To samo można zapisać w języku C++ w jeszcze inny sposób. Je¶li A jest nazw± tablicy, to zapis: *A oznacza wskazanie do pocz±tku tablicy A, a zapisy: *(A+1)€€€€€€€€€€€€*(pA+1)€€€€€€€€A[1] *(A+8)€€€€€€€€€€€€*(pA+8)€€€€€€€€A[8] itd. s± równoważne. Podobnie identyczne znaczenie maj± zapisy: x = &A[i]€€€€€€€€x=A+i *pA[i]€€€€€€€€€*(A+i) Należy jednak podkre¶lić, że pomiędzy nazwami tablic (w naszym przykładzie A) a wskaĽnikami istnieje zasadnicza różnica. WskaĽnik jest ZMIENNˇ, zatem operacje: pA = A; pA++; s± dopuszczalne i sensowne. Nazwa tablicy natomiast jest STAŁˇ, zatem operacje: A = pA;€€€€€€€€€€€€€¬LE ! A++;€€€€€€€€€€€€€€€€¬LE ! s± niedopuszczalne i próba ich wykonania spowoduje błędy ! DEKLAROWANIE I INICJOWANIE TABLIC. Elementom tablicy, podobnie jak zmiennym możemy nadawać watro¶ci. Warto¶ci takie należy podawać w nawiasach klamrowych, a wielko¶ć tablicy - w nawiasach kwadratowych. Przykład int WEKTOR[5]; Tablica WEKTOR jest jednowymiarowa i składa się z 5 elementów typu int: WEKTOR[0]....WEKTOR[4]. Przykład float Array[10][5]; Tablica Array jest dwuwymiarowa i składa się z 50 elementów typu float: Array[0][0], Array[0][1]......Array[0][4] €€€€€€€Array[1][0], Array[1][1]......Array[1][4] €€€€€........................................... €€€€€ Array[9][0], Array[9][1]......Array[9][4] Przykład const int b[4]={1,2,33,444}; Elementom jednowymiarowej tablicy (wektora) b przypisano warto¶ći: b[0]=1; b[1]=2; b[2]=33; b[3]=444; Przykład int TAB[2][3]={{1, 2, 3},{2, 4, 6}}; €€€€€TAB[0][0]=1€€€€TAB[0][1]=2€€€€TAB[0][2]=3 €€€€€TAB[1][0]=2€€€€TAB[1][1]=4€€€€TAB[1][2]=6 Przykład : Tablica znakowa. Obie formy zapisu daj± ten sam efekt. char hej[5]="Ahoj"; char hej[5]={'A', 'h', 'o', 'j'}; €€€€€hej[0]='A'€€€€€hej[1]='h'€€€€€hej[2]='o' itp. Przykład : Tablica uzupełniona zerami przez domniemanie. float T[2][3]={{1, 2.22}, {.5}}; kompilator uzupełni zerami do postaci: €€€€€T[0][0]=1€€€€€€T[0][1]=2.22€€€€€€€€T[0][2]=0 €€€€€T[1][0]=0.5€€€€T[1][1]=0€€€€€€€€€€€T[1][2]=0 Je¶li nawias kwadratowy zawieraj±cy wymiar pozostawimy pusty, to kompilator obliczy jego domnieman± zawarto¶ć w oparciu o podan± zawarto¶ć tablicy. Nie spowoduje więc błędu zapis: char D[]="Jakis napis" int A[][2]={{1,2}, {3,4}, {5,6}} Je¶li nie podamy ani wymiaru, ani zawarto¶ci: int A[]; kompilator "zbuntuje się" i wykaże bł±d. Dla przykładu, skompiluj program przykładowy. Zwróć uwagę na sposób zainicjowania wskaĽnika. [P023.CPP] # include "stdio.h" # include int a[][2]={ {1,2},{3,4},{5,6},{7,8},{9,10},{11,12} }; char b[]={ "Poniedzialek" }; int i; int *pa; char *pb; void main() { pa = &a[0][0]; pb = b; // lub pb = b[0]; clrscr(); for (i=0; i<12; i++) printf("%d\t%c\n", *(pa+i), *(pb+i)); getch(); } Zwróć uwagę, że w C++ każdy wymiar tablicy musi mieć swoj± parę nawiasów kwadratowych. Dla przykładu, tablicę trójwymiarow± należy deklarować nie tak TAB3D[i, j, k] lecz tak: int i, j, k; ... TAB3D[i][j][k]; Jest w dobrym stylu panować nad swoimi danymi i umieszczać je w tzw. BUFORACH, czyli w wydzielonych obszarach pamięci o znanym adresie, wielko¶ci i przeznaczeniu. W następnym programie przykładowym utworzymy taki bufor w postaci tablicy bufor[20] i zastosujemy zamiast funkcji scanf() czytaj±cej bezpo¶rednio z klawiatury parę funkcji: gets() - GET String - pobierz łańcuch znaków z klawiatury do bufora; sscanf(bufor) - odczytaj z bufora (z pamięci). Aby unikn±ć nielubianego goto stosujemy konstrukcję for - break. Dokładniej pętlę for omówimy w trakcie następnej lekcji. Ponieważ mam nadzieję, że "podstawow±" postać pętli for pamiętasz z przykładów LOOP-n: for(i=1; i<100; i++) { ... } pozwalam sobie trochę wyprzedzaj±co zastosować j± w programie. Niepodobny do Pascala ani do Basica zapis wynika wła¶nie z tego, że skok następuje bezwarunkowo. Nagłówek pętli for * nie inicjuje licznika pętli (zbędne typowe i=1); * nie sprawdza żadnego warunku (zbędne i<100), * nie liczy pęti (i=i+1 lub i++ też zbędne !). [P024.CPP] # include # include int liczba, ile = 0, suma = 0; void main() { char bufor[20]; clrscr(); printf("podaj liczby - ja oblicze SREDNIA i SUMA\n"); printf("ZERO = KONIEC\n"); for(;;) // Wykonuj petle BEZWARUNKOWO { gets(bufor); sscanf(bufor, "%d", &liczba); suma += liczba; ile++; if (liczba == 0) break; // JESLI ==0 PRZERWIJ PETLE } printf("Suma wynosi: %d\n", suma); printf("Srednia wynosi: %d\n", (suma / ile)); getch(); } Poniżej trochę bardziej "elegancka wersja" z zastosowaniem pętli typu while. Więcej o pętlach dowiesz się z następnej Lekcji. [P025.CPP] # include # include int liczba, ile=1, suma=0; void main() { char bufor[20]; clrscr(); printf("podaj liczby - ja oblicze SREDNIA i SUMA\n"); printf("ZERO = KONIEC\n"); gets(bufor); sscanf(bufor, "%d", &liczba); while (liczba != 0) { suma += liczba; gets(bufor); sscanf(bufor, "%d", &liczba); if(liczba == 0) printf("I to by bylo na tyle...\n"); else ile++; } printf("Suma wynosi: %d\n", suma); printf("Srednia wynosi: %d\n", suma / ile); getch(); } Program powyższy, choć operuje tablic±, robi to trochę jakby za kulisami. Utwórzmy zatem inn± - bardziej "dydaktyczn±" tablicę, której elementy byłyby łatwo rozpoznawalne. PRZYKŁADY TABLIC WIELOWYMIAROWYCH. Dzięki matematyce bardziej jeste¶my przyzwyczajeni do zapisu tablic w takiej postaci: €€€€€€€€€€a11€€a12€€a13€€a14€€a15€€a16 €€€€€€€€€€a21€€a22€€a23€€a24€€a25€€a26 €€€€€€€€€€a31€€a32€€a33€€a34€€a35€€a36 €€€€€€€€€€a41€€a42€€a43€€a44€€a45€€a46 gdzie a i,j /** indeks**/ oznacza element tablicy zlokalizowany w: - wierszu i - kolumnie j Przypiszmy kolejnym elementom tablicy następuj±ce warto¶ci: €€€€€€€€€€11€€€12€€€13€€€14€€€15€€€16 €€€€€€€€€€21€€€22€€€23€€€24€€€25€€€26 €€€€€€€€€€31€€€32€€€33€€€34€€€35€€€36 €€€€€€€€€€41€€€42€€€43€€€44€€€45€€€46 Jest to tablica dwuwymiarowa o wymiarach 4WIERSZE X 6KOLUMN, czyli krócej 4X6. Liczby będ±ce elementami tablicy s± typu całkowitego. Je¶li zatem nazwiemy j± TABLICA, to zgodnie z zasadami przyjętymi w języku C/C++ możemy j± zadeklarować: int TABLICA[4][6]; Pamiętajmy, że C++ liczy nie od jedynki a od zera, zatem TABLICA[0][0] = a11 = 11, TABLICA[2][3] = a34 = 34 itd. Znaj±c zawarto¶ć tablicy możemy j± zdefiniować/zainicjować: int TABLICA[4][6]={{11,12,13,14,15,16},{21,22,23,24,25,26} {31,32,33,34,35,36},{41,42,43,44,45,46}}; Taki sposób inicjowania tablicy, aczkolwiek pomaga wyja¶nić metodę, z punktu widzenia programistów jest trochę "nieelegancki". Liczbę przypisywan± danemu elementowi tablicy można łatwo obliczyć. TABLICA[i][j] = (i+1)*10 + (j+1); Przykładowo: TABLICA[2][5] = (2+1)*10 +(5+1) = 36 Najbardziej oczywistym rozwi±zaniem byłoby napisanie pętli int i, j; for (i=0; i<=3; i++) €€€€€{ for (j=0; j<=5; j++) €€€€€€€€€€{ TABLICA[i][j] = (i+1)*10 + (j+1);} €€€€€} Spróbujmy prze¶ledzić rozmieszczenie elementów tablicy w pamięci i odwołać się do tablicy na kilka sposobów. [P026.CPP] int TABLICA[4][6]={{11,12,13,14,15,16},{21,22,23,24,25,26}, {31,32,33,34,35,36},{41,42,43,44,45,46}}; # include # include int *pT; int i, j; void main() { clrscr(); printf("OTO NASZA TABLICA \n"); for (i=0; i<=3; i++) { for (j=0; j<=5; j++) printf("%d\t", TABLICA[i][j]); printf("\n"); } printf("\n\Inicjujemy wskaĽnik na poczatek tablicy\n"); printf("i INKREMENTUJEMY wskaĽnik *pT++ \n"); pT=&TABLICA[0][0]; for (i=0; i<4*6; i++) printf("%d ", *(pT+i)); getch(); } Zwróć uwagę, że je¶li tablica ma wymiary A * B (np. 3 * 4) i składa się z k=A*B elementów, to w C++ zakres indeksów wynosi zawsze 0, 1, 2, .....A*B-2, A*B-1. Tak więc tablica 10 x 10 (stuelementowa) będzie składać się z elementów o numerach 0...99, a nie 1...100. [P027.CPP] # include # include int TABLICA[4][6]; int *pT; int i, j; void main() { clrscr(); printf("Inicjujemy tablice\n"); for (i=0; i<4; i++) €€€€€for (j=0; j<6; j++) €€€€€{ TABLICA[i][j] = (i+1)*10 + (j+1); } // INDEKS! printf("OTO NASZA TABLICA \n"); for (i=0; i<=3; i++) { for (j=0; j<=5; j++) printf("%d\t", TABLICA[i][j]); printf("\n"); } printf("\n\Inicjujemy wskaĽnik na poczatek tablicy\n"); printf("i INKREMENTUJEMY wskaĽnik *pT++ \n"); pT=&TABLICA[0][0]; for (i=0; i<4*6; i++) printf("%d ", *(pT+i)); getch(); } RĘCZNE I AUTOMATYCZNE GENEROWANIE TABLIC WIELOWYMIAROWYCH. Aby nabrać wprawy, spróbujmy pomanipulować inn± tablic±, znan± Ci prawie "od urodzenia" - tabliczk± mnożenia. Jest to kwadratowa tablica 10 x 10, której każdy wyraz opisuje się prost± zależno¶ci± T(i,j)=i*j. Je¶li przypomnimy sobie, że indeksy w C++ zaczn± się nie od jedynki a od zera, zapis ten przybierze następuj±c± formę: int T[10][10]; T[i][j] = (i+1)*(j+1); Do pełni szczę¶cia brak jeszcze wskaĽnika do tablicy: int *pT; i jego zainicjowania pT = &T[0][0]; I już możemy zaczynać. Mogliby¶my oczywi¶cie zainicjować tablicę "na piechotę", ale to i nieeleganckie, i pracochłonne, i o pomyłkę łatwiej. Pamiętaj, że komputer myli się rzadziej niż programista, więc zawsze lepiej jemu zostawić możliwie jak najwięcej roboty. [P028.CPP] # include # include int T[10][10]; int *pT; int i, j, k; char spacja = ' '; void main() { clrscr(); printf("\t TABLICZKA MNOZENIA (ineksy)\n"); for (i=0; i<10; i++) { for (j=0; j<10; j++) { T[i][j] = (i+1)*(j+1); if (T[i][j]<10) printf("%d%c ", T[i][j], spacja); else printf("%d ", T[i][j]); } printf("\n"); } printf("\n Inicjujemy i INKREMENTUJEMY wskaĽnik *pT++ \n\n"); pT=&T[0][0]; for (k=0; k<10*10; k++) { if (*(pT+k) < 10) printf("%d%c ", *(pT+k) , spacja); else printf("%d ", *(pT+k)); if ((k+1)%10 == 0) printf("\n"); } getch(); } Po wynikach jednocyfrowych dodajemy trzy spacje a po dwucyfrowych dwie spacje. Po dziesięciu kolejnych wynikach trzeba wstawić znak nowego wiersza. Sprawdzamy te warunki: if (*(pT+k) < 10) - je¶li wynik jest mniejszy niż 10... lub if (T[i][j] < 10); if ((k+1) % 10 == 0) - je¶li k jest całkowit± wielokrotno¶ci± 10, czyli - je¶li reszta z dzielenia równa się zero... Zastosowane w powyższych programach nawiasy klamrowe {} spełniaj± rolę INSTRUKCJI GRUPUJˇCEJ i pozwalaj± podobnie jak para BEGIN...END w Pascalu zamkn±ć w pętli więcej niż jedn± instrukcję. Instrukcje ujęte w nawiasy klamrowe s± traktowane jak pojedyncza instrukcja prosta. Tablice mog± zawierać liczby, ale mog± zawierać także znaki. Przykład prostej tablicy znakowej zawiera następny program przykładowy. [P029.CPP] # include # include char T[7][12]={"Poniedzialek", "Wtorek", "Sroda", "Czwartek", "Piatek", "Sobota", "Niedziela"}; char *pT; int i, j, k; char spacja=' '; void main() { clrscr(); pT =&T[0][0]; printf("\t TABLICA znakowa (ineksy)\n\n"); for (i=0; i<7; i++) { for (j=0; j<12; j++) printf("%c ", T[i][j] ); printf("\n"); } printf("\n\t Przy pomocy wskaĽnika \n\n"); for (k=0; k<7*12; k++) { printf("%d ", *(pT+k) ); //TU! - opis w tek¶cie if ((k+1)%12 == 0) printf("\n"); } getch(); } Nazwy dni maj± różn± długo¶ć, czym więc wypełniane s± puste miejsca w tablicy? Je¶li w miejscu zaznaczonym komentarzem //TU! zmienisz format z printf("%c ", *(pT+k) ); na printf("%d ", *(pT+k) ); uzyskasz zamiast znaków kody ASCII. TABLICA znakowa (ineksy) P o n i e d z i a l e k W t o r e k S r o d a C z w a r t e k P i a t e k S o b o t a N i e d z i e l a Przy pomocy wskaĽnika: 80 111 110 105 101 100 122 105 97 108 101 107 87 116 111 114 101 107 0 0 0 0 0 0 83 114 111 100 97 0 0 0 0 0 0 0 67 122 119 97 114 116 101 107 0 0 0 0 80 105 97 116 101 107 0 0 0 0 0 0 83 111 98 111 116 97 0 0 0 0 0 0 78 105 101 100 122 105 101 108 97 0 0 0 Okaże się, że puste miejsca zostały wypełnione zerami. Zero w kodzie ASCII - NUL - '\0' jest znakiem niewidocznym, nie było więc widoczne na wydruku w formie znakowej printf("%c"...). [Z] ________________________________________________________________ 1. Posługuj±c się wskaĽnikiem i inkrementuj±c wskaĽnik z różnym krokiem - np. pT += 2; pT += 3 itp., zmodyfikuj programy przykładowe tak, by uzyskać wydruk tylko czę¶ci tablicy. 2. Spróbuj zast±pić inkrementację wskaĽnika pT++ dekrementacj±, odwracaj±c tablicę "do góry nogami". Jak należałoby poprawnie zainicjować wskaĽnik? 3. Napisz program drukuj±cy tabliczkę mnożenia w układzie szesnastkowym - od 1 * 1 do F * F. 4. Wydrukuj nazwy dni tygodnia pionowo i wspak. 5. Zinterpretuj następuj±ce zapisy: int *pt_int; float *pt_float; int p = 7, d = 27; float x = 1.2345, Y = 32.14; void *general; pt_int = &p; *pt_int += d; general = pt_int; pt_float = &x; Y += 5 * (*pt_float); general = pt_float; const char *name1 = "Jasio"; // wskaĽnik do STALEJ char *const name2 = "Grzesio"; // wskaĽnik do STALEGO ADRESU ________________________________________________________________