Prednáška 15
- Oznamy
- Opakovanie smerníkov
- Dvojrozmerné polia
- Príklad: výšková mapa
- Hra life
- Polia reťazcov
- Vstupy do funkcie main
- Deklarácie so smerníkmi a poľami
- Zhrnutie
Oznamy
- Prednáška budúci pondelok 16.11. bude mimoriadne v učebni M-IV (v F2 sa koná vedecká konferencia).
Opakovanie smerníkov
Smerníky na jednoduché premenné:
int n = 7; // premenná typu int
int *p = NULL; // smerník na premennú typu int
p = &n; // p ukazuje na n, *p a n sú zhruba to isté
*p = 8; // v premennej n je teraz 8
n = (*p)+1; // v premennej n je teraz 9
Smerníky a polia, alokovanie poľa:
int a[3];
int * b = a; // a,b sú teraz takmer rovnocenné premenné
*b = 3;
b[1] = 4;
a[2] = 5; // v poli sú teraz čísla 3,4,5
b = malloc(sizeof(int) * a[1]);
// b teraz ukazuje na nové pole dĺžky 4
free(b); // uvoľníme pamäť alokovanú pre nové pole
Dvojrozmerné polia
- Doteraz sme pracovali s jednorozmerným poľom, čo však ak potrebujeme dvojrozmerné pole, maticu?
- Príklady dvojrozmerných polí:
- dvojrozmerné tabuľky napr. body študentov z domácich úloh,
- matice z algebry (budúci semester),
- rastrové obrázky.
- Podobne môžeme potrebovať aj polia väčších rozmerov, pracuje sa s nimi analogicky ako s dvojrozmernými.
Dvojrozmerné polia s konštantnou veľkosťou
- Ak je veľkosť dvojrozmerného poľa vopred známa konštanta, môžeme ho
vytvoriť veľmi jednoducho
- napr.
int a[2][5]vytvorí tabuľku s dvomi riadkami a piatimi stĺpcami a[i][j]je potom prvok na riadkuia stĺpcij- Rozmery poľa musíme uviesť aj ak pole posielame do funkcie,
napr.
void vypis(int a[2][5])
- napr.
- Tento spôsob však nebudeme ďalej používať, lebo väčšinou chceme rozmery prispôsobiť potrebám daného vstupu.
#include <stdio.h>
int main(void) {
/* Vytvoríme pole s dvoma riadkami a piatimi stľpcami
* a rovno ho aj inicializujeme: */
int a[2][5] = { {1,2,3,4,5}, {6,7,8,9,10} };
/* Vypíšeme pole ako tabuľku: */
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 5; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
}
Dvojrozmerné pole pomocou poľa smerníkov
- Omnoho flexibilnejšou alternatívou sú polia smerníkov.
- Každý riadok tabuľky bude jedno dynamicky alokované pole a smerník na jeho začiatok si uložíme do poľa smerníkov.
- Tabuľka s
mriadkami ansĺpcami bude v pamäti uložená nejako takto:
Nasledujúci program načíta rozmery dvojrozmernej tabuľky, potom jej prvky a spočíta priemer čísel v každom stĺpci.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int m, n;
printf("Zadaj pocet riadkov: ");
scanf("%d", &m);
printf("Zadaj pocet stlpcov: ");
scanf("%d", &n);
/* Alokuj pole smerníkov na riadky: */
int **a;
a = malloc(sizeof(int *) * m);
/* Alokuj jednotlivé riadky: */
for (int i = 0; i < m; i++) {
// a[i] je smerník na i-ty riadok
a[i] = malloc(sizeof(int) * n);
}
/* Načítanie prvkov tabuľky: */
printf("Zadaj cisla tabulky:\n");
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// načítaj j-ty prvok i-teho riadku
scanf("%d", &a[i][j]);
}
}
/* Spočítaj a vypíš priemery jednotlivých stĺpcov: */
for (int j = 0; j < n; j++) {
int sum = 0;
for (int i = 0; i < m; i++) {
sum += a[i][j];
}
printf("Priemer hodnot v stlpci %d je %f\n", j, ((double)sum) / m);
}
/* Uvoľnenie pamäte: */
for (int i = 0; i < m; i++) {
free(a[i]);
}
free(a);
}
Cvičenie:
- Ako by ste spočítali priemery stĺpcov vstupnej tabuľky s využitím iba jednorozmerného poľa? (Celú tabuľku teda nechceme ukladať.)
- Ako by vyzeralo vytvorenie a použitie trojrozmernej tabuľky?
- Ako by sme navzájom vymenili prvý a druhý riadok tabuľky? Ako prvý a druhý stĺpec?
Príklad: výšková mapa
Pokračujme ukážkou o niečo väčšieho programu využívajúceho dvojrozmerné tabuľky (matice).
- Maticu uložíme ako dynamicky alokované pole smerníkov.
- V programe je niekoľko funkcií, ktoré sa môžu zísť aj v iných programoch na prácu s maticami.
int **vytvorMaticu(int m, int n)alokuje pamäť pre maticu smriadkami anstĺpcami a vráti smerník na pole smerníkov.void zmazMaticu(int **a, int m)uvolní pamäť alokovanú pre maticuasmriadkami (počet stĺpcov nepotrebujeme).void nacitajMaticu(int **a, int m, int n)dostane už alokovanú maticuasmriadkami anstĺpcami a vyplní ju číslami načítanými zo vstupu.
Cieľ programu
Náš program bude v obdĺžnikovej tabuľke celých čísel uchovávať výškovú mapu.
- Bude to obdĺžniková tabuľka s
mriadkami anstĺpcami. - Každé políčko obsahovať nadmorskú výšku od
0po2000metrov nad morom (nadmorská výška0znamená more a kladná nadmorská výška znamená pevninu). - Program na vstupe najprv dostane rozmery tabuľky
mana následne nadmorské výšky všetkých štvorčekov. - Takto zadanú mapu program vykreslí pomocou knižnice
SVGdraw, pričom každý štvorček dostane určitú farbu podľa svojej nadmorskej výšky. - Následne zavolá funkciu
najvyssiVrch, ktorá nájde najvyšší bod (resp. jeden z najvyšších bodov) vykresľovaného územia a v mape ho zvýrazní rámikom.
Príklad vstupu a výstupu:
22 11
0 0 0 0 0 0 0 0 0 0 0
0 0 0 40 80 100 120 140 120 0 0
0 40 80 120 160 200 240 280 190 100 0
0 60 120 180 240 300 360 420 260 100 0
0 80 160 240 320 400 480 560 160 0 0
0 80 120 300 400 500 600 0 0 0 0
0 0 80 160 180 600 0 0 0 0 0
0 0 0 0 0 0 0 0 70 0 0
0 0 0 0 0 800 960 700 200 0 0
0 0 0 540 420 900 700 500 0 0 0
0 0 220 600 800 1000 1200 1400 680 0 0
0 20 240 660 880 1100 1320 1540 750 100 0
0 40 280 720 960 1200 1440 1680 820 100 0
0 60 420 780 1040 1300 1560 1820 1200 400 0
0 80 360 840 1120 1400 1680 1960 1500 600 0
0 40 280 720 960 1200 1440 1680 1000 400 0
0 100 220 600 800 1000 1200 1400 680 100 0
0 60 120 480 640 800 960 1120 540 100 0
0 20 0 0 480 600 720 840 400 100 0
0 0 0 240 320 400 480 560 260 100 0
0 0 0 0 160 200 240 280 0 0 0
0 0 0 0 0 0 0 0 0 0 0
Program výšková mapa
#include <stdio.h>
#include <stdlib.h>
#include "SVGdraw.h"
/* veľkosť štvorčeka mapy v pixeloch */
#define SIZE 15
int **vytvorMaticu(int m, int n) {
/* Vytvorí a vráti maticu (obdĺžnikovú tabuľku)
* s m riadkami a n stĺpcami. */
int **a;
a = malloc(sizeof(int *) * m);
for (int i = 0; i < m; i++) {
a[i] = malloc(sizeof(int) * n);
}
return a;
}
void zmazMaticu(int **a, int m) {
/* Uvoľní z pamäte maticu s m riadkami. */
for (int i = 0; i < m; i++) {
free(a[i]);
}
free(a);
}
void nacitajMaticu(int **a, int m, int n) {
/* Načíta hodnoty do už vytvorenej matice
* veľkosti m krát n. */
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
scanf("%d", &a[i][j]);
}
}
}
void nastavFarbu(int vyska, Drawing drawing) {
/* podľa nadmorskej výšky nastaví farbu čiary
* aj výplne
* modrá -- more (nadmorská výška 0)
* zelená -- nížiny (nadmorská výška 1,...,200)
* hnedá -- "pohoria" (nadmorská výška 200,...,2000) */
// premenné pre červenú, zelenú a modrú zložku farby
int r, g, b;
// nastavenie farby podľa hodnoty
if (vyska == 0) { // modré more
r = 0;
g = 0;
b = 255;
} else if (vyska <= 200) { // zelené nížiny
double x = vyska / 200.0;
r = x * 255;
g = 127 + x * 127;
b = 0;
} else { // žlto-hnedé hory
double x = (vyska - 200) / 1800.0;
r = 255 - x * 150;
g = 255 - x * 200;
b = 0;
}
/* Nastaví farbu čiary aj výplne na dané hodnoty. */
setLineColorRGB(drawing, r, g, b);
setFillColorRGB(drawing, r, g, b);
}
void vykresliStvorcek(int riadok, int stlpec, Drawing drawing) {
/* Vykresli štvorcek pre daný riadok a stĺpec mapy.
* Použije pri tom aktuálne nastavené farby.
* Pozor, pri vykresľovaní
* uvádzame najskôr x (stĺpec), potom y (riadok) */
drawRectangle(drawing, stlpec * SIZE,
riadok * SIZE,
SIZE, SIZE);
}
void vykresliMapu(int **a, int m, int n, Drawing drawing) {
/* Vykresli mapu rozmerov m krát n do obrázku.
* Jednotlivé štvorčeky mapy ofarbi podľa ich nadmorskej výšky */
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
nastavFarbu(a[i][j], drawing);
vykresliStvorcek(i, j, drawing);
}
}
}
void maximumMatice(int **a, int m, int n, int *riadok, int *stlpec) {
/* Nájde v matici a s rozmermi m krát n
* políčko s maximálnou hodnotou
a jeho súradnice uloží do premenných *riadok, *stlpec. */
*riadok = 0;
*stlpec = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (a[i][j] > a[*riadok][*stlpec]) {
*riadok = i;
*stlpec = j;
}
}
}
}
int main(void) {
/* načítaj rozmery matice */
int m, n;
scanf("%d %d", &m, &n);
/* vytvor a načítaj maticu */
int **a = vytvorMaticu(m, n);
nacitajMaticu(a, m, n);
/* zobraz maticu, pozor výmena súradnice */
Drawing drawing = drawingInit(n * SIZE, m * SIZE,
"mapa.svg");
vykresliMapu(a, m, n, drawing);
/* nájdi najvyšší vrch a zvýrazni ho štvorčekom */
int riadok, stlpec;
maximumMatice(a, m, n, &riadok, &stlpec);
setLineColor(drawing, "black");
setLineWidth(drawing, 3);
setNoFill(drawing);
vykresliStvorcek(riadok, stlpec, drawing);
/* ukonči vykresľovanie */
drawingFinish(drawing);
/* uvoľni pamäť matice */
zmazMaticu(a, m);
}
Hra life
Hra life je jednoduchá simulácia kolónie buniek, ktorá má zaujímavé teoretické vlastnosti.
- Máme mriežku m x n štvorčekov, v každom žije najviac 1 bunka.
- Bunky sa rodia a umierajú podľa toho, koľko majú susedov v ôsmych
okolitých políčkach.
- Ak v čase t má bunka 2 alebo 3 susedov, zostane žiť aj v čase t+1, inak zomiera.
- Ak v čase t má prázdne políčko presne 3 susedov, narodí sa tam v čase t+1 nová bunka.
Stav hry si môžeme pamätať v matici boolovských hodnôt.
Príklad vstupu
Pre jednoduchosť vstup uvádzame ako nuly a jednotky bez medzier (1=živá bunka). Výslednú animáciu nájdete tu.
20 20
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000111111111100000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
00000000000000000000
Rátanie zmeny v matici
Stav v čase t máme v matici a, do matice b chceme dať stav v čase t+1.
int zratajOkolie(int m, int n, bool **a, int riadok, int stlpec) {
/* počet zivých prvkov v okolí */
int sucet = 0;
for (int i = riadok - 1; i <= riadok + 1; i++) {
for (int j = stlpec - 1; j <= stlpec + 1; j++) {
/* treba ošetriť okraje matice */
if (i >= 0 && i < m && j >= 0 && j < n && a[i][j]) {
sucet++;
}
}
}
/* samotný štvorček nechceme zarátať */
if (a[riadok][stlpec]) {
sucet--;
}
return sucet;
}
void prepocitajMaticu(int m, int n, bool **a, bool **b) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
/* spočítaj počet živých prvkov v okolí */
int pocet = zratajOkolie(m, n, a, i, j);
/* priraď do b[i][j] hodnotu podľa okolia a[i][j] */
b[i][j] = (pocet == 3 || (pocet == 2 && a[i][j]));
}
}
}
Ďalšie detaily programu
- Celý program, ktorý načíta vstup a simuluje 10 krokov hry Life aj s vytvorením animácie: Program Life
- Prepočítavanie chceme opakovať v cykle pre viacero časových intervalov.
- Môžeme prekopírovať celú maticu z b späť do a, ale rýchlejšie je len vymeniť smerníky.
for (int i = 0; i < 10; i++) {
/* podľa a spočítaj maticu do b */
prepocitajMaticu(m, n, a, b);
/* vymeň smerníky, aby v a bola nová matica */
bool **tmp = b;
b = a;
a = tmp;
}
vytvorMaticu,zmazMaticua pod. prepíšeme tak, aby robili s maticou boolovských hodnôt namiesto intov.- V animácii na začiatku vykreslíme celú maticu, potom prekreslíme vždy len tie bunky, ktoré sa zmenili.
Polia reťazcov
- Dvojrozmerné polia v C nemusia mať všetky riadky rovnako dlhé. To sa hodí napríklad na ukladanie viacerých reťazcov.
- Spomeňte si, že v C je reťazec jednoducho pole
char-ov, kde za posledným znakom ide špeciálny znak 0. - Pole reťazcov bude teda dvojrozmerné pole
char-ov. - Môžeme načítavať napr. vstup po riadkoch, pričom každý riadok načítame do dlhého poľa, ktoré by malo stačiť a potom do prekopírujeme do akurát veľkého riadku v poli.
- Nakoniec program riadky vypíše odzadu.
- Ak by sme vopred alokovali
NMAXriadkov, každý veľkostiMAXRIADKOV, vyšlo by potenciálne na zmar oveľa viac pamäte.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define NMAX 1000
#define MAXRIADOK 1000
int main(void) {
char *a[NMAX]; // pole smerníkov na char
char riadok[MAXRIADOK]; // pole pre načítavanie riadkov
int n = 0;
while (true) {
// načítame jeden riadok
char *result = fgets(riadok, MAXRIADOK, stdin);
// ak je koniec súboru alebo sa minuli položky poľa a,
// končíme načítavanie
if (result == NULL || n == NMAX) {
break;
}
// alokujeme pamäť pre n-tý reťazec poľa a
// pozor, k strlen pridáme 1 kvôli 0 na konci
a[n] = malloc(sizeof(char) * (strlen(riadok) + 1));
// prekopírujeme riadok
strcpy(a[n], riadok);
n++;
}
// vypíšeme riadky odzadu
for (int i = n - 1; i >= 0; i--) {
fputs(a[i], stdout);
}
// uvoľníme pamäť
for (int i = 0; i < n; i++) {
free(a[i]);
}
}
Cvičenia:
- Prečo nemáme na konci programu
free(a)? - Čo by sa stalo, ak by sme namiesto
a[n] = malloc(sizeof(char) * (strlen(riadok) + 1)); strcpy(a[n], riadok)dalia[n] = riadok? - Prerobte program tak, aby namiesto poľa
afixnej veľkostiNMAXpoužíval dynamické pole. - Vedeli by sme dynamické polia nejako použiť aj na načítavanie jednotlivých riadkov?
Vstupy do funkcie main
Často vidíte v programoch funkciu main s nasledujúcou hlavičkou:
int main(int argc, char ** argv) {
Vstupné argumenty argc a argv sa používajú pri spúšťaní programu na
príkazovom riadku.
argvje pole C-čkových reťazcov aargcje počet reťazcov v tomto poli- Prvý reťazec,
argv[0], je meno samotného programu a ostatné sú argumenty programu - Videli sme napríklad spúšťanie kompilátora na príkazovom riadku:
gcc program.c -o program
- Tu
gccje meno programu, ktoré je nasledované tromi argumentami"program.c","-o"a"program". Tieto argumenty dávajú kompilátoru informáciu o tom, ktorý zdrojový súbor má kompilovať a kam má uložiť spustiteľný program.
Nasledujúci jednoduchý program vypíše všetky argumenty, ktoré dostal z príkazového riadku, vrátane mena programu.
#include <stdio.h>
int main(int argc, char ** argv) {
for (int i = 0; i < argc; i++) {
printf("%s\n", argv[i]);
}
}
Deklarácie so smerníkmi a poľami
Príklady na prácu s maticami uvedené vyššie môžete modifikovať vo vlastných programoch, ale je dobré trochu lepšie rozumieť logike práce so smerníkmi v C/C++.
- Operátor
[ ]je zľava asociatívny, t.j.a[2][3]je to isté ako(a[2])[3] - Operátor
*je sprava asociatívny, t.j.**pje to isté ako*(*p). - Operátor
[ ]má vyššiu prioritu ako*, t.j.*a[2]je to isté ako*(a[2]). - Ak
xje pole smerníkov naint, tak*(x[2])znamená, že vezmeme polex, pozrieme sa na jeho druhý prvok a následne na tento druhý prvok aplikujeme dereferenciu, čím dostanemeint. - Ak
xje smerník na poleint-ov,(*x)[2]znamená, že vezmemex, aplikujeme dereferenciu, ktorej výsledkom je poleint-ov a pozrieme sa na druhý prvok tohto poľa.
Cvičenie:
- Premennú
xsme vytvorili príkazomint **x = vytvorMaticu(4,4). S ktorými prvkami tabuľky potom pracujú výrazy*(x[2])a(*x)[2]?
Komplikácie nastávajú aj pri pochopení typov premenných. Pomôžu nasledujúce rady.
- Deklaráciu premennej
int *pmôžeme čítať takto: Ak vezmeme smerníkpa aplikujeme na neho operátor*, získame hodnotu typuint. - Deklarácia
int *a[4]je to isté akoint *(a[4])a znamená: Ak vezmemea, pozrieme sa na niektorý zo štyroch prvkov tohto poľa a nakoniec aplikujeme dereferenciu, dostaneme hodnotu typuint. Vytvorili sme teda štvorprvkové pole smerníkov naint. Jednotlivé smerníky v poli zatiaľ nie sú inicializované. - Deklarácia
int (*a)[4]znamená: Ak vezmemea, aplikujeme dereferenciu, dostaneme pole a keď sa pozrieme na niektorý prvok tohto poľa, dostanemeint. Riadok teda vytvorí smerník na pole štyroch celých čísel. Tento smerník však zatiaľ ukazuje na náhodné miesto pamäte, žiadne nové pole nevzniklo. To sa nám málokedy zíde, premennúamôžeme zadefinovať radšej akoint **a. - Deklarácia
int *(*(a[4]))vytvorí štvorprvkové pole smerníkov na smerníky naint. Dá sa zapísať aj bez zátvoriekint **a[4].
Zhrnutie
- Hlavnou náplňou dnešnej prednášky bolo vytvorenie a používanie dvojrozmerných polí pomocou poľa smerníkov na riadky.
- Môžete používať a podľa potreby upravovať funkcie
int ** vytvorMaticu(int m, int n),void zmazMaticu(int **a, int m)avoid nacitajMaticu(int **a, int m, int n)z programu s výškovou mapou. - Pole reťazcov je vlastne dvojrozmerná tabuľka znakov, ktorej riadky môžu mať rôznu dĺžku (a každý je správne ukončený nulou).
- Pozor na správnu alokáciu a dealokáciu. Pri práci so smerníkmi sa oplatí nakresliť si obrázok, čo kam ukazuje.