User Tools

Site Tools


dydaktyka:cprog:2016:arrays

This is an old revision of the document!


Tablice

Tablica to ciąg wartości tego samego typu (np. liczb) przechowywanych obok siebie.

Tablice jednowymiarowe

Deklarowanie

Uogólniona deklaracja tablicy:

typ_danych nazwa_tablicy[liczba_elementow];

przy czym dostępne typy elementów są identyczne, jak dla “zwykłych” zmiennych (int, float itd.)

Przykład:

int tab[8]; // tablica osmiu wartosci typu calkowitego

Definiowanie

Definicja tablicy polega na przypisaniu do tablicy podanej w nawiasach klamrowych listy wartości kolejnych elementów (rozdzielonych przecinkami).

Przykład:

int tab[4] = {3, 2, 5, 1};

W przypadku podania zbyt małej liczby wartości do inicjalizacji, pozostałe elementy tablicy otrzymują wartość 0.

Dostęp do elementów

Dostęp do poszczególnych elementów tablicy można uzyskać za pomocą indeksu elementu, będącego zawsze liczbą naturalną, umieszczonego w nawiasach kwadratowych.

:!: W języku C indeksowanie zaczyna się od zera – czyli aby uzyskać wartość pierwszego elementu piszemy tab[0]!

Przykład:

int tab[] = {1, 2, 3, 4};
int elem = tab[2];  // przypisz zmiennej `elem` wartosc TRZECIEGO elementu tablicy `tab`

Zadania

Zadanie SUM1D

Stwórz jednowymiarową tablicę wartości zmiennoprzecinkowych zainicjalizowaną wybranymi przez siebie wartościami, a następnie oblicz sumę jej elementów.

Wskazówka: Skorzystaj z pętli for.

Zadanie MAX1D

Napisz program, który wyznaczy największą wartość w jednowymiarowej tablicy liczb całkowitych.

Zadanie REV

Napisz program, który odwróci kolejność elementów tablicy “w miejscu”, czyli bez deklarowania tablicy pomocniczej.
Przykład: {1, 4, 3}{3, 4, 1}.

Wskazówka: Stosowanie “zwykłej” zmiennej pomocniczej jest dozwolone!

Jak wyświetlać tablicę?

Cóż… Niestety, język C nie pozwala na wyświetlanie tablicy jedną instrukcję.
Tablicę trzeba wyświetlać element po elemencie.

Podobnie w przypadku, gdy to użytkownik ma podawać wartości elementów tablicy, trzeba to zrobić element po elemencie (najlepiej – w pętli!).

Jak "elastycznie" określać rozmiar tablicy?

Standard ANSI C nie pozwala na używanie zmiennych bądź stałych do określania rozmiaru tablicy podczas deklaracji:

int n = 3;
int tab[n];  // blad
const int n = 3;
int tab[n];  // blad

:!: Oznacza to więc, że nie ma możliwości utworzenia (statycznie) tablicy o rozmiarze zadanym przez użytkownika! – Nie pobieraj od użytkownika rozmiaru tablicy.
Można to zrobić korzystając z dynamicznej alokacji pamięci, ale to temat na jedno z ostatnich laboratoriów…

Z pomocą przychodzą stałe symboliczne.

Stałą symboliczną tworzy się z użyciem derektywy preprocesora #define, na przykład:

#define N   3

Preprocesor to program uruchamiany przed właściwą kompilacją (stąd jego nazwa: preprocesor), a zajmuje się on m.in. dołączaniem plików nagłówkowych (poprzez znaną Ci derektywę #include). Derektywa #define mówi “zamień w tekście programu ciąg znaków po lewej stronie na ciąg znaków po prawej stronie”. W tym przypadku – ciąg znaków N na ciąg znaków 3.
Zatem po tym, jak preprocesor skończy swoją robotę, kod

#define N   3
...
int tab[N];

wygląda następująco (to widzi kompilator):

int tab[3];

Preprocesor działa więc momentami jak cenzor, którzy przed dostarczeniem listu do adresata zmienia to i owo – coś wykreśla, coś dodaje… ;-)

