D/Wskaźniki
Czym jest wskaźnik
edytujJak wiadomo każda komórka w pamięci RAM ma swój adres (numer), w zależności od procesora jest to liczba typu uint (23bit) lub ulong (64bit). Wynika z tego że wskaźnik jest zwykłą liczbą. Wiadomo jednak że różne zmienne mają różny rozmiar, int – 4 bajty, byte – 1 a double – 8. Wskaźniki z kolei mogą przechować jedynie numer jednego bajtu - początku zmiennej. Dlatego też wskaźniki nie są deklarowane jako zwykłe zmienne i należy poinformować kompilator o typie danych, na które wskazujemy.
Deklaracja wskaźnika
edytujSkoro wiemy że wskaźnik musi nie tylko zawierać liczbę, ale także informować kompilator o tym jakie dane przechowujemy nie może mieć po prostu typu pointer etc., deklarujemy je więc tak:
typ_danych *nazwa_wskaznika;
Teraz zmienna nazwa_wskaznika jest wskaźnikiem i możemy jej przypisywać adresy innych zmiennych, ale muszą być one typu typ_danych.
Pobranie adresu
edytujSkoro mamy już jakiś pojemnik na adres to trzeba też skądś wziąć właśnie sam „numer” komórki. Służy do tego operator &. Wystarczy poprzedzić nim nazwę zmiennej by zamiast jej zawartości operować na adresie.
int i = 5;
writefln("0x%X", &i);
Na wyjściu wypisane zostanie mniej-więcej to (wynik może być inny):
0x7FFCA3397568
Możemy też chcieć wypisać adres w formie decymalnej, wiedząc że jest to zwykła zmienna wystarczy rzutować ją na odpowiedni typ liczbowy:
int i = 5;
writefln("%d", cast(ulong) &i);
Otrzymamy wtedy wynik podobny do tego:
140728413839864
Odwołanie do wskazywanego obiektu
edytujDo tego na co wskazuje wskaźnik możemy odwołać się operatorem *. Wystarczy poprzedzić nim nazwę wskaźnika i już możemy operować na nim jak na normalnej zmiennej. Prosty przykład:
int num = 5; //Zmienna typu int z wartością 5
int *num_ptr = # //Wskaźnik wskazujący na „num”
writeln(*num_ptr); //Wypisanie danych spod wskaźnika
Stdout:
5
Skoro num i *num_ptr odnoszą się do tego samego miejsca w pamięci, to po zmianie tego na co wskazuje num_ptr zmienimy też zawartość oryginalnej zmiennej. Przykład:
int num;
int *num_ptr = #
*num_ptr = 12;
writeln(num); //Wypisze „12”
Alokacja pamięci
edytujMożemy dynamicznie zaalokować pamięć na 2 sposoby, używając malloc i new.
New
edytujNew jest bardzo proste w obsłudze i intuicyjne, jednak w D jego możliwości są ograniczone względem C++. Jeśli chodzi o wskaźniki (bo w D nie tylko tam new znajduje zastosowanie) to możemy zaalokować tylko pamięć na typy podstawowe, struktury itd., nie można z kolei alokować tablic (bo nie mają one typu int* a int[]) i klas.
Przykład:
struct Dog
{
string name;
int age;
}
int main(string[] args)
{
int* num = new int(57);
Dog* dog = new Dog("Reksio", 5);
writeln(*num, "\n", *dog);
return 0;
}
Stdout:
57 Dog("Reksio", 5)
Jak widać sprawa jest raczej prosta. Trzeba tylko pamiętać że w D nie ma operatora delete i pamięć usunie GC.
Malloc & Free
edytujDwie funkcje z nagłówka dają bardzo dużo kontroli nad zaalokowaną pamięcią. Sami możemy (nawet musimy) zdecydować kiedy oddać pamięć systemowi. Destruktory i konstruktory nie są wywoływane, więc w praktyce malloc i free nadają się tylko do operacji na suchych ciągach danych. Taka alokacja bardzo rzadko ma praktyczne zastosowania, więc przykład powinien wyczerpać temat:
import std.math; //pow - kwadrat liczby
import std.stdio; //IO
import core.memory; //Malloc & free
int main(string[] args)
{
//Dynamiczna alokacja tablicy 4096 elementów typu ulong (↓) bez maski (to 0)
ulong *longTab = cast(ulong*) GC.malloc(4096, 0, typeid(ulong));
//Pętla iterująca po wszystkich elementach…
foreach(double i; 0 .. 4096)
longTab[cast(int) i] = cast(ulong) pow(i / 100, 2); //f(i) = (i / 100) ^ 2
//Pętla iterująca po co 16 elemencie
for(int i = 0; i < 4096; i += 16)
{
//Pętla iterująca po najbliższych 16 elementach
foreach(int j; 0 .. 16)
writef("%5d", longTab[i + j]); //Wypisanie ich kolejno w wieszach po 16
writeln(); //Zejście linię niżej
}
GC.free(longTab); //Zwolnienie zaalokowanej pamięci (ok. 32KB)
return 0;
}
Wynik ma 256 linii, więc go tu nie zamieszczę.