Elektronika - baza wiedzy

Cześć 14 kursu C/C++


14. Tablice


Ponieważ istotę tablic najłatwiej jest pokazać na przykładach, to w następnych kilku podpunktach zostaną zaprezentowane przykładowe programy. Jednak zanim do nich dojdziemy przedstawię ich podstawowe cechy.
Tablic używa się w przypadku, gdy chcemy przechowywać dużą ilość danych tego samego typu przy zachowaniu łatwego do nich dostępu. Mimo tego, że tablica może przechowywać wiele danych jednego typu, odwołujemy się do niej za pomocą jednej nazwy. Jednak aby móc określić, o który dokładnie element nam chodzi, musimy użyć dodatkowo indeksu, czyli kolejnego (liczonego od zera) numeru elementu. Kolejne elementy są umiejscowione w pamięci komputera jeden za drugim.


14.1. Tablice o elementach typu prostego


W poprzednim punkcie zostały przedstawione ogólne informacje dotyczące tablic. Ponieważ jednak suchy tekst nigdy nie wyjaśni tematu tak dobrze jak przykład, posłużymy się właśnie tym narzędziem. Załóżmy, że chcemy napisać program, który obliczy nam średnią z pięciu ocen. Będzie on wyglądał następująco:


#include <stdio.h>

void main(void) {

float srednia;

int i;

float oceny[5];

// wpisujemy do tablicy przykladowe oceny z pieciu przedmiotow

oceny[0] = 3; oceny[1] = 5; oceny[2] = 5; oceny[3] = 3.5; oceny[4] = 3;



// sumujemy wszystkie oceny

srednia = 0;

for(i=0; i<5; i++) srednia += oceny[i];



// dzielimy sume przez ilosc ocen

srednia /= 5;



// wypisujemy wynik na ekranie

printf("Srednia ocen wynosi %1.1f ", srednia);

}



Na początku programu nie ma nic nowego - włączenie pliku nagłówkowego, deklaracja dwóch zmiennych o nazwach srednia i i. Jednak w następnej linijce jest nowa rzecz - jak zapewne się już domyśliłeś jest to właśnie deklaracja tablicy. Przyjrzyjmy się jej uważnie - wygląda ona praktycznie identycznie, jak deklaracja zwykłej zmiennej. Najpierw wpisujemy typ danych, następnie nazwę naszej tablicy (tutaj oceny). Różnica pomiędzy deklaracją zwykłej zmiennej, a tablicą jest widoczna w ostatnim członie. Przy deklaracji tablicy musimy jeszcze podać jej wielkość. Robi się to podając tą wartość w nawiasach klamrowych. W naszym przypadku chcemy obliczyć średnią z pięciu ocen, tak więc zadeklarowaliśmy tablicę o wielkości pięć.
Do poszczególnych elementów w tablicy odwołujemy się przy pomocy indeksu. Indeks jest liczony od zera, tak więc w naszym przypadku do poszczególnych elemetów tablicy możemy się dostać używając indeksów o numerach od zera do cztery. Pokazane jest to w następnej linijce programu - wpisujemy tu kolejne oceny do tablicy. Własnie w dostępie poprzez indeks tkwi cała siła tablic. Szczególnie objawia się to w dwóch następnych linijkach programu. Pomyśl co by było, gdybyś chciał wykorzystać zwykłe zmienne zamiast tablic - musiałbyś zadeklarować pięć osobnych zmiennych typu float (np. o nazwach ocena1, ocena2... itd.), a następnie wpisać coś takiego:

srednia = ocena1 + ocena2 + ocena3 + ocena4 + ocena5;

srednia /= 5;

