Skip to content

Instantly share code, notes, and snippets.

@drobilc
Last active April 2, 2018 10:56
Show Gist options
  • Save drobilc/80daf69568b7ded5c901104a5f71d3ab to your computer and use it in GitHub Desktop.
Save drobilc/80daf69568b7ded5c901104a5f71d3ab to your computer and use it in GitHub Desktop.
Predavanja pri predmetu Programiranje 2

Spremenljivke

Spremenljivke ustvarjamo na naslednji način:

[vrsta spremenljivke] [tip spremenljivke] ime spremenljivke = začetna vrednost;

Vrste spremenljivk je opcijska, ravno tako je opcijska začetna vrednost.

int n;
int n = 7;

Primer:

int n = 7;

int f(int m) {
	for (int i = 0; i < n; i++) {
		m = m * 2;
	}
	return m;
}

int main() {
	printf("main\n");
	printf("%d %d\n", n, f(n + 1));
	return 0;
}

Tukaj imamo 3 spremenljivke (m, n in i). Spremenljivka n je živa ves čas izvajanja programa (pravimo, da je globalna). Spremenljivki m in i pa sta živi le med izvajanjem funkcije f (tem pravimo lokalne spremenljivke).

Vrsta spremenljivke

Poznamo več vrst spremenljivk (auto, register, static, extern):

  1. avtomatske spremenljivke: auto (lokalne spremenljivke in parametri). Spremenljivke so uničene, ko se konča izvajati funkcija v kateri smo deklarirali spremenljivko.
  2. statične spremenljivke: static (globalne spremenljivke, lokalne s static) To lahko uporabimo tudi znotraj funkcije, taka spremenljivka bo preživela izvajanje funkcije. Uporabimo jo lahko npr. če želimo narediti random funkcijo.
unsigned int seed = 0;

unsigned int random() {
	seed = (1103515245 * seed + 12345) % (1 << 31);
	return seed;
}

int main() {
	for (int i = 0; i < 1000; i++) {
		printf("%d %u", i, random());
	}
	return 0;
}

V zgornjem programu je spremenljivka seed statična, spremenljivka i pa avtomatska. Ravno tako bi bilo pravilno, če bi poleg njiju napisal ustrezni besedi static oziroma auto.

Moti nas, da je spremenljivka seed vidna vsem v programu. Radi bi, da je dostopna samo funkciji random. To lahko naredimo tako, da jo ustvarimo znotraj funkcije. Tako bo vidna samo tej funkciji, a se bo ob vsakem njenem izvajanju ustvarila in uničila. Da to rešimo jo spremenimo v statično. Tako dobimo naslednji program.

unsigned int random() {
	static unsigned int seed = 0;
	seed = (1103515245 * seed + 12345) % (1 << 31);
	return seed;
}

Tako smo sedaj dosegli, da je spremenljivka seed prisotna čez celotno izvajanje programa.

Spremenljivka seed se izvede samo ob prvem izvajanju funkcije. Inicializacija se izvede samo enkrat v programu. Tako spodnji program ne bi deloval.

unsigned int random(unsigned int init) {
	static unsigned int seed = init;
	seed = (1103515245 * seed + 12345) % (1 << 31);
	return seed;
}

Opomba 1: a << b se imenuje bit shifting, deluje pa tako, da zamakne binarno vrednost števila a za b mest v levo, na konec (na desno stran števila) pa doda ničle. Dejansko tako število a pomnožimo z 2^b.

Opomba 2: Če želimo z uporabo funkcije printf izpisati nepredznačeno vrednost (unsigned) je potrebno uporabiti prinft("%u", n).

  1. Zunanje spremenljivke: extern (spremenljivke "od drugje")
extern int n;

To so spremenljivke v knjižnici oziroma v drugem delu programa.

  1. Registrske spremenljivke: register (lokalne z register) Take spremenljivke so vedno v pomnilniku, tako procesorju ni potrebno vedno brati s pomnilnika in je tako lahko program dosti hitrejši. Tega se danes ne uporablja več. Najverjetneje bo prevajalnik to ignoriral.

Sestava pomnilnika

[DODAJ SLIKO]

Avtomatske spremenljivke se shranjujejo na stack, statične spremenljivke na data prostor na pomnilniku.

Naloga 1

Napiši program, ki vrne vse možne kombinacije ničel in enic z določeno širino k.

int k = 7;

