Z pohledu programu se pamět jeví jako pole bajtů, kde indexu říkáme adresa. Je-li v paměti uložen nějaký objekt, jsou jeho jednotlivé bajty uloženy za sebou, přičemž první bajt je uložen na nejmenší adrese. Za adresu vícebajtového objektu považujeme právě adresu jeho prvního bajtu.
Program běžící na běžném OS má díky virtuální paměti přístup k celému adresnímu rozsahu na dané platformě. Adresní rozsah je množina adres, se kterými jsme schopni pracovat a je typicky dána velikostí adresy. Je-li například adresa 64-bitová, jak tomu je na moderních počítačích, adresní rozsah je od 0 do 264.
Při psaní programu se o to, na jakých konkrétních adresách jsou objekty uloženy, nestaráme. O tom rozhoduje operační systém. Pokus o zápis na nebo čtení z adresy, která nebyla programu operačním systémem zpřístupněna, vede k nedefinovanému chování.
Základní operace, které můžeme s pamětí dělat, je zjištění adresy na
níž je konkrétní objekt (např. proměnná). To se provádí pomocího
unárního prefixového operátoru adresy: &
.
int x = 10;
("%p\n", &x); // vypiseme adresu, na ktere je promenna x printf
Pomocí %p
vypisuje printf
adresy, je zvykem
je tisknout v hexadecimální soustavě.
Adresu můžeme uložit i do proměnné, k tomu ale potřebujeme znát její
typ. Ke každému existujícímu typu z je v programu automaticky i
typ adresa, na které je uložena hodnota typu z. Proměnným a
často i hodnotám tohoto nového typu programátoři říkají pointer na
z. Klíčovým slovem pro tento nový typ je pak z*
.
int x = 10;
int* ptr_x = &x;
Při definici pointeru se znak *
ve skutečnosti váže ke
jménu proměnné, to se ovšem projevuje pouze při definici více proměnných
na jednom řádku. To ovšem v kurzu nepovažujeme za dobrý styl a nebudeme
to dělat. Stejně tak můžou být před *
bílé znaky.
Je nutné si uvědomit, že samotná proměnná ptr_x
je v
paměti a operátorem adresy lze zjistit na jaké adrese.
int x = 10;
int* ptr_x = &x;
int** ptr_ptr_x = &ptr_x;
Druhou základní operací je přístup k hodnotě na dané adrese. To se
dělá pomocí unárního prefixového operátoru dereference: *
.
Tímto způsobem můžeme hodnotu číst i na dané místo zapisovat.
int x = 10;
int* ptr_x = &x;
("%i\n", *ptr_x); // vytiskne 10
printf*ptr_x = 20; // zapise na adresu ptr_x hodnotu 20
("%i\n", x); // vytiskne 20 printf
Je nutné si uvědomit, že k přístupu k hodnotě na dané adrese je nutné znát typ této hodnoty. To je zajištěno typovým systémem, kdy je v typu pointeru zachycen typ hodnoty na dané adrese.
Dereferencování pointeru, které vede k přístupu na adresu paměti, která není programu přidělena, vede k nedefinovanému chování. O takovém pointeru řekneme, že je neplatný. Obecně nelze testovat, jesli je pointer neplatný. Existuje ovšem jedna výjimka a tou je adresa 0 (jakéhokoliv typu). Ta je vždy neplatná a jako obecná pravdivostní hodnota odpovídá nepravdě. Je proto dobrým zvykem udržovat hodnoty neplatných pointerů rovny 0.
// pokud udrzujeme neplatne pointery rovny 0, nasledujici nevede k nedefinovanemu chovani
int* ptr_x = 0;
// nejaky kod, ktery mozna manipuluje s ptr_x
if (ptr_x) {
("%i\n", *ptr_x);
printf}
Jméno pole je konstantní pointer s adresou prvního prvku pole.
int a[] = { 0,1,2,3,4,5 };
int* ptr_3 = a;
("a[0] = %i\n", *a); // tiskne 0
printf*ptr_3 = 10;
("a[0] = %i\n", a[0]); // tiskne 10 printf
Pointer je konstatní, nelze tak měnit jeho hodnotu.
int b[] = { 10, 20 };
= b; // Chyba: a je konstantni pointer a nelze do nej priradit jinou hodnotu. a
Při předání pole jako argumentu funkce pole degeneruje na pointer.
void fce(int array[]) {
// array je typu int*
// size je tedy sizeof(int*) / sizeof(int)
int size = sizeof(array)/sizeof(array[0]);
("ve funkci: %i\n", size);
printf}
int main() {
int a[] = { 0,1,2,3,4,5 };
int size = sizeof(array) / sizeof(array[0]);
("v mainu: %i\n", size);
printf(a);
fce}
Kvůli degeneraci tak vlastně můžeme do funkce předat přímo pointer.
void fce(int* array)
Jak je to s přístupem k prvkům pole? To řeší pointerová aritmetika.
Protože pole je v paměti v souvislém bloku, lze adresu prvku na indexu
i
získat tak, že adresu začátku pole zvětšíme o
i
-krát velikost jednoho prvku pole. Tím se v paměti
posuneme z prvního bajtu prvku na indexu 0 na první bajt prvku na indexu
i
. V programu počet bajtů, o které je nutno se posunout
nemusíme počítat, aritmetické operace s pointery to udělají za nás. K
pointeru lze totiž přičíst, nebo od něj odečíst, celé kladné číslo.
Výsledkem přičtení čísla c
k pointeru pt
je
pointer, který získáme zvětšením pt
o c
-krát
velikost typu, na který pt
ukazuje. Pokud by byl například
pt
pointer na int
, tak bychom je zvětšili o
c
-krát sizeof(int
). Odečítání celého čísla
funguje analogicky.
int a[] = {0,1,2};
int* ptr = a;
("velikost int: %lu\n", sizeof(int));
printffor (int i = 0; i < 3; i += 1) {
("index: %i, adresa: %p\n", i, a + i);
printf}
Pointerové aritmetiky v kombinaci s operátorem dereference můžeme
využít pro přístup k prvkům pole. Použití []
je totiž jenom
syntaktický cukr pro pointerovou aritmetiku a následnou dereferenci.
int array[] = { 1, 2, 3, 4 };
int* ptr = array + 1;
("array[2] = %i\n", array[2]); // vytiskne 3
printf("*(array+2) = %i\n", *(array+2)); // vytiskne 3
printf("ptr[1] = %i\n", ptr[1]); // vytiskne 3 printf
Operátor dereference má větší prioritu než sčítání, je proto nutné použít závorky. Protože je navíc operátor sčítání komutativní, je korektní i následující kuriózní přístup k prvku na indexu 2.
("array[2] = %i\n", 2[array]); // vytiskne 3 printf
Všimněme si také přístupu pomocí pointeru ptr
. Ten
ukazuje, že pole v C skutečně jsou adresou prvního prvku a současně
každý pointer můžeme chápat jako adresu prvního prvku v poli (i když je
toto pole vlastně jednoprvkové). Přístup k jednotlivým prvkům je potom
realizován pomocí pointerové aritmetiky. V našem případě se můžeme na
ptr
dívat jako na pole, která začíná druhým prvkem
array
.
Pointery stejného typu můžeme od sebe odečíst, výsledkem je počet prvků daného typu, který se mezi obě adresy vejde. Je to tedy rozdíl adres (v bajtech) dělený velikostí typu. Toto dělení je celočíselné.
Pointery také můžeme mezi sebou porovnávat.
Pointeru na strukturu lze použít k přístupu k položkám struktury s
pomocí operátoru ->
, alternativně lze použít dereference
následované operátorem .
(tečka).
typedef struct {
int pol1;
float pol2;
} My_Str;
= {10, 13.1};
My_Str str * ptr = &str;
My_Str// oba radky vytisknou totez
("polozka1: %i, polozka2: %f\n", ptr->pol1, ptr->pol2);
printf("polozka1: %i, polozka2: %f\n", (*ptr).pol1, (*ptr).pol2); printf
Operátory adresy a dereference mají menší prioritu než operátor
tečky, proto jsme v příkladu museli použít závorky. V opačném případě
bychom udělali dvě chyby: přistupovali bychom k položce struktury pomocí
tečky a přitom ptr
je pointer na strukturu; a
dereferencovali bychom int
a float
. Naopak,
při použití operátorů adresy a dereference na položky struktury závorky
můžeme vynechat (můžeme je psát kvůli čitelnosti, nebo “pro
jistotu”).
("adresa prvni polozky: %p\n", &str.pol1); printf
Struktura nemůže obsahovat položku stejného typu jako je sama. Může ovšem obsahovat položku, která je pointerem na typ, které je sama. Toho často využíváme při kostrukci datových struktur, například spojového seznamu.
typedef struct _node {
int key; // data v uzlu
struct _node* next; // pointer na dalsi prvek v seznamu
} Node;
[10];
Node storagefor(int i = 0; i < 10; i += 1) {
// naplnime seznam
[i].key = i;
storageif (i < 9) {
[i].next = &storage[i+1];
storage}
else {
[i].next = 0; // posledni prvek nema souseda
storage}
}
// projdeme seznam a vytiskneme jej
*first = storage;
Node while(first) { // posledni uzel ma polozku next rovnu 0
("%i ", first->key);
printf= first->next;
first }
("\n"); printf
Už víme, že pokud předáme funkci pole jako argument a funkce změní některé jeho prvky, zachovají se tyto změny i po skončení funkce na místě, ze kterého jsme ji volali. Děje se tak proto, že volané funkci nepředáváme jako argument pole, ale jeho adresu.
Tohoto mechanismu můžeme využít i pro jiné argumenty než pole, pokud chceme umožnit, aby funkce mohla změnit jejich hodnotu. Stačí předat adresu argumentu, místo jeho hodnoty. Toho se často používá, potřebujeme-li z funkce vrátit více než jednu hodnotu.
// funkce pro celociselne deleni
// argument rem je predan odkazem, je to adresa, na kterou funkce zapisuje
int division(int a, int b, int* rem) {
*rem = a % b;
return a / b;
}
int main() {
int x = 0;
int y = 0;
= division(10, 3, &y); // tady predame adresu promenne y
x ("vysledek %i, zbytek %i\n", x, y); // vypise: vysledek 3, zbytek 1
printf}
Naprogramujte vlastní verze knihovních funkcí strlen
a strcmp
(hlavičkový souborstring.h
), které
nepoužijí indexů, ale pouze pointerové aritmetiky a
dereference.
Vytvořte strukturu s několika položkami a aspoň jednu její proměnnou. Vypište informace o layoutu této proměnné: pro každou položku vypište její adresu a velikost. Vypište i adresu a velikost samotné proměnné.
Naprogramujte funkci pro výpočet průměru a mediánu pole čísel
(např. float
). Jednu z hodnot vraťte pomocí argumentu
předaného odkazem.
Napište funkci swap_int
, která prohodí hodnoty dvou
proměnných typu int
.
V jazyce C existuje pointer bez informace o typu hodnoty, na kterou
pointer ukazuje. To je užitečné například v situaci, kdy pracujeme čistě
s pamětí a nepotřebujeme znát, jakého je typu. Například při kopírování,
porovnávání apod. (K tomu se ještě dostaneme). Typ takového pointeru je
void*
, programátoři mu říkají void pointer.
Void pointer lze implicitně přetypovat na pointer jakéhokoliv typu, a pointer jakéhokoliv typu lze implicitně přetypovat na void pointer. Přetypování mezi ostatními typy pointerů je nutno provádět explicitně (jinak překladač vypíše warning nebo chybu).
Příkladem jednoduché funkce pracující s pamětí je tisk jejího obsahu na obrazovku. Jako argumenty funkce postačuje adresa začátku paměti a její velikost v bajtech.
void dump_mem(void *mem, size_t size);
Pamět budeme tisknout po bajtech do tabulky, která bude mít na každém
řádku 8 bajtů. Jeden bajt vytiskneme jako dvouciferné hexadecimální
číslo (zamyslete se proč je to vhodné). Ve funkci využijeme toho, že typ
unsigned char
má vždy velikost jeden bajt. Na začátku
funkce (implicitně) přetypujeme pointer mem
na pointer na
unsigned char
a můžeme se na pamět dívat jako na pole
bajtů. Poté stačí toto pole projít a vytisknout.
void dump_mem(void *mem, size_t size) {
unsigned char *bytes = mem; // tady pretypujeme
for (size_t i = 0; i < size; i += 1, bytes += 1) {
if (i && !(i % 8)) {
("\n");
printf}
("%.2X ", *bytes);
printf}
("\n");
printf}
Chceme-li funkci použít, musíme získat její argumenty: získat adresu začátku paměti a její velikost.
int x = 300;
(&x, sizeof(x)); // tady dochazi k implicitnimu pretypovani int* na void*
dump_mem
float f = 2.4;
(&f, sizeof(f));
dump_mem
struct { char c; int a; } s = {'x', 55 };
(&s, sizeof(s));
dump_mem
char a[] = "ahoj svete";
(a, strlen(a) + 1);
dump_mem
double b[5] = { 1.1, 2.2, 3.3, 0, 4.4 }
(b, sizeof(b)); dump_mem
Přetypování z void*
na unsigned char
je
vždy bezpečné. Jinde může dojít k nedefinovanému chování. Prvním důvodem
jsou tzv. trap reprezentace. Mohou existovat typy, kde ne každý
obsah paměti odpovídá platné hodnotě daného typu. Takový obsah paměti,
který neodpovídá platné hodnotě je past: pokus o dereferenci vede k
nedefinovanénu chování. Podle standardu jediný typ, který zaručeně nemá
trap reprezentaci právě unsigned char
. Ostatní typy trap
reprezentaci mít mohou, ale nemusí. Dalším problémem může být tzv.
zarovnání (anglicky alignment): na některých architekturách je
vyžadováno, aby vícebajtové objekty začínaly na adrese s určitou
vlastností, například na adrese dělitelné velikostí objektu. Dereference
nezarovnané adresy vede k nedefinovanému chování.
Pro explicitní konverze mezi pointery jiných typů existují ve
standardu pravidla (strict aliasing rules), která svým rozsahem do kurzu
nepatří. Doporučuji se prozatím takovým konverzím vyhýbat. (Mimo void
pointer jsou výjimkou i unsigned char
pointery).
Pro manipulaci s pamětí lze využít některé funkce ze
string.h
. Jsou to zejména:
void* memcpy(void* dest, void* src, size_t count)
Zkopíruje count
bajtů z adresy src
na
adresu dest
. Oblasti paměti odkud a kam se kopíruje se
nesmí překrývat.
void* memmove(void* dest, void* src, size_t count)
Zkopíruje count
bajtů z adresy src
na
adresu dest
. Oblasti paměti odkud a kam se kopíruje se
mohou překrývat.
void* memset(void* dest, int ch, size_t count)
Zkopíruje hodnotu (unsigned char)ch
do
count
bajtů paměti začínajících adresou
dest
.
int memcmp(void* lhs, void* rhs, size_t count)
Lexikograficky porovná prvních count
bajtů paměti na
adresách lhs
a rhs
. Vrací znaménko rozdílu
mezi hodnotami prvních dvou bajtů na kterých se lhs
a
rhs
liší. Pokud takové bajty neexistují, vrací 0.
Další podrobnosti o zmíněných funkcích si čtenář nastuduje z referenční příručky. Důležité je podívat zejména okrajové chování (např. když je některý argument 0).
Upravte funkci dump_mem
tak, aby na začátku každého
řádku vypsala adresu prvního bajtu na tomto řádku. Přidejte možnost
vypisovat bajty jako posloupnost 8 bitů (je nutné využít bitových
operátorů).
Naprogramujte vlastní verze funkcí
memcpy, memcmp, memset
. Funkce mohou být implementovány
naivně.
Najděte způsob, jak bez použití sizeof
programově
zjistit velikost nějakého typu, například float
.
Napište funkci pro detekci endianity a pro převod mezi
big endian a small endian (například pro typ
unsigned int
, nebo obecnou funkci). Wiki o
endianitě.
Pro nás význam: kdy je objekt v paměti během běhu programu? (anglicky storage duration). Když je pamět objektu přidělena, říkáme, že je pro něj alokována. Uvolnění paměti objektu někdy říkáme dealokace.
Existují tři typy životnosti, statická, automatická a manuální.
Pro objekty se statickou životností je paměť alokována při startu programu, uvolněna na konci běhu programu. O její alokaci je rozhodnuto při překladu programu. Statickou zprávu paměti mají zejména
static
,Objekty lze inicializovat na hodnotu známou v době překladu. Bez explicitní inicializace v kódu jsou automaticky inicializovány na 0.
Storage class static
se u proměnné specifikuje při
definici, píše se na první místo. Proměnná si uchovává hodnotu mezi
jednotlivými zavoláními funkce.
int fce() {
static int count; // inicializace na 0 pri startu programu
+= 1;
count return count; // vraci kolikrat byla zavolana
}
Řetězcové literály jsou pouze ke čtení, pokus o jejich změnu vede k nedefinovanému chování. Mohou být uloženy tak, že se překrývají, například sdílejí svůj konec.
char *r = "retezcovy literal";
[0] = 'x'; // nedefinovane chovani r
Složené literály lze měnit.
typedef struct {
float x;
float y;
} vec2f;
// globalni promena
*ptr = &(vec2f){.x=3.2f, .y=2.1f};
vec2f
// nekde uvnitr funkce
->x = 2.4; // OK prt
Alokace a dealokace objektů s automatickou životností se děje automaticky za běhu programu. Mezi objekty s automatickou životností patří
static
,Objekty typicky vznikají v momentě vstupu do bloku, ve kterém jsou definovány a zanikají při výstupu z tohoto bloku. Nejsou automaticky inicializovány, je nutné je inicializovat explicitně. Je chyba vracet z funkce adresu objektu s automatickou životností, objekt je totiž po skončení funkce dealokován. Dereference adresy pak vede k nedefinovanému chování.
int* fce() {
int array[] = {1,2,3};
return array; // chyba !!!!!
}
// nekde v kodu
int *a = fce();
[0] = 5; // nedefinovane chovani a
K manuální správě paměti aplikační programátor většinou
využije nejakou knihovnu, kde je implementován alokátor paměti (ten se
stará o přidělování paměti, sám ji získává od operačního systému,
případně spravuje paměť se statickou životností). Rozhraní alokátoru ze
standardní knihovny je dáno funkcemi z hlavičkového souboru
stdlib.h
.
malloc, calloc
,free
,realloc
.Princip použití alokačních funkcí je následující: funkci předáme počet bajtů, které chceme alokovat. Funkce vrátí adresu prvního bajtu souvislého kusu paměti požadované velikosti. V případě, že se alokace nepovede, vrací funkce 0.
Funkci free
předáme jako argument adresu, kterou někdy
předtím vrátila některá alokační nebo realokační funkce, a která ukazuje
na doposud neuvolněnou paměť.free
tuto paměť uvolní. Pokud
jí předáme 0, nedělá free
nic. Pokud jí ovšem předáme
jakoukoliv jinou adresu, vede to nedefinovanému chování.
Funkce realloc
umožňuje změnit velikost již alokované
paměti.
Podrobnosti k jednotlivým funkcím si čtenář najde v referenční příručce, řekneme si pouze příklady typického použití a častých chyb.
int m = 20;
int* array = malloc(m * sizeof(int)); // alokace pro pole int velikosti m
(array); // pro ucely kurzu staci tato kontrola,
assert// obecne muze byt potreba komplikovanejsi test uspechu alokace
// pracuj s polem array
(array); // uvolnime pamet
free= 0; // nastavime pointer na neplatny array
Funkce realloc
muze při změně velikosti alokovane paměti
přesunout její obsah na jinou adresu a původní paměť uvolnit.
int realloc_size[] = {10, 12, 512, 32768, 65536, 32768};
int m = sizeof(realloc_size)/sizeof(realloc_size[0]);
int *next = 0;
for (int i = 0; i < m; i += 1) {
int *ret = realloc(next, sizeof(int) * realloc_size[i]);
(ret);
assert("%p -> %p\n", next, ret); // pokud dojde k presunu, vypisi se ruzne adresy
printf= ret;
next }
(next); free
Mezi hlavní chyby při manuální správě paměti patří tzv. memory leak a double free. První chyba nastane tak, že v programu zapomeneme adresu alokované paměti bez toho, abychom ji uvolnili. Tím pádem už ji nikdy uvolnit nemůžeme. Program má pak tuto paměť alokovánu zbytečně. Ke druhé chybě dojde tak, že se alokovanou paměť pokusíme uvolnit dvakrát (nebo vícekrát), mimo první pokus to vede k nedefinovanému chování.
Na závěr dodejme, že po skončení programu všechnu jeho paměť uvolní operační systém.
Existují také jiné přístupy ke správě paměti, které nahrazují nebo vylepšují manuální správu, například tzv. počítání referencí nebo garbage kolektory. Ty jsou v určité formě pro jazyk C přístupné jako knihovny (např.Boehm garbage collector).
Implementujte funkci, která vrátí nově alokovaný řetězec, jehož obsah vznikne spojením dvou řetězců předaných funkci jako argumenty.
Implementuje zásobník pomocí dynamického pole.
typedef struct {
int *data; // pole pro vlozena data
int top; // pocet vlozenych prvku
int cap; // velikost pole data
} Stack;
() {
Stack create_stackreturn (Stack){0};
}
Doprogramujte operaci push
tak, aby v momentě, kdy je
zásobník zaplněn, tato operace realokovala položku data na dvojnásobnou
velikost.
Doprogramujte operaci ‘pop’ tak, aby v momentě, kdy je zásobník zaplněn z jedné čtvrtiny, zmenšila položku data na polovinu.
První nenulovou velikost zásobníku vyberte jako malou mocninu 2, např. 16.
Vytvořte strukturu struct matrix
pro matici
desetinných čísel (double
), s položkami pro rozměry matice
(počet řádků a sloupců) a data (obsah matice). O vnitřním uložení a
adresování jednotlivých prvků matice rozhodněte sami.
Naprogramujte funkce:
void allocate_matrix(struct matrix *m, int rows, int cols);
Funkce nastaví v m
položky pro rozměry matice a alokuje
pamět pro data.
void initialize_matrix_from_array(struct matrix *m, double *array);
Funkce inicializuje obsah podle pole array
v row-major
pořadí: řádky matice jsou naskládány za sebe, jako reprezentaci
dvourozměrného pole jednorozměrným z minulého semestru. Můžete
předpokládat, že array
je dostatečně velké.
void destroy_matrix(struct matrix *m);
Funkce dealokuje paměť a nastaví odpovídající pointery na 0.
struct matrix multiply_matrix(struct matrix a, struct matrix b);
Funkce vynásobí matice (předpokládejme, že vynásobit jdou). Pro data výsledné matice alokuje novou paměť.
void print_matrix(struct matrix a);
Funkce vytiskne matici jako tabulku.