Derektywy #define umieszcza się na początku kodu programu, zaraz pod derektywami #include.

Można tworzyć wiele stałych symbolicznych – każdą w osobnej linii programu:

#define R   3
#define C   3

(to będzie przydatne przy tworzeniu tablic wielowymiarowych)

:!: Stosuj stałe symboliczne wszędzie tam, gdzie normalnie powinien występować rozmiar tablicy (tj. dany jej wymiar) – w deklaracjach, w pętlach for itd.

Dlaczego nie należy wychodzić poza zakres tablicy?

W poniższym przykładzie mimo, że tablica składa się z trzech elementów (a więc poprawne indeksy to 0, 1 i 2), możemy odwołać się do elementów spoza zakresu – ani kompilator nie zgłosi błędów, ani (w tym przypadku) nie wystąpi błąd podczas wykonania programu:

#include <stdio.h>
 
int main()
{
    int tab[3] = {1, 2, 3};
 
    printf("%d\n", tab[-1]);
    printf("%d\n", tab[3]);
 
    return 0;
}

Tyle, że… wartości tab[-1] i tab[3] zawierają “śmieci”. W dodatku ta pamięć nie należy już do zmiennej tab.
W ogólnym przypadku odwoływanie się do nie-swojej pamięci może spowodować błąd działania programu.

Tablice wielowymiarowe

Deklarowanie

Uogólniona deklaracja tablicy wielowymiarowej:

typ_danych nazwa_tablicy[rozmiar_1][rozmiar_2]...[rozmiar_n];

na przykładzie dwuwymiarowym:

typ_danych nazwa_tablicy[ilosc_wierszy][ilosc_kolumn];

Przykład:

int tab[8][3]; // tablica o osmiu wierszach i trzech kolumnach, wartosci typu calkowitego

Komputer przechowuje w pamięci taką tablicę dwuwymiarową liniowo, jako kilka następujących po sobie tablic jednowymiarowych.

Definiowanie

Tablicę wielowymiarową można zdefiniować na dwa sposoby:

  • używając “zagnieżdżonych” klamer (pierwszy zestaw → piewszy rząd, drugi zestaw → drugi rząd itd.)
  • stosując “płaską” definicją – wszystkie wartości znajdują się w jednych klamrach, przypisywane są do kolejnych elementów wierszami (najpierw całkowicie zapełniany jest pierwszy wiersz, potem drugi itd.)

Przykład sposobu z “zagnieżdżaniem” klamer:

int tab[2][3] = {
        {3, 2, 6},
        {5, 1, 9}
    };

Przykład sposobu “płaskiego”:

int tab[2][3] = {3, 2, 6, 5, 1, 9};  // zdecydowanie mniej czytelne,
                                     // w miare mozliwosci unikac!

Dostęp do elementów

Podobnie jak w przypadku tablicy jednowymiarowej, dostęp uzyskuje się przez podanie w nawiasach kwadratowych numerów pozycji dla kolejnych wymiarów.
Dla przypadku dwuwymiarowego, podaje się najpierw numer wiersza, a potem numer kolumny.

Przykład:

int tab[3][2];
tab[2][0] = 5;  // przypisz wartosc elementowi w 3. wierszu w 1. kolumnie

Poniższy rysunek pomoże Ci załapać tę zasadę:

Zadania

Zadanie SUM2D

Zadeklaruj tablicę dwuwymiarową $A_{N \times M}$. Przypisz do każdego z jej elementów $a(i,j)$ wartość $i + 2 \cdot j$, gdzie $i$ – numer wiersza, a $j$ – numer kolumny. Oblicz sumę elementów leżących w co drugim wierszu, zaczynając od pierwszego wiersza.

Wskazówka 1: Użyj zagnieżdżonych pętli for zarówno do inicjalizacji elementów tablicy, jak i do obliczenia sumy elementów.
Wskazówka 2: Zastosuj osobny zestaw pętli dla inicjalizacji i osobny dla sumowania.