// Gremo cez vse mozne kombinacije 7 bitov (teh je 2^k)
for (unsigned comb = 0; comb < 128; comb++) {
	// Gremo cez vseh k bitov
	for (int b = 0; b < 7; b++) {
		if ((comb & (1 << b)) == 0)
			printf("0");
		else
			printf("1");
	}
	printf("\n");
} 

Opomba 1: Če napišemo samo unsigned i je to enako kot unsigned int i.

Opomba 2: Brez uporabe if stavka bi lahko za izpis izpisali tudi z uporabo

printf("%s", ((comb & (1 << b)) == 0) ? "0" : "1");

ali pa enako tudi kot

printf("%c", 48 + !((comb & (1 << b)) == 0));

Naloga 2

Napiši program, ki šteje z uporabo črk a, b, c in d.

int k = 14;
for (int comb = 0; comb < (1 << 14); comb++) {
	for (int b = 0; b < 7; b++) {
		switch ((comb >> (2 * b)) & 3) {
			case 0:
				printf("a");
				break;
			case 1:
				printf("b");
				break;
			case 2:
				printf("c");
				break;
			case 3:
				printf("d");
				break;
		}
	}
	printf("\n");
}

Opomba 1: Z uporabo (comb >> (2 * b)) & 3 dobimo ustrezen par bitov. (comb >> (2 * b)) nam premakne vse bite v desno, tako da sta bita, ki jih iščemo na skrajni desni. Ker je mogoče, da je levo od teh bitov še kakšen bit, ki ni ena, jih je potrebno izločiti. To storimo tako, da izvedemo operacijo and nad rezultatom in številom 3 (3 ima binarni zapis 00...011).

Opomba 2: Namesto uporabe switch stavka bi lahko vrednosti, ki jo dobimo prišteli ASCII kodo za a (97) in jo izpisali kot znak.

printf("%c", (comb >> (2 * b)) & 3) + 97);

Kazalci

[dopiši, manjka]

Primer:

static int n = 1000;
static int *pn = &n;

Ker je n statična spremenljivka je v razdelku data na našem pomnilniku. Je na točno določenih (ponavadi zaporednih) 4B na pomnilniku. Spremenljivka pn je kazalec (pointer) na n. Na pomnilniku pn ravno tako zaseda 4 bajte.

         |        |
         |        |
         |________|
12A0  n  |__1000__| (4 bytes)
         |        |
         |________|
1287  pn |__12A0__| (4 bytes)
         |        |
         |        |
         |        |

Torej imamo naslednje možnosti:

  • n je int

  • &n je naslov n-ja

  • pn je kazalec na int

  • *pn je int na katerega kaže pn

  • &pn je naslov kazalca na int

S pomočjo ukaza sizeof lahko dobimo število bajtov, ki jih zaseda spremenljivka. Če želimo dobiti število bajtov v spremenljivki n in število bajtov v kazalcu pn lahko dobimo s pomočjo spodnjega programa.

printf("%d %d\n", sizeof(n), sizeof(pn));

Vsi kazalci na nekem sistemu imajo enako širino (enako število bajtov).

Primer ko nimamo samo statičnih spremenljivk ampak avtomatske:

int main() {
	int n = 1000;
	int *pn = &n;
	printf("%d %lu\n", n, &n);
	printf("%lu %lu\n", (unsigned long int)pn, (unsigned long int)(&pn));
}

Opomba 1: Na Intel procesorjih so naslovi v unsigned long int spremenljivkah.

Branje števil v spremenljivko.

int n;
n = 10;
printf("%d\n", n);
printf("%d\n", 2 * 5);
scanf("%d", n);  // NAROBE
scanf("%d", &n); // PRAVILNO

scanf("%d", n); je napačno, ker mi funkciji scanf pošljemo vrednost spremenljivke 10. Pri drugem primeru scanf("%d", &n); pa funkciji scanf pošljemo naslov, na katerega naj shrani prebrano vrednost.

void swap(int a, int b) {
	int t = a;
	a = b;
	b = t;
}

spaw(i, j);

To ne deluje, ker funkcija swap sprejme dva parametra po vrednosti. V funkciji sta torej 2 neodvisni spremenljivki, ki imata enaki imeni, a nista enaki prejšnjim. Nato ti dve spremenljivki zamenjamo in ob koncu izvajanja funkcije tudi uničimo. Tako smo dosegli, da se vrednosti nista dejansko zamenjali, temveč sta se zamenjali le novo-ustvarjeni spremenljivki v funkciji swap.