Przy tej ilości ocen do zsumowania jest to jeszcze akceptowalne, ale jeśli byłoby ich więcej to linijka ta koszmarnie by się wydłużyła. Dzięki temu zaś, że do tablicy możemy odwoływać sie używając indeksu, mogliśmy zawrzeć całe sumowanie w pojedynczej pętli przebiegającej od zera do czterech. W każdym jej przebiegu zmienna srednia zostaje zwiększona o wartość, która jest zawarta w elemencie tablicy o numerze i. Po całkowitym wykonaniu pętli zmienna srednia zawiera sumę wszystkich elementów tablicy i wystarczy ją tylko podzielić przez ich ilość (w naszym przypadku przez pięć), aby otrzymać średnią ocen, która zostanie wyświetlona w ostatniej linijce programu.

14.2. Tablice struktur


Oprócz tablic, których elementy będą typu prostego, w języku C można także budować bardziej złożone tablice. Dla przykładu w tym punkcie utworzymy tablicę struktur. Załózmy, że mamy do czynienia z bardzo małą firmą, w której pracuje trzech pracowników. Dla każdego pracownika chcemy mieć możliwość wyświetlenia informacji o jego pensji oraz numerze identyfikacyjnym. Poza tym chcemy także, aby program wyświetlał raport o kwocie, którą będziemy musieli co miesiąc przeznaczyć na wypłaty dla pracowników. Program realizujący te zadania będzie wyglądał następująco:


#include <stdio.h>


typedef struct {

int nr_id;

float pensja;

} PRACOWNIK;

void main(void) {

float suma_wyplat;

int i;

PRACOWNIK kadra[3]={ {25803, 1299.10}, {25809, 2100}, {7, 1500} };

// wyswietlamy informacje o pracowniku - jego nr id, oraz pensje

for(i=0; i<3; i++)

printf("Nr identyfikacyjny: %d Pensja: %5.2f ",

kadra[i].nr_id, kadra[i].pensja);

// obliczamy kwote potrzebna na wyplaty

suma_wyplat = 0;

for(i=0; i<3; i++) suma_wyplat += kadra[i].pensja;

printf("Suma wyplat wynosi: %5.2f ", suma_wyplat);

}



Zacznijmy analizę tego programu. Początek programu to dla nas nic nowego - deklaracja struktury o nazwie PRACOWNIK oraz dwóch zmiennych. Następna linijka też wygląda już znajomo, mamy tu deklarację tablicy o nazwie kadra, której elementy są strukturą PRACOWNIK. Jednak mamy tu także przykład pokazujący w jaki sposób możemy od razu zainicjalizować tablicę. Tak jak to było w przypadku "zwykłych" zmiennych po nazwie stawiamy znak równości, a następnie wartość, którą chcemy przypisać. Jednak o ile w tamtym przypadku od razu wpisywaliśmy liczbę, czy teżznak, to teraz jest to trochę bardziej skomplikowane. Wszystkie wartości, które chcemy wpisać do tablicy musimy zawrzeć w nawiasach klamrowych, oddzielając je od siebie przecinkami. W naszym przypadku mamy jednak do czynienia z sytuacją, gdzie każdy element tablicy jest także typem złożonym. Musimy więc zastosować tą technikę także oddzielnie dla każdego elementu tablicy, wpisując w nawiasach klamrowych wartości poszczególnych pól struktury - pierwsza wartość zostanie przypisana polu nr_id, natomiast druga wartość polu pensja. Tylko na pierwszy rzut oka wydaje się to strasznie zagmatwane. Wystarczy jednak uruchomić program i porównać wyniki wyświetlone na ekranie z wartościami wpisanymi w tej linijce i wszystko stanie się jasne. Dobrze, przejdźmy do analizy następnej linijki. Mamy tu do czynienia z podobną sytuacją jak w poprzednim punkcie. Pętla przebiega po kolei wszystkie elementy tablicy, jednak o ile w poprzednim programie były one sumowane, to w tym są po prostu wyświetlane na ekranie. Mamy tu także przykład odwołwania się do poszczególnych pól struktury, która jest elementem tablicy. Tak jak to było w przypadku jednej zmiennej strukturalnej, odwołujemy się do poszczególnych pól oddzielając je od nazwy zmiennej przy pomocy kropki. Ostatnie trzy linijki programu są praktycznie identyczne do tych, które były w poprzednim programie - po prostu sumujemy wartości pól pensja wszystkich elementów tablicy i sumę tą wpisujemy do zmiennej suma_wyplat. Otrzymaną wartość wypisujemy na ekranie w ostatniej linijce programu.



