Podstawy programowania.
Wykład 7
Tablice wielowymiarowe, SOA, AOS, itp.
Tablice wielowymiarowe
➔
C umożliwia definiowanie tablic wielowymiarowych
najczęściej stosowane są tablice dwuwymiarowe
matematycznym odpowiednikiem tablic dwuwymiarowych są macierze
➔
Definicja tablicy k-wymiarowej ma postać:
typ nazwa[N_1][N_2]..[N_k]; // + ewentualne inicjowanie, np.
int tab_int_2D[2][3] = { {0,1,2}, {4,5,6} };
np. dla tablicy znaków przechowującej wiele linii teksu:
#define MAX_DL_L 55 // maksymalna liczba znaków w jednej linii tekstu
#define MAX_N_L 15 // maksymalna liczba linii tekstu char tekst_wieloliniowy[MAX_N_L][MAX_DL_L];
➔
Tablice wielowymiarowe przechowywane są w kolejnych komórkach pamięci
istnieje tylko jeden symbol (nie zmienna) w programie zawierający adres tablicy wielowymiarowej
Tablice wielowymiarowe
➔
Dostęp do elementu tablicy wykorzystuje notację:
int linia = 5; int znak_w_linii = 27;
tekst_wieloliniowy[ linia ][ znak_w_linii ] = 'q';
printf("%c", tekst_wieloliniowy[ 5 ][ 27 ]);
➔ Standardowy sposób przechowywania tablic dwuwymiarowych w C oznacza przechowywanie macierzy wierszami
indeksy elementu tablicy służą do prostego obliczenia jego adresu:
adres = początek + ( linia*MAX_DL_L + znak_w_linii ) * rozmiar_typu
z tego względu tablice wielowymiarowe często zamieniane są na tablice jednowymiarowe:
char tekst_wieloliniowy_1D[ MAX_N_L*MAX_DL_L ];
int linia = 5; int znak_w_linii = 27;
tekst_wieloliniowy_1D[ linia*MAX_DL_L + znak_w_linii ] = 'q';
printf("%c", tekst_wieloliniowy_1D[ 5*MAX_DL_L + 27 ]);
Tablice wielowymiarowe
➔
Przesyłanie tablic wielowymiarowych jako argumentów funkcji realizuje prosta notacja:
funkcja_tab_wielo( nazwa_tab_wielo );
wczytaj_tekst ( tekst_wieloliniowy );
➔
Każdorazowo wewnątrz funkcji musi być możliwe obliczenie adresu dowolnego elementu posługując się indeksami
elementu, jak np. w przypadku przykładowej tablicy dwuwymiarowej:
adres = początek + ( linia*MAX_DL_L + znak_w_linii ) * rozmiar_typu
oznacza to, że poza nazwą tablicy (czyli adresem jej początku)
muszą zostać przekazane dodatkowe parametry określające rozmiar tablicy, np. w przypadku przykładowej tablicy dwuwymiarowej
parametr MAX_DL_L
parametr ten musi znaleźć się w deklaracji funkcji, co oznacza, że musi być znany w trakcie kompilacji! (w klasycznym C)
Tablice wielowymiarowe
➔
Deklaracje funkcji przyjmujących jako argument tablicę
wielowymiarową mogą mieć jedną z równoważnych postaci:
wczytaj_tekst ( char tekst_wieloliniowy [N_L] [MAX_DL_L] );
wczytaj_tekst ( char tekst_wieloliniowy [ ] [MAX_DL_L] );
wczytaj_tekst ( char (*tekst_wieloliniowy)[MAX_DL_L] );
ostatnia postać wykorzystuje utożsamienie nazwy tablicy ze wskaźnikiem
➔
W przypadku alternatywnego przechowywania macierzy w postaci tablic jednowymiarowych:
parametry (wymiary) tablicy nie muszą być znane w czasie
kompilacji – muszą jednak zostać w jakiś sposób przekazane funkcji
deklaracja funkcji:
wczytaj_tekst_1D( char* tekst_wieloliniowy, int max_dl_l);
wywołanie:
wczytaj_tekst_1D( tekst_wieloliniowy, max_dl_l);
Tablice wskaźników
➔
Tablice wielowymiarowe nie są elastycznym i często używanym elementem C
➔
Znacznie częściej stosowane są bardziej elastyczne tablice wskaźników, np. dla poprzedniego przykładu:
#define MAX_N_L 15 // maksymalna liczba linii tekstu
char* tekst_wieloliniowy_tab_wsk[MAX_N_L] = {NULL}; // notacja!
wskaźniki są zainicjowane dla bezpiecznego użycia
ich wartości muszą zostać podstawione jako adresy istniejących zmiennych lub za pomocą funkcji alokacji pamięci, np.
char napis_tab[10] = "..."; tekst_wieloliniowy_tab_wsk[0] = napis_tab;
int dl_l = 25; int linia =5;
tekst_wieloliniowy_tab_wsk[linia] = malloc( dl_l*sizeof(char));
w powyższym przypadku tekst_wieloliniowy_tab_wsk jest symbolem oznaczającym tablicę (jej adres), natomiast składowe tej tablicy są zmiennymi, które mogą przyjmować różne wartości (adresów)
Tablice wskaźników
➔
Wskaźniki będące składowymi tablicy wskaźników mogą pokazywać na rozmaite obiekty w pamięci
wskaźniki muszą zachować zgodność typu wskazywanych obiektów z deklaracją tablicy:
int* tab_wsk_int[10] = {NULL};
double* tab_wsk_d[10] = {NULL};
char* tekst_wieloliniowy_tab_wsk[MAX_N_L] = {NULL};
➔
Dostęp do obiektów wskazywanych przez elementy tablicy wskaźników zależy od tego na co wskazują elementy:
elementy mogą pokazywać na pojedyncze zmienne, np.:
int liczba_int=5; int* tab_wsk_int[10] = {NULL};
tab_wsk_int[0] = &liczba_int;
printf("%d", *tab_wsk_int[0]); // lub printf("%d", tab_wsk_int[0][0]);
• taki sposób daje możliwość dostępu do pojedynczej zmiennej z wielu miejsc (funkcji, struktur) w programie
Tablice wskaźników
➔
Dostęp do obiektów wskazywanych przez elementy tablicy wskaźników zależy od tego na co wskazują elementy:
elementy mogą pokazywać na tablice zmiennych:
double tab_d[10] = {1.0, 2.0, 0};
double* tab_wsk_d[10] = {NULL};
tab_wsk_d[0] = tab_d;
printf("%lf", tab_wsk_d[0][1]);
• taki sposób użycia daje elastyczność stosowania – wskazywane tablice mogą mieć różną długość (jak w poprzednim przykładzie)
elementy mogą pokazywać na struktury
typedef struct mg_punkt_2D{ double x; double y; } mg_punkt_2D;
mg_punkt_2D* koniec[2] = {NULL}; mg_punkt_2D p1 = {0.0,0.0};
koniec[0] = &p1; koniec[0]->x = 1.0;
printf("%lf\n", koniec[0]->x);
• tablice wskaźników do struktur mogą służyć do konstruowania złożonych struktur danych
Tablice wskaźników
➔
Przesyłanie tablic wskaźników jako argumentów funkcji wykorzystuje notacje:
deklaracji funkcji (2 możliwe warianty):
wypisz_tekst( char* tekst_wieloliniowy_tab_wsk[] );
wypisz_tekst( char** tekst_wieloliniowy_tab_wsk );
• notacja char** zwraca uwagę na fakt, że na stosie funkcji
wypisz_tekst będzie znajdowała się zmienna będąca wskaźnikiem do wskaźnika – czyli wskaźnikiem do pierwszego elementu tablicy wskaźników)
wywołania funkcji
wypisz_tekst( tekst_wieloliniowy_tab_wsk );
wewnątrz funkcji musi istnieć sposób poprawnego realizowania dostępu do obiektów wskazywanych przez elementy tablicy
• dla tablic napisów można wykorzystać fakt istnienia '\0' na końcu napisu
Struktury, wskaźniki i lista powiązana
➔
Lista powiązana (linked list) jest strukturą danych zawierającą elementy, tworzące węzły listy, zawierające dowolne dane
lista powiązana jest strukturą dynamiczną, liczba elementów listy może się zmieniać w trakcie wykonania programu
dostęp do elementów listy wykorzystuje zasadę, że każdy element listy wskazuje na element następny
najważniejszym elementem listy jest jej "głowa" – uchwyt (wskaźnik) do elementu początkowego
• "głowa" jest reprezentacją listy w funkcjach korzystających z listy
• chcąc uzyskać dostęp do dowolnego elementu listy należy uzyskać dostęp do jej "głowy", a następnie odwiedzać kolejne węzły
(korzystając ze wskaźników w celu przejścia od jednego węzła do kolejnego), aż do znalezienia szukanego elementu
• inicjowanie listy polega na nadaniu wartości NULL "głowie" listy
końcem listy jest węzeł, który nie wskazuje na żaden kolejny węzeł
Struktury, wskaźniki i lista powiązana
➔
Lista powiązana (linked list)
struktury są wygodnym typem danych do przechowywania zawartości elementów listy:
typedef struct element_listy{
char* nazwa_wezla;
// dowolne inne składniki pojedynczego elementu listy
wsk_el_list nastepny_wezel; // wskaźnik do kolejnego elementu } el_list;
wskaźnik do następnego elementu może posługiwać się typem:
typedef struct element_listy* wsk_el_list;
pojedyncze elementy listy są obiektami typu el_list:
el_list element_1 = { "Węzeł 1", NULL };
operacje na elementach mogą posługiwać się wskaźnikami:
el_list* element_1_wsk = &element_1;
miejsce w pamięci dla elementów listy można alokować dynamicznie
Struktury, wskaźniki i lista powiązana
➔
Lista powiązana (linked list)
zmiana liczby elementów listy polega na wstawianiu kolejnych elementów i ich usuwaniu
węzeł listy można wstawić w dowolne miejsce: na początek
(zmieniając wartość wskaźnika będącego "głową" listy), w środek (pomiędzy dwa węzły) lub na koniec
wstawienie i usunięcie węzła polega zawsze na modyfikacji powiązań pomiędzy węzłami, np.:
int wstaw_na_poczatek( // funkcja zwraca kod sukcesu lub błędu el_list** Glowa_wsk, // lista - identyfikowana przez wskaźnik el_list* Element // wskaźnik do elementu wstawianego na listę ){
// na początku: obsługa błędów danych wejściowych; następnie:
Element->nastepny_wezel=*Glowa_wsk;
*Glowa_wsk=Element; // modyfikacja Głowy używając *Głowa_wsk return(0); }
Struktury, wskaźniki i lista powiązana
➔
Lista powiązana (linked list)
niektóre z operacji na liście mogą wymagać przeglądania kolejnych elementów listy:
int usun_element_listy( // funkcja zwraca kod sukcesu lub błędu el_list** Glowa_wsk, // lista - identyfikowana przez wskaźnik el_list* Element // wskaźnik do elementu usuwanego z listy ) {
// na początku: obsługa błędów danych wejściowych
if(*Glowa_wsk==Element) *Glowa_wsk=Element->nastepny_wezel;
else{
el_list* aktualny_wezel = *Glowa_wsk;
while(aktualny_wezel->nastepny_wezel != Element){
aktualny_wezel = aktualny_wezel->nastepny_wezel; } aktualny_wezel->nastepny_wezel = Element->nastepny_wezel;
} // funkcja może także zwalniać pamięć jeśli była zaalokowana
Przykład złożonej struktury danych
// definicje struktur w pakiecie mg – moja grafika – wersja 1 typedef struct mg_punkt_2D{ // punkt w przestrzeni 2D
double x; // współrzędna x double y; // współrzędna y } mg_punkt_2D;
typedef struct mg_krawedz_2D{ // krawędź w przestrzeni 2D mg_punkt_2D koniec[2]; // punkty będące wierzchołkami } mg_krawedz_2D;
typedef struct mg_trojkat_2D{ // trójkąt w przestrzeni 2D mg_krawedz_2D krawedzie[3]; // krawędzie trójkąta } mg_trojkat_2D;
➔ Uwaga: nie ma punktów wspólnych dla krawędzi, krawędzi wspólnych dla trójkątów itp.
Uwaga: zmiana współrzędnych punktów nie zmienia położenia krawędzi
Przykład złożonej struktury danych
// definicje struktur w pakiecie mg – moja grafika – wersja 2 typedef struct mg_punkt_2D{ // punkt w przestrzeni 2D
double x; // współrzędna x double y; // współrzędna y } mg_punkt_2D;
typedef struct mg_krawedz_2D{ // krawędź w przestrzeni 2D mg_punkt_2D* koniec[2]; // punkty będące wierzchołkami } mg_krawedz_2D;
typedef struct mg_trojkat_2D{ // trójkąt w przestrzeni 2D mg_krawedz_2D* krawedzie[3]; // krawędzie trójkąta } mg_trojkat_2D;
➔ Uwaga: mogą istnieć punkty wspólne dla krawędzi, krawędzie wspólne dla trójkątów itp.
Przykład złożonej struktury danych
// definicje struktur w pakiecie mg – moja grafika – wersja 2.1 typedef struct mg_punkt_2D{ // punkt w przestrzeni 2D double x; // współrzędna x
double y; // współrzędna y
struct mg_trojkat_2D* *trojkaty; // trójkąty zawierające punkt // tablica o długości nieznanej w trakcie kompilacji } mg_punkt_2D;
typedef struct mg_krawedz_2D{ // krawędź w przestrzeni 2D
struct mg_punkt_2D* koniec[2]; // punkty będące wierzchołkami struct mg_trojkat_2D* trojkaty[2]; // trójkąty zawierające krawędź } mg_krawedz_2D;
typedef struct mg_trojkat_2D{ // trójkąt w przestrzeni 2D struct mg_krawedz_2D* boki[3]; // krawędzie (boki) trójkąta struct mg_trojkat_2D* sasiedzi[3]; // sąsiednie trójkąty
Przykład złożonej struktury danych
➔
Definicja struktury obejmującej całą siatkę trójkątów
typedef struct mg_siatka_2D{
int liczba_punktow;
mg_punkt_2D* tablica_punktow;
int liczba_krawedzi;
mg_krawedz_2D* tablica_krawedzi;
int liczba_trojkatow;
mg_trojkat_2D* tablica_trojkatow;
} mg_siatka_2D;
➔
Parametry siatki (liczba punktów, krawędzi, trójkątów) zadawane są w trakcie wykonania programu
➔
Tablice alokowane są w trakcie wykonania
➔
Składowe struktur zawierają wskaźniki do odpowiednich
Przykład złożonej struktury danych
➔
Zastosowanie tablic struktur pozwala na posługiwanie się indeksami w tablicach zamiast wskaźnikami do struktur:
// definicje struktur w pakiecie mg – moja grafika – wersja 2.2 typedef struct mg_punkt_2D{ // punkt w przestrzeni 2D
double x; // współrzędna x double y; // współrzędna y
int *trojkaty; // indeksy trójkątów zawierających punkt } mg_punkt_2D;
typedef struct mg_krawedz_2D{ // krawędź w przestrzeni 2D int koniec[2]; // indeksy punktów będących wierzchołkami int trojkaty[2]; // indeksy trójkątów zawierających krawędź } mg_krawedz_2D;
typedef struct mg_trojkat_2D{ // trójkąt w przestrzeni 2D int boki[3]; // indeksy krawędzi (boków) trójkąta
int sasiedzi[3]; // indeksy sąsiednich trójkątów } mg_trojkat_2D;
Przykład złożonej struktury danych
➔
Zdefiniowane wcześniej struktury danych pozwalają na szybkie wykonywanie dużej liczby operacji na całej siatce (dodawanie nowych punktów, krawędzi, trójkątów)
➔
Istnieją inne możliwe struktury danych wystarczające do realizacji operacji, np. minimalistyczna struktura danych:
➔
// definicje struktur w pakiecie mg – moja grafika – wersja 3 typedef struct mg_punkt_2D{ // punkt w przestrzeni 2D
double x; double y; // współrzędne x i y } mg_punkt_2D;
typedef struct mg_trojkat_2D{ // trójkąt w przestrzeni 2D
int wierzcholki[3]; // indeksy wierzchołków trójkąta w tablicy punktów } mg_trojkat_2D;
typedef struct mg_siatka_2D{
int liczba_punktow; mg_punkt_2D* tablica_punktow;
int liczba_trojkatow; mg_trojkat_2D* tablica_trojkatow;
Przykład złożonej struktury danych
➔
W definicji siatki zastosowane zostały tablice struktur (AOS – array of structures)
➔
W celu zwiększenia wydajności obliczeń, stosuje się często
alternatywne warianty struktur danych zawierające pojedynczą strukturę z tablicami zmiennych elementarnych typów
liczbowych (SOA – structure of arrays)
➔
W przypadku programu grafiki 2D struktura danych SOA mogłaby mieć postać:
typedef struct mg_siatka_2D{
int liczba_punktow; double* wspolrzedne_punktow;
int liczba_trojkatow; int* punkty_trojkatow;
} mg_siatka_2D;
➔
Tak zdefiniowana struktura eliminuje potrzebę definiowania jakichkolwiek innych struktur
ceną jest zmniejszenie czytelności kodu operacji na siatce trójkątów