Če pa napišemo funkcijo swap takole, se vrednosti dejansko zamenjata.

void swap(int *a, int *b) {
	int t = *a;
	*a = *b;
	*b = t;
}

swap(&i, &j);

V tem primeru pa mi funkciji kot parametre posredujemo lokaciji spremenljivk i in j na pomnilniku, znotraj metode zamenjamo podatke na pomnilniku na teh dveh lokacijah. Tako sta se zamenjali tudi dejanski vrednosti spremenljivk i in j.

Kazalci

int *pi;
char *pc;
double *pd;

To so trije kazalci. Vsebujejo naslov spremenljivke na pomnilniku. Kazalec tipa int je enak kazalcu tipa double. Procesor ju smatra kot pomnilniško lokacijo, te so pa vse enake.

Prevajalnik bi nas opozoril, če bi poskusili narediti.

pi = pd;

Kazalci, tabele, nizi

int i;
int *pi;
int **ppi;

Imamo dva unarna operatorja nad kazalci:

  • * vrne vrednost na katero kaže kazalec
  • & vrne naslov na katerem je shranjena spremenljivka
int i = 12;
int *pi;
printf("%d %lu\n", i, (unsigned long) pi); // Izpis: 12 0xAF0B

i = 3;
printf("%d %lu\n", i, (unsigned long) pi); // Izpis: 3 0xAF0B

pi = &i;
printf("%d %lu\n", i, (unsigned long) pi); // Izpis: 3 0xFF72

*pi = 0;
printf("%d %lu\n", i, (unsigned long) pi); // Izpis: 0 0xFF72

pi = 0;
printf("%d %lu\n", i, (unsigned long) pi); // Izpis: 0 0x0000 (oziroma Segmentation fault)

Ker tu govorimo samo o pi nam to vrne naslov na katerega kaže pi. Prvi printf tako izpiše vrednost spremenljivke i in lokacijo kjer se nahaja. Drugi printf nam izpiše spremenjeno vrednost spremenljivke i (lokacija ostane enaka); Tretji printf nam izpiše enako vrednost spremenljivke i, spremeni se lokacija na katero kaže kazalec pi. Zatem se vrednost spremenljivke na katero kaže kazalec (to je i) spremeni na 0. V zadnjem koraku spremenimo kazalec pi tako, da kaže na pomnilniško lokacijo 0x0000. Tukaj se program sesuje (ker ne smemo pisati na pomnilniško lokacijo 0)! Unix sistemi ponavadi to izpišejo kot Segmentation fault.

Tabele

Deklaracija tabele

int a[10];

Tako deklariramo statično oziroma lokalno tabelo z desetimi elementi.

int nums[10000000];

int sum(int n) {

	for (int i = 0; i < n; i++) {
		nums[i] = i;
	}

	int rezultat = 0;
	for (int i = 0; i < n; i++) {
		rezultat = rezultat + nums[i];
	}
	return rezultat;
}

Ustvarili smo neko statično tabelo, ki je zelo velika. Ker je ta tabela statična bi to delovalo (če bi bila lokalna bi se nam program sesul). Nato smo jo napolnili s števili od 0 do 10000000 in jih sešteli.

int sum(int n) {
	int nums[10000000]; // Ustvarimo lokalno spremenljivko znotraj funkcije
	...
}

Na skladu (stack) je malo prostora. Tja se shranjujejo lokalne spremenljivke. Če bi znotraj funkcije sum ustvarili lokalno tabelo velikosti 10000000 elementov, bi se nam program sesul zaradi Stack overflowa.

int sum(int n) {
	static int nums[10000000]; // Ustvarimo statično spremenljivko znotraj funkcije
	...
}

Če naredimo tabelo nums statično, se v pomnilniku shrani v data prostor na pomnilniku, kjer pa je dovolj prostora za tako tabelo. Tako bi naš program normalno deloval.

C nam ne bo vrnil napake če bomo šli čez velikost tabele (Java bi nam v tem primeru vrnila ArrayIndexOutOfBoundsException) ampak bo enostavno poskusil zapisovati naprej po tabeli. To lahko privede do velikih težav - do sesutja programa.