14.3. Tablice o wielkości ustalanej przy kompilacji



W tym punkcie zostanie przedstawiona jeszcze jedna ciekawa cecha tablic w języku C, a mianowicie określanie wielkosći tablicy przez kompilator na podstawie danych podanych przy jej automatycznej inicjalizacji. Rozważmy program z poprzedniego punktu, jednak zapisany w nieco inny sposób:



#include <stdio.h>

typedef struct {

int nr_id;

float pensja;

} PRACOWNIK;

void main(void) {

float suma_wyplat;

int i;

PRACOWNIK kadra[]={ {25803, 1299.10}, {25809, 2100}, {7, 1500} };

int wielkosc = sizeof(kadra) / sizeof(PRACOWNIK);


// wyswietlamy informacje o pracowniku - jego nr id, oraz pensje

for(i=0; i < wielkosc; i++)

printf("Nr identyfikacyjny: %d Pensja: %5.2f ",

kadra[i].nr_id, kadra[i].pensja);

// obliczamy kwote potrzebna na wyplaty

suma_wyplat = 0;

for(i=0; i < wielkosc; i++) suma_wyplat += kadra[i].pensja;

printf("Suma wyplat wynosi: %5.2f ", suma_wyplat);


}

Z początku program ten wydaje się być identyczny jak poprzedni. Pierwsza różnica występuje dopiero w linijce, w której mamy deklarację tablicy. Zauważ, że w nawiasach kwadratowych nie została podana jej wielkość. Jednak mimo tego konstrukcja taka jest poprawna, ponieważ kompilator może domyślić się wielkości tablicy na podstawie ilości danych wprowadzonych w części inicjalizacyjnej. Ponieważ wpisaliśmy tam dane o trzech pracownikach to kompilator utworzy tablicę o trzech elementach. Jednak wynika z tego dla nas następne zadanie - musimy określić ile pracowników zawiera tablica, aby móc wyświetlić dane na ekranie. Poprzednio mieliśmy na stałe ustawioną wielkość tablicy na trzy, tak więc obie pętle for ustawiliśmy tak, aby wykonały się trzykrotnie. Zastanawiasz się zapewne po co w takim razie zastosowaliśmy taką konstrukcję, skoro musimy wykonywać dodatkowe prace aby program działał tak jak poprzednio. Otóż rozwiązanie jest proste - pomyśl co by było gdybyś zatrudnił czwartego pracownika. Musiałbyś wpisać go do tablicy i zmienić jej wielkość na cztery. Jednak to nie wszystko - we obu pętlach musiałbyś także zmienić warunek kontynuacji tak, aby wykonywały się cztery razy. A przy zastosowaniu konstrukcji użytej w tym programie jedyne co będziesz musiał zrobić, to wpisać nowego pracownika do tablicy - reszta wykona się automatycznie. Dodatkowy pracę, polegającą na napisaniu wyrażenia obliczającego wielkość tablicy będziesz musiał wykonać tylko jeden raz - podczas pisania programu. Korzyści są chyba oczywiste ? Przejdźmy więc do następnej linijki, która obliczy ilość elementów w tablicy. Zastanówmy się w jaki sposób możaby obliczyć tą wartość. Najprościej jest chyba podzielić wielkość całej tablicy przez wielkość pojedynczego jej elementu - właśnie ten sposób jest zastosowany w naszym programie. Aby uzyskać wielkość jakiejś danej musimy użyć funkcji sizeof . Zwraca ona wielkość (w bajtach) podanej jako parametr danej. W naszym przypadku użyliśmy jej dwa razy. Najpierw do określenia wielkości całej tablicy - "sizeof(kadra)", a następnie do określenia wielkości pojedynczego jej elementu (w naszym przypadku struktury PRACOWNIK) - "sizeof(PRACOWNIK)". Po podzieleniu pierwszej wartości przez drugą otrzymaliśmy ilość elementów w tablicy, którą to ilość przypisaliśmy zmiennej o nazwie wielkosc. Teraz, mając ilość elementów w tablicy, jedyne co musimy zrobić to zamienić w stosunku do poprzedniego programu obie trójki w pętli for na naszą zmienną wielkosc i to wszystko ! Program działa zgodnie z naszymi zamierzeniami.