Tablice bezwymiarowe

Kompilator C może automatycznie obliczyć rozmiar tablicy, jeśli tablica zostanie zadeklarowana jako bezwymiarowa (czyli programista nie poda ilości elementów tablicy).

Jeśli kompilator napotka, przykładowo, zapis

int tab[] = {1,2,3};

to utworzy tablicę na tyle dużą, by pomieścić wszystkie te elementy.

:!: Kompilator jest w stanie obliczyć tylko jeden wymiar tablicy – pierwszy od lewej. Pozostałe wymiary muszą zostać podane:

int tab[][2] = {{1,2}, {3,4}, {5,6}};  // OK
int tab[][] = {{1,2}, {3,4}};  // blad

Operator "sizeof"

Operator sizeof zwraca rozmiar operandu (w bajtach). Używa się go podobnie do wywołania funkcji, przy czym argumentem może być nazwa zmiennej lub typ danych:

int s = sizeof(char);  // wartosc `s` to 1, bo tyle wynosi rozmiar typu 'char'
int s2 = sizeof(s);  // wartosc `s2` to (prawdopodobnie) 4

Operator ten można wykorzystać, aby otrzymać ilość elementów tablicy:

int tab[5];
int ilosc_elementow = sizeof(tab) / sizeof(int);

Łańcuchy znaków

Język C nie posiada specjalnego typu łańcuchowego. Zamiast tego, łańcuchy przechowywane są w tablicach elementów typu char. Kolejne znaki, z których składa się łańcuch, znajdują się w kolejnych komórkach pamięci, a na ich końcu znajduje się znak zerowy (null character).

Znak zerowy nie jest cyfrą zero, tylko znakiem niedrukowanym o kodzie ASCII równym 0. Łańcuchy w języku C są przechowywane razem z tym znakiem. Oznacza to, że tablica musi mieć długość przynajmniej o jedną komórkę większą niż długość zapisanego w niej łańcucha.
Literał znaku zerowego to '\0'.

Przykład – łańcuch znaków "Hello":

:!: Znaki cudzysłowu nie są częścią łańcucha. Informują one jedynie kompilator, że to, co znajduje się pomiędzy nimi, jest łańcuchem – podobnie jak apostrofy sygnalizują stałą znakową.

Wyświetlanie i wczytywanie

Do wyświetlania łańcuchów znaków służy specyfikator konwersji %s:

printf("Miasto: %s\n", "Krakow");

Do wczytywania łańcuchów znaków najwygodniej wykorzystać funkcję gets(), która odczytuje znaki do momentu napotkania znaku końca linii (ENTER) i zapisuje je jako łańcuch w tablicy znaków – sam znak końca linii nie jest kopiowany. Funkcja automatycznie dołącza do wczytanych znaków znak zerowy, tworząc tym samym łańcuch znaków.

char imie[15];
printf("Podaj swoje imie: ");
gets(imie);

Oczywiście możesz też użyć funkcji scanf() ze specyfikatorem konwersji %s:

char imie[15];
printf("Podaj swoje imie: ");
scanf("%s", imie);  // `imie` to tablica, a nazwa tablicy to rownoczesnie ADRES jej 
                    // pierwszego elementu - nie trzeba zatem dodawac '&' przed nazwa tablicy

:!: Pamiętaj, żeby wczytywać znaki do wystarczająco dużej tablicy (aby zmieściły się wszystkie znaki i jeszcze znak zerowy)!

Przydatne funkcje

Do badania długości łańcucha znaków – czyli ilości znaków faktycznie tworzących łańcuch, z pominięciem znaku zerowego – służy funkcja strlen().
Zwróć uwagę, że sizeof i strlen zwracają różne wartości dla tablicy przechowującej łańcuch znaków, gdyż łańcuch może nie zajmować całej tablicy (uruchom poniższy program):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main ()
{
    char str[10] = "ABC"; // lancuch zajmuje 4 elementy tablicy
 
    printf("sizeof = %d , strlen = %d\n", sizeof(str), strlen(str));
 
    return 0;
}

