Celem dzisiejszych zajęć jest wprowadzenie do tablic w języku C. Tablicą (ang. array) nazywamy ciąg zmiennych tego samego typu, które zajmują kolejne komórki pamięci. Aby dostać się do zadanego elementu, używamy nazwy tablicy i indeksu identyfikującego element. Na tych zajęciach zajmiemy się tablicami statycznymi, tzn. takimi, których rozmiar jest określany w momencie deklaracji1. Tablicę statyczną deklarujemy tak jak zwykłą zmienną, przy czym dodatkowo określamy jej długość (czyli liczbę elementów). Przykładowo:
double a[4]; // deklaracja tablicy
a[0] = 5.5; // definicja - przypisanie wartosci do zmiennych
a[1] = 3.521;
a[2] = 6.45;
a[3] = 4.51;
Zwróć uwagę, że elementy tablicy są indeksowane liczbami od \(0\) do \(N - 1\), gdzie \(N\) to rozmiar tablicy. Elementy tablicy można również zainicjalizować natychmiast – w momencie deklaracji:
Zadanie polegać będzie na:
Przykładowy ekran początkowy widoczny jest na Rysunku 1 (strzałki zaznaczono poglądowo).
Nasze piłki przechowywane będą jako zestawy współrzędnych \((x, y)\) oraz prędkości \((xV, yV)\). Oznacza to, że będą potrzebne następujące tablice:
Gdy będziemy chcieli obejrzeć piłki w oknie graficznym, użyjemy
funkcji circle()
. Jako argumenty podamy elementy tablic
odpowiadające danej piłce. Przykładowo, jeśli chcemy obejrzeć pierwszą
piłkę:
for
Większość operacji będziemy wykonywać, używając funkcji, które będą przyjmować wprowadzone wyżej tablice jako argumenty. Funkcje będą musiały także pobierać długość tablic tak, aby operacje można było wykonać dla każdego z jej elementów. Jeśli chcemy np. zainicjalizować wszystkie współrzędne wartością \(0\), napiszemy funkcję:
void init(double *x, double *y, int N) {
int i;
for (i = 0; i < N; i++) {
x[i] = 0.0;
y[i] = 0.0;
}
}
Wykorzystaliśmy tutaj pętlę for
, która pobiera \(3\) argumenty:
Taką funkcję wywołujemy w programie głównym, podając nazwy tablic, na których ma działać oraz ich długość:
Czytelnik zauważy, że funkcja init
pobiera dwa wskaźniki
do tablic (u nas do tablicy x
oraz y
). Oznacza
to, że w momencie wywołania funkcja oczekuje podania adresów tych
tablic. My podaliśmy jedynie ich nazwy (x
i y
)
– wynika stąd, że nazwa tablicy jest jednocześnie jej adresem.
Warto podkreślić, że wykorzystanie wskaźnika to podstawowy sposób na
przekazanie tablicy do funkcji. Tablicy nie da się przekazać przez
wartość, tak jak w przypadku ,,zwykłych’’ zmiennych (int
,
double
, itd.). Można ją przekazać jedynie przez wskaźnik3.
Ponieważ x
oraz y
są wskaźnikami do
pierwszych elementów tablic, można wykorzystać mechanizm działań na
wskaźnikach. Poniższy fragment kodu pokazuje dwa równoważne sposoby
dostępu do wartości zawartej w tablicy:
Przed wykonaniem ćwiczeń upewnij się, że dołączono bibliotekę
winbgi2.h
.
x
, y
,
xV
i yV
) o długości \(10\).initPositions
, która losuje położenia
początkowe kulek tak, aby mieściły się w oknie graficznym. Użyj funkcji
rand()
z biblioteki stdlib.h
(patrz zajęcia
\(4\)).display
, która wyświetli w oknie
graficznym aktualne położenia kulek
init
,showTable
, która drukuje w terminalu
zawartość tablic (tym razem funkcja będzie pobierała cztery wskaźniki i
liczbę).Kulki powinny poruszać się, musimy zatem:
xV
oraz yV
) wylosuj tak, aby ich
wartości znalazły się w przedziale \([-1,
1]\).showTable
sprawdź czy wylosowane
wartości są prawidłowe.move
, która wykona przesunięcie każdej z
piłeczek. Przemieszczenie będzie polegać na zwiększeniu każdej
współrzędnej o odpowiadającą jej składową prędkości.main
napisz pętlę for
, która
wykona \(i = 50\) wywołań funkcji
move
. Pamiętaj żeby po każdym kroku w oknie graficznym były
wyświetlane nowe położenia piłek. W ciele pętli koniecznie użyj funkcji
animate(100)
. Spowolni ona wykonywanie kolejnych kroków
pętli. Jeśli nie dodasz tej funkcji, nie zauważysz, że piłki się
poruszają.animate
wygląda następująco:Chcielibyśmy, aby piłeczki miały wbudowany mechanizm kolizji ze ścianami. Zderzenia są doskonale sprężyste, zatem w układzie współrzędnych związanym ze ścianami, kąt padania będzie równy równy kątowi odbicia.
collideWall
, która sprawdza czy piłka
zderzyła się ze ścianą. W przypadku kolizji należy zastosować prawo
odbicia, które sprowadza się do:
showEnergy
, która w terminalu będzie
wyświetlała wartość całkowitej energii kinetycznej układu.Napisz funkcję collideBall
, która sprawdza czy piłki
zderzają się ze sobą nawzajem. W celu wykonania tego sprawdzenia, należy
wyznaczyć odległość dla każdej pary piłek i sprawdzić czy jest mniejsza
niż \(2\cdot R = 40\) (przyjęliśmy
wcześniej, że promień piłki to \(R =
20\)).
Następnie należy zmodyfikować prędkości piłek zgodnie z poniższymi
wzorami.
Oznaczenia:
\[\mathbf{v}'_1= \mathbf{v}_1 + \Delta \mathbf{v}\] \[\mathbf{v}'_2= \mathbf{v}_2 - \Delta \mathbf{v}\] \[\Delta \mathbf{v} = C \cdot (\mathbf{r}_{2} - \mathbf{r}_{1})\] gdzie: \[C = \frac{(\mathbf{r}_{1}-\mathbf{r}_{2}) \cdot (\mathbf{v}_1-\mathbf{v}_2)}{||\mathbf{r}_{1}-\mathbf{r}_{2}||^2}\]
Założenia:
Analiza:
Z założeń 1. i 2. wynika, że w momencie zderzenia na kulki będzie działać siła o kierunku takim samym jak wektor łączący środki kulek. W naszym przypadku, wektorem tym będzie \[\Delta \mathbf{r} = \mathbf{r}_{2} - \mathbf{r}_{1}.\] Siłę możemy więc zapisać jako proporcjonalną do tego wektora \[\mathbf{F} = A \cdot (\mathbf{r}_{2} - \mathbf{r}_{1})\textrm{, gdzie}\] \(A\) to pewien parametr, który nie zmieni kierunku wektora ale zmieni jego zwrot i wartość.
Z II zasady dynamiki wiemy, że zmiana pędu będzie proporcjonalna do przyłożonej siły. Zatem: \[\Delta \mathbf{p} = B \cdot (\mathbf{r}_{2} - \mathbf{r}_{1}) \textrm{, gdzie}\] rola parametru \(B\) jest taka sama jak parametru \(A\). Informacja ta upraszcza równania zasady zachowania pędu. Możemy teraz zapisać, że nowe pędy kulek to stare wartości \(\pm\) zmiana pędu: \[ \left\{ \begin{array}{rcl} \mathbf{p}'_1 & = & \mathbf{p}_1 + \Delta \mathbf{p} \\ \mathbf{p}'_2 & = & \mathbf{p}_2 - \Delta \mathbf{p} \end{array} \right. \]
W tym momencie mamy \(7\) niewiadomych (\(\mathbf{p}'_1\), \(\mathbf{p}'_2\), \(\Delta \mathbf{p}\), B) i \(6\) równań (\(3\) równania wektorowe). Musimy więc skorzystać także z równania zachowania energii: \[ \frac{m_1||\mathbf{v}_1||^2}{2} + \frac{m_2||\mathbf{v}_2||^2}{2} = \frac{m_1||\mathbf{v}'_1||^2}{2} + \frac{m_2||\mathbf{v}'_2||^2}{2} \]
Biorąc pod uwagę, że masy piłek są takie same równania możemy uprościć: \[\Delta \mathbf{v} = C \cdot (\mathbf{r}_{2} - \mathbf{r}_{1}) \textrm{ (1)}\] \[ \left\{ \begin{array}{rcl} \mathbf{v}'_1 & = & \mathbf{v}_1 + \Delta \mathbf{v} \\ \mathbf{v}'_2 & = & \mathbf{v}_2 - \Delta \mathbf{v} \end{array} \right. \textrm{ (2)} \] \[ ||\mathbf{v}_1||^2 + ||\mathbf{v}_2||^2 = ||\mathbf{v}'_1||^2 + ||\mathbf{v}'_2||^2 \mathrm{ (3)} \] Podstawiamy teraz równanie (2) do (3) otrzymując: \[ 0 = ||\Delta \mathbf{v}||^2 + (\mathbf{v}_1 - \mathbf{v}_2) \Delta \mathbf{v} \textrm{ (4)} \] Do równania (4) wstawiamy równanie (1) i otrzymujemy wzór na \(C\): \[C = \frac{(\mathbf{r}_{1}-\mathbf{r}_{2}) \cdot (\mathbf{v}_1-\mathbf{v}_2)}{||\mathbf{r}_{1}-\mathbf{r}_{2}||^2} \textrm{ (5)}\] Otrzymany parametr \(C\) możemy podstawić do równania (1), które z kolei podstawiamy do równania (2) otrzymując szukane wyrażenia na prędkości po zderzeniu.
Zderzenie piłek ilustruje poniższy schemat:
Celem lepszego zrozumienia działania stosu i statycznej alokacji
pamięci przanalazuj poniższy kod. Jest on niepoprawny, ponieważ w pętli
for
wartości są wpisywane do komórek tablicy
tab
poza jej zadeklarowanym rozmiarem. Wyjście poza tablice
może spowodować “bałagan” w pamięci skutkujący zupełnie nieoczekiwanym i
trudnym do zdebugowania zachowaniem się programu. Częstym objawem są
“niedeterministyczne” wyniki.
Możliwość wyjścia poza tablice w przypadku programów użytkowych czy serwerów, może być wykorzystane do umyślnego nadpisania fragmentów pamięci. Taki atak określany jest jako stack buffer overflow. Najprostsze zabezpieczenie polega na określeniu maksymalnej ilości pamięci która może być wczytana/skopiowana do bufora.
Sprawdź wynik uruchomienia programu w wersji ‘release/debug’ oraz ‘x84/x64’.
a
i b
?b
zostaje nadpisane tylko w wersji
‘release’?a
i b
zaalokowane są “bliżej” tablicy tab
?tab
.void static_array_overflow() {
int a = 123;
int b = 456;
int tab[5];
printf("Adress of &a = %p, value of a = %d \n", &a, a);
printf("Adress of &b = %p, value of b = %d \n", &b, b);
for (int i = -10; i < 5; i++) {
tab[i] = i;
}
for (int i = -10; i < 10; i++) {
printf("&tab[%d] = %p tab[%d] = %d \n ", i, &tab[i], i, tab[i]);
}
printf("Adress of &a = %p, value of a = %d \n", &a, a);
printf("Adress of &b = %p, value of b = %d \n", &b, b);
}
int main() {
static_array_overflow();
printf("\n---DONE---\n");
return 0;
}
W konfiguracji ‘release’ kompilator dokonuje optymalizacji programu. Dzięki temu działa on z pełną prędkością natomiast utrudnione jest jego debugowanie. Przykładowo linie kodu mogą mieć zmienioną kolejność wykonywania, a niektóre funkcje mogą być rozwinięte (inline). Inline oznacza zamienienie przez kompilator wywołania funkcji na jej bezpośrednie instrukcje. Dzięki temu program nie musi ‘skakać’ po pamięci.
W konfiguracji ‘debug’ kompilator załącza informacje (w pliku .pdb) pozwalające określić które instrukcje assemblera odpowiadają konkretnej lini kodu. Wadą jest zajmowanie przez program większej ilości pamięci i wolniejsze działanie.
Bardziej zaawansowany mechanizm alokacji tablic będzie tematem następnych zajęć↩︎
Teoretycznie możemy w tym miejscu wykonać dowolną operację, jednak dla czytelności kodu zazwyczaj zwiększamy licznik pętli↩︎
Można wykorzystać struktury aby przekazać tablicę przez wartość – temat ten wykracza jednak po za zakres ćwiczeń.↩︎