14.4. Tablice wielowymiarowe


Oprócz jednowymiarowych tablic poznanych w poprzednich podpunktach, język C umożliwia tworzenie tablic wielowymiarowych. Ich zastosowanie zostanie pokazane na przykładzie dwuwymiarowej tablicy, która będzie odpowiadała planszy do gry "w statki." Jako założenie programu przyjmiemy, że polu pustemu odpowiada wartość zero w tablicy, natomiast jeśli na danym polu znajduje się jakiś statek, to ma ono wartość jeden. Poza tym dla uproszczenia programu postawimy dla planszy tylko trzy statki jednomasztowe. A oto jak wygląda taki program:


#include <stdio.h>

void main(void) {

int plansza[10][10];

int i, j;

// wyczyszczenie planszy - wypelnienie jej zerami

for(i=0; i<10; i++)

for(j=0; j<10; j++)

plansza[i][j] = 0;

// ustawienie trzech jednomasztowcow

plansza[3][6] = 1; plansza[8][3] = 1; plansza[2][9] = 1;

// wyswietlenie informacji na ktorych polach znajduja sie statki

for(i=0; i<10; i++)

for(j=0; j<10; j++)

if(plansza[i][j]) printf("Statek znajduje sie na polu %d,%d ", i, j);

}


Zaraz w pierwszej linijce funkcji main mamy do czynienia z deklaracją tablicy dwuwymiarowej. Jak widzisz nie różni się ona mocno od deklaracji zwykłej, jednowymiarowej tablicy. Jedyną różnicą jest to, że występują tu dwie sekcje z nawiasami kwadratowymi, które określają wielkości poszczególnych wymiarów tablicy. W naszym przykładzie obie mają wielkość dziesięć, choć oczywiście mogą mieć różne wymiary. Jeśli chciałbyś utworzyć tablicę o więcej niż dwóch wymiarach, to wystarczy, że dopiszesz jeszcze jedną, lub więcej takich sekcji. Następnie mamy wyczyszczenie naszej tablicy zerami. Odbywa się to przy pomocy dwóch pętli for przebiegających od zera do dziewięciu. Pętla zewnętrzna, w której zwiększana jest zmienna i odpowiada poszczególnym wierszom, natomiast pętla wewnętrzna poszczególnym kolumnom tablicy. Masz tu także przykład w jaki sposób można odwoływać się do poszczególnych elementów tablicy wielowymiarowej. Różnica jest taka, jak przy deklaracji - wystarczy dodać dodatkową sekcję z nawiasami kwadratowymi, w których wpisujemy indeks dla danego wymiaru. W następnej linijce ustawiamy na planszy trzy statki jednomasztowe, czyli po prostu w trzech miejscach w tablicy wpisujemy wartość jeden. Ostatnie trzy linijki służa do wyświetlenia informacji, na których polach znajdują się statki. Sprawdzamy po kolei wszystkie elementy tablicy (czyli naszej planszy do gry) i jeśli którymś z nich ma wartość różną od zera to wyświetlamy informację o pozycji statku.



14.5. Tablice jako parametr funkcji


Długo się zastanawiałem, czy napisać o tym już teraz, w tym punkcie, czy też dopiero w następnym - już po wyjaśnieniu istoty wskaźników. Jednak zdecydowałem się umieścić to tutaj, gdyż mimo wszystko informacje te dotyczą głównie tablic. Jednak jeśli będziesz miał problemy ze zrozumieniem tego podpunktu to przeczytaj najpierw następny punkt o wskaźnikach, a następnie powróć do czytania tego podpunktu.