int sum(int n) {
	int nums[n];
	
	for (int i = 0; i < n; i++) {
		nums[i] = i;
	}

	int rezultat = 0;
	for (int i = 0; i < n; i++) {
		rezultat = rezultat + nums[i];
	}
	return rezultat;
}

Ta program bi nam v Cju za majhne vrednosti n deloval, za večje vrednosti bi prišlo do StackOverflowa. Tega se zato v C tega ne počne.

V Javi bi tako tabelo ustvarili tako.

int[] nums = new int[n];

Ker je Java dovolj pametna bo znala tabelo sama shraniti na kopico (sklad), c pa tega avtomatično ne naredi.

Na kopici je veliko prostora, tja se shranjuje dinamične spremenljivke.

int sum(int n) {
	int *nums;
	nums = (int*) malloc(n * sizeof(int));
	if (nums == NULL) {
		exit(1);
	}

	for (int i = 0; i < n; i++) {
		nums[i] = i;
	}

	int rezultat = 0;
	for (int i = 0; i < n; i++) {
		rezultat = rezultat + nums[i];
	}

	// Sprosti porabljen pomnilnik
	free(nums);

	return rezultat;
}

Kazalec nums se nam ustvari na skladu in nam zasede 8 bajtov. malloc je funkcija, ki vrne naslov bloka podatkov na kopici, ki je tako velik kot ga določimo z atributom v oklepaju. Ta funkcija nato na pomnilniku najde toliko prostora in nam vrne lokacijo tega pomnilniškega prostora.

Če malloc vrne NULL, to pomeni, da na pomnilniku ni našel toliko prostora. V vsakem drugem primeru dobimo neko lokacijo.

Z uporabo funkcije free sprostimo pomnilnik nazaj sistemu. Če ga ne sprostimo to imenujemo memory leak.

int sum(int n) {
	static int *nums = NULL;
	static int lenth = 0; // v intih
	if (n > length) {
		if (nums != NULL) {
			free(nums);
		}
		length = n;
		nums = (int*) malloc(length * sizeof(int));
		if (nums == NULL) {
			exit(1);
		}
	} else if (n == -1) {
		free(nums);
		return -1;
	}

	int rezultat = 0;
	...
	return rezultat;
}

stdlib.h funkcije

Če želimo uporabljati malloc je potrebno vključiti stdlib.h knjižnico.

#include <stdlib.h>

Funkcije, ki jih imamo v stdlib.h knjižnici bodo opisane v nadaljevanju.

malloc

void *malloc(size_t num_bytes)

malloc vrne nek kazalec, ki pa nima nobenega podatkovnega tipa (je tipa void). Če želimo notri shranjevati cela števila je potrebno typecastati ta kazalec v (int*).

realloc

void *realloc(void *ptr, size_t num_bytes)

S tem ukazom lahko povečamo velikost pomnilnika, ki smo ga že zasedli z uporabo funkcije malloc. Če je velikost manjša od stare, potem bomo v novem bloku izgubili podatke, ki so naprej od konca novega bloka. Če je velikost večja od stare, nam bodo stari podatki ostali notri, na koncu bomo dobili še nekaj dodatnega prostora.

free

void free(void *ptr);

S tem ukazom lahko sprostimo zaseden blok na pomnilniku.

Tabela kot atribut funkcije

Funkciji lahko tabelo posredujemo z uporabo kazalcev.

int sum(int nums[], int n) {
	for (int i = 0; i < n; i++) {
		nums[i] = i;
	}

	int rezultat = 0;
	for (int i = 0; i < n; i++) {
		rezultat = rezultat + nums[i];
	}
	return r;
}

Funkciji pošljemo samo naslov tabele na pomnilniku. Tako bi bila zgornja funkcija ekvivalentna temu.

int sum(int *nums, int n)

Pri tem je slabo, ker tako ne moremo nikakor določiti velikosti tabele.

Glavo funkcije lahko enakovredno napišemo tudi na naslednja dva načina.

int sum(int nums[100], int n);
int sum(int nums[0], int n);

Če pokličemo spodnjo kodo, bomo sešteli prvih 50 elementov, čeprav je velikost tabele tab 1000 elementov.

int tab[1000];
...
sum(tab, 50);

Ravno tako bi lahko klicali sum(tab, 2001). Prevajalnik se ne bi pritožil, čeprav naš program ne bi deloval. Sami smo si krivi za napako. Program se bi sesul tudi če bi klicali sum(NULL, 50). Ker ne smemo brati iz NULL prostora na pomnilniku bi prišlo do sesutja programa.

