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
0] = 5.5; // definicja - przypisanie wartosci do zmiennych
a[1] = 3.521;
a[2] = 6.45;
a[3] = 4.51; a[
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:
double b[3] = {1.2, 2.4, -4.3}; // wartosci zawarte w nawiasach
// "{" i "}" definiuja tablice
double c[5] = {0}; // wszystkie elementy tablicy zostana
// uzupelnione zerami
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:
double x[10], y[10]; // wspolrzedne pilek
double xV[10], yV[10]; // skladowe predkosci pilek
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ę:
0], y[0], 5); circle(x[
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++) {
0.0;
x[i] = 0.0;
y[i] =
} }
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ść:
10); init(x, y,
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:
double a[3];
0] = 1.2; // inicjalizacja klasyczna z wykorzystaniem "[" i "]"
a[1] = 3.13;
a[2] = 0.22;
a[
0) = 1.2; // inicjalizacja z wykorzstaniem wskaznikow
*(a + 1) = 3.13;
*(a + 2) = 0.22; *(a +
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.for (i = 0; i < N; i++) {
x[i] += xV[i];
y[i] += yV[i]; }
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:for(i = 0; i < 50; i++) {
100); // jako argument funkcji wpisujemy ilość klatek na sekundę (oczekiwanie przez 10 ms)
animate(// czyszczenie okna dla nowej klatki
clear(); // pozostale instrukcje...
}
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];
"Adress of &a = %p, value of a = %d \n", &a, a);
printf("Adress of &b = %p, value of b = %d \n", &b, b);
printf(for (int i = -10; i < 5; i++) {
tab[i] = i;
}
for (int i = -10; i < 10; i++) {
"&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);
printf(
}
int main() {
static_array_overflow();"\n---DONE---\n");
printf(
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ń.↩︎