Na koniec wykładu o tablicach pozostała nam jeszcze do omówienia jedna rzecz, a mianowicie przekazywanie tablic do funkcji jako jeden z parametrów. Wiesz już co to są funkcje oraz jak przekazywać do nich zmienne jako ich parametry, tak więc z samą składniową stroną tego problemu nie powinieneś mieć kłopotów. Jedynym problemem może być zrozumienie istoty przekazywania tablic, ale myślę, że nawet jeśli teraz tego nie zrozumiesz, to po przeczytaniu następnego punktu zrozumiesz to z pewnością.
Jak zapewne pamiętasz z wcześniejszych punktów, w języku C mamy do czynienia z tzw. przekazywaniem przez wartość parametrów do funkcji. Znaczyło to, że nawet jeśli w ciele funkcji zmienna, która została do niej przekazana została zmieniona (tzn. przypisano jej nową wartość) to po powrocie do miejsca wywołania funkcji miała dalej starą wartość. Po prostu w momencie wywołania funkcji została utworzona kopia zmiennej, która została przekazana jako parametr i zmianom ulegałą właśnie ta kopia, a nie oryginalna zmienna. Od reguły tej jest jednak pewien wyjątek. Jak się zapewne domyślasz wyjątkiem tym jest przekazywanie do funkcji tablic. Związane jest to z tym, że tablice mogą mieć na prawdę ogromną wielkość i przy tworzeniu ich kopii mogłoby np. zabraknąć wolnej pamięci. Poza tym samo kopiowanie trwało by dość długo, a język C był zaprojektowany z myślą o jak najszybszym działaniu programów w nim napisanych. Tablice w języku C nie są więc przekazywane przez wartość, lecz zamiast tego przekazuje się tzw. wskaźnik do pierwszego elementu. Innymi słowy do funkcji przekazuje się adres pamięci, pod którym znajduje się pierwszy element tablicy (ułożenie tablicy w pamięci zostało omówione na początku tego punktu).
Aby pokazać sposób w jaki możesz przekazać tablicę do funkcji oraz udowodnić fakt, że nie są one przekazywane przez wartość napisałem program, który to zademonstruje. Przed dalszą lekturą skompiluj go proszę, uruchom i przyjżyj się uważnie wynikom jego działania.




#include <stdio.h>

int Suma(int tab[], int ilosc) {

int i, suma;

// obliczenie sumy wszystkich wartosci w tablicy

suma = 0;

for(i=0; i < ilosc; i++) suma += tab[i];

// robimy to aby udowodnic, ze tablica nie jest, natomiast zwykla zmienna

// jest przekazywana przez wartosc

tab[ilosc-1] = 11; ilosc = 100;

return suma;

}

void main(void) {

int tablica[]={6, 3, 123, 3, 5, 200};

int ilosc = sizeof(tablica) / sizeof(int);

printf("Przed wywolaniem funkcji ostatni element jest rowny %d. ",

tablica[ilosc-1]);

printf("Przed wywolaniem funkcji zmienna ilosc jest rowna %d. ",

ilosc);


printf("Suma wszystkich elementow jest rowna %d. ", Suma(tablica, ilosc));

printf("Po wywolaniu funkcji ostatni element jest rowny %d. ",

tablica[ilosc-1]);

printf("Po wywolaniu funkcji zmienna ilosc jest rowna %d. ",

ilosc);

}



Większość konstrukcji użytych w tym programie powinna być dla Ciebie zrozumiała. Praktycznie wszystko było już użyte w poprzednich programach, z jednym wyjątkiem, a mianowicie deklaracją pierwszego parametru funkcji Suma jako tablicy. Przyjrzyj się sposobowi deklaracji - wygląda ona bardzo podobnie jak deklaracja parametru typu prostego. Jedyną różnicą jest to, że po nazwie podaliśmy jeszcze nawiasy klamrowe. Czyli