Če bi želeli sešteti vsa števila med 30 in 50, bi lahko funkcijo klicali tudi takole.

sum(&(tab[30]), 50);

Za funkcijo bi se tabela tab začela šele na indeksu 30, to pa zato, ker funkcija sum ne ve kako velika je tabela oziroma kje se začne na pomnilniku. Tako bi taka funkcija seštela vse elemente med 30 in 80.

Pri tem bi lahko prišlo do težav, če bi funkcijo klicali takole

sum(&(tab[999]), 50);

Pri tem bi prva iteracija zanke še šla skozi, nato pa bi šli čez zadnji indeks tabele in bi se sesul.

int tab[1000];
tab[10] + tab[20];

int *pi = &(tab[0]);
pi[10] + pi[20];

Ker kazalec pi kaže na prvi indeks tabele, je tab[10] + tab[20] enakovredno pi[10] + pi[20].

Kazalčna aritmetika:

S kazalci lahko delamo tudi aritmetične operacije. Tako lahko pi[10] + pi[20] zapišemo tudi kot

*(pi + 10) + *(pi + 20)

Pri tem (pi + 10) pomeni 10 mest naprej od katerega kaže kazalec (to je 10 * sizeof(int) bajtov). Ravno tako lahko uporabljamo pi++ in pi--. Pri tem se pomaknemo za toliko bitov kolikor je velikost podatkovnega tipa kazalca.

Prevajanje programov, ki so napisani v več datotekah

Recimo, da imamo datoteko main.c.

#include <stdlib.h>

...

int main() {
	...
}

Tako datoteko lahko prevedemo in poženemo na naslednji način

$ gcc -o main main.c
$ ./main

Ko postane taka datoteka prevelika za urejanje (par tisoč vrstic), jo razdelimo na več delov. Vsak izmed delov opravlja svoje opravilo (računanje, branje podatkov, ...). Datoteko lahko razrežemo na več kosov in jih shranimo v ločene datoteke.

Še vedno imamo datoteko main.c. Vso kodo, ki smo jo v zgornjem primeru napisali med #include <stdlib.h> in glavno metodo main prenesemo v datoteko support.c.

Tako je naša datoteka main.c sedaj taka

#include <stdio.h>

int main() {
	f(3, 5);
}

Dodamo pa še datoteko support.c.

int f(int a, int b) {
	...
}

Program prevedemo

$ gcc -c main.c

Tako dobimo datoteko main.o. Če bi ta program pognali, bi program ob klicu f(3, 5) ne vedel katero funkcijo je potrebno poklicati in bi se pritožil.

Prevajalniku lahko povemo, kje se nahaja funkcija f(a, b) tako, da jo navedemo v main.c, ne napišemo jo pa v celoti.

#include <stdio.h>

int f(int a, int b);

int main() {
	printf("%d\n", f(3, 5));
	return 0;
}

Tako prevajalniku povemo, da funkcija f nekje obstaja, ne povemo pa mu kje se nahaja.

Potrebno je seveda še napisati funkcijo v datoteki support.c.

int f(int a, int b) {
	return a + b;
}

Da bo sedaj ta program deloval je potrebno prevesti še support datoteko. To storimo na naslednji način.

$ gcc -c support.c

Dobimo datoteko support.o, ki vsebuje kodo za funkcijo f.

V vsaki datoteki (main.o in support.o) je koda za neko funkcijo(main in f). Da ju povežemo skupaj v program je potrebno vpisati naslednji ukaz.

$ gcc -o main main.o support.o

Tako dobimo datoteko main, v kateri funkcija f normalno deluje.

Include

#include <stdio.h>
#include "support.c"

int main() {
	printf("%d\n", f(3, 5));
	return 0;
}

Če pri include napišemo v koničastih oklepajih (<, >), to pomeni, da jo išče med sistemskimi knjižnicami. Če to damo v narekovaje, nam jo poišče v enaki mapi kot poganjamo prevajalnik.

Prevajalnik bi tako iz datoteke "vstavil" kodo in prevedel

V svojem programu ne želimo dosti include ukazov, saj bi se tako program prevajal zelo počasi. Potrebno bi bilo namreč prevesti vsak posamezen header in jih vključiti v glavni program.