Aby określić typ znaku (np. czy dany znak jest cyfrą) warto skorzystać z biblioteki ctype.h.

Zadania

Zadanie IDL

Napisz program, który pobiera imię i nazwisko użytkownika (używając jednego wywołania funkcji scanf()), a następnie wyświetla: w pierwszym wierszu imię i nazwisko, a w drugim liczbę liter w imieniu oraz liczbę liter w nazwisku.

Na przykład:

Podaj imie i nazwisko: Jan Nowak
 
Jan Nowak
3 5

Wskazówka: Przechowuj imię i nazwisko w osobnych tablicach.

Przydatne operatory arytmetyczne

Arytmetyczne operatory przypisania

Język C udostępnia arytmetyczne operatory przypisania <op>= (gdzie <op> oznacza operator arytmetyczny: +, -, *, / bądź %): instrukcja x <op>= y; jest równoważna x = x <op> (y);

Przykład:

int a = 5;
int b = 2;
a *= 3;      // nowa wartosc `a` to 5 * 3 = 15
b *= 3 + 1;  // nowa wartosc `b` to 2 * (3 + 1) = 8

Inkrementacja i dekrementacja

Operatory inkrementacji (++) i dekrementacji (--) powodują odpowiednio zwiększenie bądź zmniejszenie wartości zmiennej o 1.

Są one dostępne w dwóch trybach:

  • przedrostkowym (pre-incrementation) – najpierw następuje zwiększenie wartości zmiennej, potem dalsze operacje na zwiększonej wartości; np. ++i
  • przyrostkowym (post-incrementation) – najpierw wykonywane są operacje (na pierwotnej wartości), a dopiero potem następuje zwiększenie wartości zmiennej; np. i++

Przykład:

int a1 = 3;
int a2 = 3;
int b_pre = 2 * ++a1;  //po wykonaniu instrukcji: b_pre = 6, a1 = 3
int b_post = 2 * a2++; //po wykonaniu instrukcji: b_post = 4, a2 = 3
 
printf("a = %d, b_pre = %d, b_post = %d\n", b_pre, b_post);

:!: Zwróć uwagę na to, że np. i+1 to nie to samo co i++:

int tab[3] = {1, 2, 3};
int i = 0;
printf("%d\n", tab[i++]);  // wypisz i-ty element ORAZ (nastepnie) zwieksz `i` o 1
// teraz `i` wynosi 1
printf("%d\n", tab[i+1]);  // wypisz (i+1)-wszy element (ale nie zmnieniaj wartosci `i`)
// `i` nadal wynosi 1

Operatory inkrementacji i dekrementacji działają tylko dla zmiennych (a ściślej: dla modyfikowalnych l-wartości). Oznacza to, że instrukcja int a = 6++; jest błędna.

Stosuj powyższe operatory ostrożnie – zwiększają one zwięzłość kodu, lecz łatwo popełnić błąd w zapisie.

"Co za dużo to niezdrowo!"

Nie łącz zbyt wielu operatorów w jednym wyrażeniu, nie rób “optymalizacji” długości kodu kosztem czytelności – w ten sposób unikniesz wielu błędów. W szczególności:

  • Nie stosuj operatora inkrementacji lub dekrementacji do zmiennej, która jest częścią więcej niż jednego argumentu funkcji.
  • Nie stosuj operatora inkrementacji lub dekrementacji do zmiennej, która w wyrażeniu pojawia się więcej niż jeden raz.

Na przykład po wykonaniu kodu

int n = 3;
int y = n++ + n++;

y może mieć albo wartość 6 – jeśli kompilator dwukrotnie użyje starej wartości – albo wartość 7 (3 + 4), jeśli zwiększenie nastąpi przed obliczeniem sumy. Standard języka C nie narzuca jednego słusznego sposobu obliczania takiego wyrażenia.

Zadania podsumowujące

dydaktyka/cprog/2016/arrays.1481008862.txt.gz · Last modified: 2020/03/25 11:46 (external edit)