int Suma(int tab[], int ilosc)

przeczytamy jako: definicja funkcji o nazwie Suma zwracającej wartość typu int, która przyjmuje dwa parametry, z których pierwszy jest typu tablica intów i nazywa się tab, natomiast drugi jest typu int i nazywa się ilość.
Skoro już wiesz wszystko co potrzebne odnośnie składni zastosowanej w programie możemy przejść do jego analizy.
Zaczynamy, jak zwykle, od funkcji main . Początek to nic nowego - deklaracja tablicy intów o wielkości automatycznie obliczanej przez kompilator oraz deklaracja zmiennej o nazwie ilosc, która tą wielkość będzie przechowywać. Następnie, przy pomocy funkcji printf , wyświetlamy na ekranie zawartość ostatniego elementu tablicy oraz ilość elementów w niej zawartych. Następnej linijce będziemy musieli się przyglądnąć dokładniej - zawiera ona wywołanie funkcji Suma . Wartość, którą ta funkcja zwróciła zostaje przekazana jako parametr do funkcji main i zostaje wyświetlona na ekranie. Funkcja Suma oblicza sumę tylu elementów tablicy, która została przekazana jako pierwszy parametr, ile zostło przekazanych jako parametr numer dwa. W naszym przypadku funkcja ma obliczyć sumę wszystkich (bo zmienna ilość zawiera ilość elementów w tablicy) tablicy o nazwie... tablica. Przyjżyj się dokładnie sposobowi, w jaki przkazaliśmy oba parametry do funkcji Suma - po prostu podaliśmy nazwy zmiennych. Skoro wywołaliśmy naszą funkcję to przejdźmy do analizy jej wnętrza. Początek to nic nowego - sumujemy elementy tablicy w sposób taki, jak w poprzednich programach i uzyskaną wartość przypisujemy zmiennej o nazwie suma. Wartość tą zwracamy do miejsca wywołania przy użyciu return w ostatniej linijce ciała funkcji. Jednak we wcześniejszej linijce użyliśmy dwóch przypisań, aby zaprezentować to, o czym wspomniałem na początku tego podpunktu - tablice są przekazywane przez wskaźnik, w przeciwieństwie do innych typów przekazywanych przez wartość. Aby to udowodnić do ostatniego elementu tablicy wpisaliśmy liczbę jedenaście (na początku było tam dwieście), natomiast zmiennej ilosc przypisaliśmy wartość sto (była tam początkowo wartość sześć ponieważ tyle właśnie jest elementów w naszej tablicy). Po wykonaniu funkcji wyświetlamy ponownie zawartość obu tych danych, aby sprawdzić czy fakt, że zmodyfikowaliśmy je w funkcji return ma jakiekolwiek znaczenie. A oto jaki jest wynik wyświetlony na ekranie po uruchomieniu tego programu:

Przed wywolaniem funkcji ostatni element jest rowny 200.

Przed wywolaniem funkcji zmienna ilosc jest rowna 6.

Suma wszystkich elementow jest rowna 340.

Po wywolaniu funkcji ostatni element jest rowny 11.

Po wywolaniu funkcji zmienna ilosc jest rowna 6.

Jak widzisz wynika z tego jednoznacznie, że o ile w przypadku zmiennej ilosc jej wartość w ogóle się nie zmieniła, to w przypadku tablicy jej wartość uległa zmianie.

Mimo faktu, że język C nie posiada tzw. przekazania przez referencję (czyli odpowiednika var z pascala) to jednak sposób, który kompilator wykorzystuje przy przekazywaniu tablic do funkcji, możemy samodzielnie zastosować także w stosunku do typów prostych przy zastosowaniu tzw. wskaźników. Jest to jednak temat na tyle obszerny, że poświecony zostanie temu zagadnieniu cały następny punkt.