Če napišemo funkcije tako kot smo naredili v prvem primeru je to hitreje, saj je potrebno program main.c samo enkrat prevesti, če pa želimo funkcijo f spremeniti, jo spremenimo, prevedemo samo support.c in program zlinkamo.

Headerji

Datoteka main.c.

#include <stdio.h>
#include "support.h"

int main() {
	printf("%d\n", f(3, 5));
	return 0;
}

Datoteka support.c.

int f(int a, int b) {
	return a + b;
}

Datoteka support.h pa vsebuje prototip funkcije f.

int f(int a, int b);

V support.h napišemo prototipe funkcij, ki jih želimo vključiti iz datoteke support.c. V Javi bi bile to javne metode. V support.c nato napišemo dejanske funkcije, ki bodo delo opravljale. Funkcije v datoteki support.c bi bile privatne, glavni program (main.c) jih ne bi videl, lahko bi videl le tisto kar bi bilo napisano v support.h.

Niz znakov (string)

V Javi lahko nize znakov zapišemo takole.

String s;
s = new String();
s = "string";
System.out.println(s);

V Cju so znaki tabele znakov (char).

char s[6 + 1];

s[0] = 's';
s[1] = 't';
s[2] = 'r';
s[3] = 'i';
s[4] = 'n';
s[5] = 'g';
s[6] = '\0';

printf("%s\n", s);

Zadnji znak je dejansko znak z indeksom 0 v ASCII kodni tabeli. Funkcija printf izpiše vse znake v tabeli s do zadnjega znaka (to je ta znak \0). Če tega znaka ne bi dali, bi printf poskušal iti naprej po tabeli do prve ničle. Ta znak imenujemo tudi null znak.

Pisanje stringov na tak način je dolgo in nima smisla, zato se to ponavadi naredi na naslednji način.

char *s; // Naredi nov kazalec, ki kaze kar nekam
s = "string";
printf("%s\n", s);

Tako lahko sedaj string zapišemo na krajši način. C nam v nek del pomnilnika našega programa doda tabelo znakov. Nato v vsako polje tabele zapiše po en znak in zaključi z null znakom (\0).

Da dobimo dolžino stringa se moramo sprehoditi čez celotno tabelo dokler ne pridemo do terminatorja (torej do \0 znaka).

int strlen(char *s) {
	int length = 0;
	while (*s != '\0') {
		length = length + 1;
		s++; // Kazalec pokazemo na naslednje polje tabele
	}
	return length;
}

Lahko bi poskušali dobiti dolžino stringa z uporabo funkcije sizeof(s), vendar bi nam ta funkcija vrnila dolžino podatkovnega tipa. Ker je s kazalec na znak, bi nam funkcija vrnila 8 (ker 8b * 8b = 64b, kar pa je velikost naslova na procesorju).

Če za primer izvedemo podoben program kot prej, vidimo kako deluje null terminator.

char s[6 + 1];

s[0] = 's';
s[1] = 't';
s[2] = 'r';
s[3] = 'i';
s[4] = 'n';
s[5] = 'g';
s[6] = '\0';

printf("%d\n", strlen(s)); // Izpise 6
s[3] = '\0';
printf("%d\n", strlen(s)); // Izpise 3

Ko prvič pokličemo funkcijo strlen(s) dobimo dolžino stringa s, torej število znakov do znaka \0, kar je 6 znakov. Nato ročno spremenimo tretji znak na null znak, ko naslednjič pokličemo to funkcijo, dobimo da je dolžina le 3 znake.

Večdimenzionalne tabele

Večdimenzionalne tabele ustvarjamo podobno kot enodimenzionalne.

double a[10][10], b[10][10], c[10][10];
for (int i = 0; i < 10; i++) {
	for (int j = 0; j < 10; j++) {
		c[i][j] = 0.0;
		for (int k = 0; k < 10; k++) {
			c[i][j] = c[i][j] + a[i][k] * b[k][j];
		}
	}
}

Zgornji program izvede množenje matrik a in b (C = AB).

Če namesto tega programa zamenjamo le 1 vrstico (glej spodaj), pride do opazne razlike.

double a[10][10], b[10][10], c[10][10];
for (int i = 0; i < 10; i++) {
	for (int j = 0; j < 10; j++) {
		c[i][j] = 0.0;
		for (int k = 0; k < 10; k++) {
			c[i][j] = c[i][j] + a[i][k] * b[j][k];
		}
	}
}

Če ustvarimo tabelo (3 x 2).

int a[3][2];

To je taka tabela kot je prikazana spodaj.

| a00 | a01 |
| a10 | a11 |
| a20 | a21 |

Se na pomnilniku taka tabela zloži po vrsticah (v C, drugi programski jeziki imajo to implementirano drugače).

a00, a01, a10, a11, a20, a21

Če sta 2 elementa sosedna v vrstici potem sta sosednja tudi na pomnilniku. Če pa sta sosednja v stolpcu, potem je med njima cela vrstica podatkov. Zato je branje hitrejše če beremo po vrsticah.

Večdimenzionalne matrike s kazalci

// a je kazalec na tabelo kazalcev
double **a;

// a sedaj kaže na tabelo enojnih kazalcev tipa double
a = (double*) malloc(10 * sizeof(double*));

// Sedaj vsak enojni kazalec v tabeli usmerimo na tabelo double vrednosti
for (int i = 0; i < 10; i++) {
	a[i] = (double*) malloc(10 * sizeof(double));
}

Najprej ustvarimo tabelo dvojnih kazalcev, ki kažejo na tabelo enojnih kazalcev.

**a --> | a* | b* | c* | d* | e* | f* | g* | h* | i* | j* |

Vsak izmed teh 10 enojnih kazalcev kaže na tabelo double vrednosti.

*a --> | d0,1 | d0,2 | d0,3 | d0,4 | d0,5 | d0,6 | d0,7 | d0,8 | d0,9 | d0,10 |, d0,1, d0,2, ..., d0,10 so double vrednosti
*b --> | d1,1 | d1,2 | d1,3 | d1,4 | d1,5 | d1,6 | d1,7 | d1,8 | d1,9 | d1,10 |, d1,1, d1,2, ..., d1,10 so double vrednosti
...
*j --> | d10,1 | d10,2 | d10,3 | d10,4 | d10,5 | d10,6 | d10,7 | d10,8 | d10,9 | d10,10 |, d10,1, d10,2, ..., d10,10 so double vrednosti

Tako smo ustvarili matriko 10 x 10 elementov. Zasedemo 888 bajtov prostora na pomnilniku.

Spodnje trikotne matrike

Če pa bi želeli le spodnje trikotno matriko, bi lahko program spremenili tako, da bi bila vsaka tabela krajše dolžine.

// a je kazalec na tabelo kazalcev
double **a;

// a sedaj kaže na tabelo enojnih kazalcev tipa double
a = (double*) malloc(10 * sizeof(double*));

// Sedaj vsak enojni kazalec v tabeli usmerimo na tabelo double vrednosti
for (int i = 0; i < 10; i++) {
	a[i] = (double*) malloc((i + 1) * sizeof(double));
}

Tako a** kaže na tabelo enojnih kazalcev.

**a --> | a* | b* | c* | d* | e* | f* | g* | h* | i* | j* |

Vsak izmed teh 10 enojnih kazalcev kaže na tabelo double vrednosti, ki pa je za 1 daljša od prejšnje tabele.

*a --> | d0,1 |, d0,1 je double vrednost
*b --> | d1,1 | d1,2 |, d1,1, d1,2 sta double vrednosti
...
*j --> | d10,1 | d10,2 | d10,3 | d10,4 | d10,5 | d10,6 | d10,7 | d10,8 | d10,9 | d10,10 |, d10,1, d10,2, ..., d10,10 so double vrednosti

Tako bi porabili manj pomnilnika, saj bi porabili le (1 + 2 + ... + 10) * 10 prostorov na pomnilniku (+ še nekaj za tabeli kazalcev).

Tabela nizov znakov

Da ustvarimo tabelo stringov, lahko to storimo na naslednji način.

char *stringi[100];

Element stringi[0] je kazalec na tabelo znakov char*, kar lahko razumemo kot niz.

Atributi ukazne vrstice

V Javi smo kot parameter main metode lahko prejeli tabelo nizov atributov programa v ukazni vrstici. To je mogoče storiti tudi v Cju.

int main(int argc, char *args[]) {
	...
}

Parameter argc je število nizov ki smo jih vpisali v ukazni vrstici. Parameter args pa je kazalec na tabelo nizov v katerem so argumenti v ukazni vrstici.

Torej imamo stringe args[1], ..., args[argc - 1]. Prvi argument args[0] je ime programa, ki ga kličemo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment