D/Zmienne
Zmienne i stałe
edytujKomputery operują na danych umieszczonych w pamięci. Pamięć jest podzielona na wiele komórek o stałym rozmiarze (bajtów) do których można odwoływać się poprzez adres, numer komórki.
Zmienne są fragmentem pamięci komputera o określonym rozmiarze do którego możemy się łatwo odwoływać poprzez nazwę, inaczej identyfikator.
Identyfikatory
edytujIdentyfikatory w języku D buduję się z liter, cyfr oraz znaku podkreślenia. Pierwszy znak identyfikatora nie może być cyfrą. W D można używać znaków narodowych dla identyfikatorów, jednak jeśli kod będzie edytowany przez ludzi z różnych krajów nie jest to najlepszy pomysł. (Znaki narodowe mogą się przydać natomiast przy nauce języka, u młodych uczniów).
Przykłady poprawnych identyfikatorów:
v x3 prędkość sc_d2 p55 V // UWAGA: patrz opis poniżej _syscall // j.w.
nie są poprawne następujące identyfikatory:
4ped // BŁĄD: zaczyna się od cyfry ilosc.znakow // BŁĄD: zawiera kropkę
Język D rozróżnia małe i duże litery (ang. case sensitive) w identyfikatorach, tak więc identyfikator v jest inny niż V. Należy również zaznaczyć, iż identyfikatorów zaczynających się od podkreślenia nie powinno się używać w programach, ponieważ są one używane przez biblioteki oraz kompilator do specjalnych celów, co może powodować konflikty.
W języku D identyfikatory to ciągi liter, cyfr oraz znaku podkreślenia. Identyfikator nie może rozpoczynać się cyfrą. |
Przy wybieraniu identyfikatora (nazwy) staraj się aby była ona samowyjaśniająca się, tzn. odpowiadała temu czemu służy i nie wprowadzała w błąd. Dobrze aby nie była zbyt długa (lepiej opisać jej znaczenie w komentarzu), ani zbyt krótka (jako trudny do odszyfrowania skrót). Zwykle dobrymi nazwami są pojedyncze litery jako nazwy zmiennych (o ile nie używa się ich w dużej ilość w jednym miejscu), w szczególności są pewne konwencje co do pojedynczych liter. Dobre są również pojedyncze słowa. Przykłady
a, c, d // parametry x, y, z // szukana niewiadoma i, j, k // indeks pętli tekst // tekst wyświetl // funkcja wyświetlająca xgg1 // NIEDOBRZE: co to jest? f // NIEDOBRZE: funkcja, która nie wiadomo co robi, // chyba że to jakaś ogólna funkcja zapisz // jeśli zapisz jest funkcją, która wykonuje coś innego // niż zapisywanie, NIE JEST TO DOBRA nazwa
Używaj treściwych identyfikatorów, opisujących ich znaczenie. Nie używaj identyfikatorów rozpoczynających się od podkreślenia. |
Część nazw jest zarezerwowana jako słowa kluczowa i nie można ich używać jako identyfikatorów. Słów kluczowych jest kilkadziesiąt i poznamy wszystkie w toku całego podręcznika, jednak juz tutaj podamy ich część (bez wyjaśnienia) ponieważ może Ci przyjść pomysł użycia ich jako identyfikatorów, a nie jest to dozwolone (o czym zapewne poinformowałby kompilator):
in, out, body, double, do, this, version, super,
new, function, default, debug, alias, auto
Uwaga!
|
Deklarowanie zmiennych
edytujZmienne w języku D deklaruje się używając następującej składni:
typ nazwa_zmiennej;
jeśli chcemy zaś zadeklarować naraz więcej zmiennych tego samego typu można to zrobić wypisując zmienne po przecinku:
typ zmienna1, zmienna2, zmienna3;
Zmienne w języku D mogą być deklarowane praktycznie w dowolnym miejscu programu, jednak muszą się one pojawić przed użyciem (wyjątkiem są zmienne globalne).
Aby wyjaśnić lepiej mechanizm działania zmiennych skupimy się na typie int (od ang. integer - liczba całkowita), jest to typ liczb całkowitych, a zmienna zadeklarowana z takim typem może zmieścić dowolną liczbę w zakresie od ok. -2 mld. do +2 mld. Typ ten zajmuje 4 bajty (32 bity) pamięci. Za chwilę poznamy dokładnie wszystkie wbudowane typy oraz ich zastosowania.
int x;
Definiuje to zmienną o nazwie x oraz typie int. Zmienna ta zostanie zainicjalizowana liczbą 0 (jest to wartość stałej int.init). Możemy również zainicjalizować tą liczbą inną wartością, np. 100:
int x = 100;
albo kazać kompilatorowi jej nie inicjalizować:
int x = void; // UWAGA: zmienna x może wtedy zawierać śmieci i mieć przypadkową wartość
w języku D wartość którą inicjalizujemy może być pewnym wyrażeniem (np. wyrażeniem matematycznym), na przykład:
int x = 24*60*60; // prawidłowo zainicjalizuje się jako 86400
Inicjalizację można przeprowadzić używając dowolnych wyrażeń o ile jest to wykonywane w bloku funkcyjnym, na przykład:
int s = 6;
int x = 2*s;
Inicializacja zmiennych globalnych jest też możliwa w podobny sposób, ale zwykle musi być wykonywalna na etapie kompilacji.
Jak już widzisz, zmiennych typu int używa się jak zmiennych w matematyce. Można na nich wykonywać podstawowe operacje matematyczne, oraz podstawiać wyniki tych operacji do innych (albo tych samych!) zmiennych. Na przykład, nasz pierwszy program używający zmiennych może wyglądać tak:
import std.stdio;
void main() {
int a = 6; // utworzenie zmiennej o nazwie a i wartości początkowej 6
int b = 7; // analogicznie o nazwie b i wartości 7
int c; // utworzenie zmiennej c i inicjalizacja przy pomocy 0
c = a+b; // przypisanie zmiennej c wartości będącej sumą zmiennej a i b
writefln(c); // wyświetlenie wyniku
}
Przykład powyżej można napisać na kilka różnych sposobów. Wypiszemy tutaj kilka wariantów, aby oswoić się ze składnią.
import std.stdio;
void main() {
int a = 6, b = 7; // utworzenie dwóch zmiennych: a, b
int c = a+b; // utworzenie zmiennej c z wartością początkową równą a+b
writefln(c);
}
albo po prostu:
import std.stdio;
void main() {
int a = 6, b = 7;
writefln(a+b); // wynik dodawania nie zostanie zapisany do zmiennej
// tylko od razu przekazany funkcji writefln
}
Stałe i niezmienniki
edytujStałe
edytujStałe to zmienne, których nie możemy w sposób jawny modyfikować. Mimo paradoksalnej nazwy, przyjeło się je nadal określać jako zmienne, ponieważ zachowują się one praktycznie tak jak zmienne z punktu widzenia języka. Używanie stałych ma dwojaki cel: ustrzega nas przed ewentualnym błędem, który byśmy popełnili zmieniając zawartość takiej zmiennej oraz pozwala lepiej i łatwiej zoptymalizować kod.
Stałe deklaruje się dopisując modyfikator (atrybut zmiennej) const:
const int n = 10;
możemy teraz używać zmiennej n jak każdej innej zmiennej, jednak nie możemy jej zmienić:
writefln(5*n); // wypisz 50
n = 20; // BŁĄD: kompilator się na to nie zgodzi!
Nawet próba zmiana zmiennej na tą samą wartość jest błędem:
const int n = 10;
n = 10; // BŁĄD
n = n; // też BŁĄD
Ważne jest, aby stałą zainicjalizować w momencie deklaracji, ponieważ bardzo podobna konstrukcja jest nie poprawna:
const int n; // stwórz stałą
n = 10; // przypisz jej wartość 10
Kompilator zaprotestuje w obu linijkach. W pierwszej ponieważ nie zainicjalizowaliśmy stałej, a w drugiej ponieważ zmieniamy stałą (która mogła bybyć równa 0, gdyby inicjalizować ją podobnie jak zwykłe zmienne int).
Przykładem stałych mogą być np. pi, e (podstawa logarytmów naturalnych), wielkość zbioru danych na którym operuje program, czy jakieś parametry ustalone przez użytkownika, które w czasie wykonania programu się nie zmieniają.
Nazwę typu możemy pominąć, jeśli wyrażenie inicjalizujące po prawej stronie znaku przypisania jest znanego typu:
const n = 10; // równoważne const int n = 10;
n zostanie stworzona jako typu int, ponieważ 10 ma typ int. Jest to tak zwane automatyczne wnioskowanie (ang. inference) typu. Więcej o tej cennej własności opowiemy na końcu tego rozdziału.
Stałe mogą być też inicjalizowane dynamicznie, co ma szczególnie zastosowanie w funkcjach (i metodach):
auto f(int a, int b) {
const s = a + b;
// s = 3; // BŁĄD.
return s * s;
}
Tutaj użyliśmy const jako tymczasowe miejsce do przechowania jakieś wartości w funkcji, której nie chcemy zmieniać do końca wywołania tej funkcji. Ułatwia to też często czytanie i analizowaniu kodu, ponieważ nawet przy długiej funkcji, wiemy od razu, że taka zmienna nie będzie zmieniała swojej wartości (nigdzie w funkcji nie ma instrukcji która by nadpisywała wartość tej funkcji).
Niezmienniki
edytujNiezmienniki są to dane, które nie mogą zostać w żaden sposób zmienione. Różnica pomiędzy stałymi a niezmiennikami jest taka, że stałe mogą zostać zmienione z innej referencji nieoznaczonej słowem const, a niezmienniki nie.
Do oznaczania danych jako niezmienniki służy słowo kluczowe immutable.
Domyślnie niezmiennikami są na przykład ciągi znaków. Ten fakt może z początku sprawić problem, ponieważ kompilacja poniższego kodu skończy się błędem:
char[] x = "tekst";
Kompilator poinformuje nas wtedy o niemożności konwersji typu immutable(char[]) do char[]. W takim przypadku należy użyć własności tablic dup, która zwróci zwykłe dane (nie immutable):
char[] x = "tekst".dup;
Inną opcją rozwiązania tego problemu jest użycie typu string zamiast char[].
string x = "tekst";
Dzięki temu jeśli będziemy potrzebowali stałej, której na 100% nikt nie zmieni bo informuje o czymś elementarnym i poprawność tych danych ma wpływ na działanie całego programu, np. wbudowany w program klucz do szyfrowania/deszyfrowania danych (to tylko przykład - rzadko dobre rozwiązanie) będziemy mogli użyć niezmiennika i nie martwić się o te dane.
immutable char[] klucz_128bit = "0123456789abcdef";
Dodatkowym udogodnieniem jest to, że definiując niezmiennik typu char[] nie musimy dodawać .dup na końcu ciągu.
Zasięg i czas życia
edytujZmienne mogą się znajdować poza funkcjami, wtedy nazywam je zmiennymi globalnymi (poprawniej zmienne o zasięgu bieżącego modułu). Są dostępne ze wszystkich funkcji. Na przykład
import std.stdio;
void func() { // tworzymy własną funkcję o nazwie func
a = 5; // zmieniającą zmienną o nazwie a ustawiając ją na 5
}
int a = 6; // utworzenie zmiennej globalnej o nazwie a i wartości 6
void main() { // tu program rozpoczyna działanie
writefln(a); // wyświetli 6
a = 7; // zmieniamy wartość
writefln(a); // wyświetli 7
func(); // wywołujemy naszą funkcję (która modyfikuje a)
writefln(a); // wyświetli 5
}
Należy zauważyć, iż w funkcji func użyliśmy zmiennej a zanim w kodzie źródłowym pojawiła się informacja o tej zmiennej (ang. forward reference). W D jest to dozwolone. Tak samo, mogli byśmy napisać funkcje func za funkcją main (na samym dole; patrz następny przykład), i użyć identyfikatora func w funkcji main. Jest to znaczący postęp w stosunku do języka C w którym przed użyciem wszystkie identyfikatory muszą być znane.
Zmienne które definiuje się na "prywatne" potrzeby funkcji nazywamy zmiennymi lokalnymi i nie są dostępne z innych funkcji. A w przypadku gdy istnieje zmienna globalna o takiej samej nazwie zostanie ona "zasłonięta" (ang. shadowing).
import std.stdio;
int a = 5; // zmienna globalnej
void main() {
int a = 6; // zmienna lokalna (zasłania globalne a)
writefln(a); // wyświetli 6 (a nie 5)
func();
writefln(a); // wyświetli 6 (ponieważ func modyfikuje zmienną globalną, a nie lokalną)
a = 8;
writefln(a); // wyświetli 8
func2(); // wyświetli 11
writefln(.a); // wyświetli 11 (kropka służy do dostępu do zmiennych
// globalnych w przypadku ich zasłonięcia)
}
void func() {
a = 11; // modyfikacja zmiennej globalnej (bo jedynie takie a "zna")
}
void func2() {
writefln(a); // wyświetli zmienną globalna
}
Fragment kodu źródłowego w którym zmienna jest dostępna nazywamy zasięgiem (ang. scope) zmiennej. I tak np. zmienna a zdefiniowana w funkcji ma zasięg ograniczony do funkcji main. Natomiast zasięg zmiennej globalnej rozciąga się na cały plik (moduł).
Zasięg zwykle jest zdefiniowany przez strukturę blokową - zmienna jest dostępna w bloku w którym została zadeklarowana oraz w blokach wewnętrznych. W blokach wewnętrznych można zasłaniać zmienne spoza funkcji w której zostały zadeklarowane (np. zmienne globalne):
import std.stdio;
int d = 1;
void main() {
int a = 6; // zmienna lokalna
int c = 7; // j.w.
{ // tworzymy nowy blok
int b = 11; // nowa zmienna lokalna, w bloku wewnętrznym
//int c = 12; // j.w. + zasłonięcie - BŁĄÐ: nie wolno
int d = 13; // j.w., ale zasłaniamy zmienną globalną (spoza funkcji main)
writefln(a, " ", b, " ", d); // wyświetli "6 11 13"
a = 100;
//c = 101;
d = 102;
} // koniec bloku. Tu zmienna b staje się już niedostępna (kończy się jej czas życia)
writefln(a); // wyświetli 100
writefln(c); // wyświetli 7 // (a nie 101!)
writefln(d); // wyświetli 1 (a nie 102!)
//writefln(b); // BŁĄD. zmienna b jest niedostępna
}
Zmienne o ograniczonym zasięgu, można tworzyć również w wyrażeniach warunkowych oraz pętlach. Ich zasięg będzie ograniczony zawartością bloku warunkowego czy też treści pętli.
Typy zmiennych wbudowanych
edytujJęzyk D posiada kilkanaście typów wbudowanych. Przedstawimy tutaj po kolei ich cechy i zastosowania.
Typ logiczny
edytujTyp bool jest typem przeznaczonym do przechowywania zmiennych boolowskich, są to zmienne logiczne o wartościach prawda (ang. true) lub fałsz (ang. false).
bool czyZapisywac = true;
bool blokada = false;
Zmienne typu bool są używane w instrukcjach warunkowych, oraz są wynikiem działania operatorów relacji oraz operatorów logicznych. Operatorów logicznych jest kilka:
bool a, b;
bool a_i_b = a && b; // logiczny operator && - logiczne i (ang. and)
bool a_lub_b = a || b; // logiczny operator || - logiczne lub (ang. or)
bool nie_a = ! a; // jednoargumentowy operator ! - logiczne nie (ang. not)
bool a_albo_b = a ^ b; // logiczny (i bitowy) operator ^ - alternatywa wykluczająca (ang. xor - exclusive or)
Wartość odpowiedniego wyrażenia, będzie zgodna z znanymi z logiki regułami. a_i_b będzie miało wartość true, jeśli a i b, mają równocześnie wartość true, w przeciwnym razie będzie miało wartość false (jedna, lub obie wartości były false). a_lub_b będzie miało wartość true, jeśli a lub b (lub obie zmienne), ma wartość true, w przeciwnym wypadku będzie miało wartość false (obie zmienne miały wartość false). Natomiast, nie_a będzie mieć wartość true, jeśli a miało wartość false, w przeciwnym wypadku odwrotnie, nie_a będzie mieć wartość false, jeśli miało wartość true. a_albo_b będzie miało wartość true, jeśli a i b, mają różne wartości, tzn. true i false, albo false i true, w przeciwnym wypadku będzie mieć wartość false (oznaczające że obie zmienne miały taką samą wartość, tzn. true i true, albo false i false).
Na przykład:
bool a = true;
bool b = false;
bool c = a && b; // c będzie miało wartość false
Operatory and, or, not oraz xor, można również używać na zmiennych typów całkowitych, lecz mają one trochę inne znaczenie, oraz inny zapis.
Typy całkowite
edytujDo typów całkowitych (integer) zalicza się następujące typy:
typy | rozmiar (typ.sizeof) |
---|---|
byte, ubyte | zmienne o rozmiarze 1 bajta |
short, ushort | zmienne o rozmiarze 2 bajtów |
int, uint | zmienne o rozmiarze 4 bajtów |
long, ulong | zmienne o rozmiarze 8 bajtów |
cent, ucent | zmienne o rozmiarze 16 bajtów (zarezerwowane na przyszłość) |
Czasami również zalicza się tutaj typy:
typy | rozmiar (typ.sizeof) |
---|---|
char | 1 bajt |
wchar, dchar | 2 oraz 4 bajty |
jednak ponieważ służą one do przechowywania znaków, a nie liczb, zajmiemy się w następnym podpunkcie.
Jak widać ilość typów zmiennych jest bardzo duża, różnią się one wielkością oraz każdy typ ma typ o identycznym rozmiarze tylko z dopisanym u. Z racji różnic w wielkości zmiennych mogą one przechowywać wartości całkowite z różnych zakresów, oraz zajmować więcej lub mniej miejsca w pamięci komputera. Wybór typu zmiennej jest ważny z kilku powodów. Po pierwszej jeśli zmiennych danego typu będzie bardzo dużo, to najlepiej wybrać typ możliwie najmniejszych który spełni swoje zadania celem redukcji zużycia pamięci. Po drugie typy o mniejszym rozmiarze zwykle są szybsze (zarówno ich przesyłanie z pamięci do procesora jak i wykonywanie operacji na nich).
Kiedy już wybierzemy typ należy uważać, aby nie przekroczyć dozwolonego zakresu - nastąpi tzw. przekręcenie, na przykład dla zmiennej typu ubyte największa możliwa wartość zmiennej to 255, jeśli dodamy do zmiennej z wartością 200 inną zmienną z wartością 60 to kompilator zapewne nas ostrzeże ponieważ wynik 260 nie jest możliwy do zapisania w takiej zmiennej, a dla procesora będzie to naprawdę oznaczać liczbę 0 (początek zakresu). W przypadku int taki koniec jest o wiele większy jednak w takim wypadku kompilator nie ostrzeże nas o tym.
Zmienne standardowo są zmiennymi ze znakiem, i w przybliżeniu potrafią reprezentować tyle samo liczb ujemnych co dodatnich.
Zmienne z dopisanym "u" na początku są to zmienne bez znaku (ang. unsigned, ang. sign - znak). Zmienne takie mogą reprezentować dwa razy więcej liczb dodatnich niż zmienne bez znaku, ale równocześnie nie mogą reprezentować liczb ujemnych.
typ | rozmiar (typ.sizeof) | zakres (typ.min .. typ.max) | typ.init |
---|---|---|---|
byte | 1 bajt (8 bitów) | -128..127 | 0 |
ubyte | 1 bajt (8 bitów) | 0..255 | 0 |
short | 2 bajty (16 bitów) | -32768..32767 | 0 |
ushort | 2 bajty (16 bitów) | 0..65535 | 0 |
int | 4 bajty (32 bitów) | -2147483648..2147483647 | 0 |
uint | 4 bajty (32 bitów) | 0..4294967295 | 0 |
long | 8 bajtów (64 bitów) | -9223372036854775808..9223372036854775807 | 0L |
ulong | 8 bajtów (64 bitów) | 0..18446744073709551615 | 0L |
cent | 16 bajtów (128 bitów) | -2^127..2^127-1 | 0 |
ucent | 16 bajtów (128 bitów) | 0 .. 2^128-1 | 0 |
Typy cent i ucent na razie nie są dostępne, ale słowa kluczowe zostały zarezerwowane na przyszłość. |
W większości naszych programów, nie będziemy przykładać większej wagi do typów całkowitych, i dla wygody będziemy używać standardowo typu int, który jest bardzo uniwersalny.
Często w programie będziemy musieli użyć konkretnej wartości liczbowej, używamy do tego literałów.
Literały całkowite zapisujemy jako ciąg cyfr dziesiętnych, poprzedzonych opcjonalnym znakiem minusa (negacji). Literały domyślnie są typu int:
int x = -817263;
Jeśli chcemy aby literał był typu long należy dopisać literę L na samym końcu:
long y = 172387618726L;
Jeśli chcemy literał o typie bez znaku, należy dopisać dodatkowo literę u.
ulong z = 172387618726Lu;
Literały można wprowadzać również w systemie dwójkowy (ang. binary), ósemkowym (ang. octal) oraz szesnastkowym (ang. hexadecimal). W systemie ósemkowym możemy wykorzystywać cyfry 0..7, a całą liczbę poprzedzić zerem, np.
int trzynaście = 015 // 1*8 + 5 = 13
W sysetmie szesnastkowym możemy wykorzystywać cyfry 0..9, oraz litery a..f (lub A..F) jako cyfry 10..15, a całą liczbę należy poprzedzić dwoma znakami, mianowicie 0x (lub 0X).
int pięćset = 0x01f4; // 1*(16*16) + 15*16 + 4 = 256 + 4 + 240 = 500
W systemie dwójkowym możemy wykorzystywać jedynie cyfry 0 i 1, a liczbę należy poprzedzić dwoma znakami: 0b (lub 0B):
ushort d = 0b1000011010100101
Przy wprowadzaniu długich literałów wygodnie może być rozdzielić kolejne tysiące, albo czwórki w systemie dwójkowym czy szesnastkowym. Służy temu znak podkreślenia, np.
int x = -817_263;
ulong y = 172_387_618_726L;
ushort d = 0b_1000_0110_1010_0101
uint kod = 0xA_D___E0__1522;
Dzięki temu można poprawić czytelność liczb, ale czasami również utrudnić, należy więc używać tej cechy z rozwagą.
Typy zmiennoprzecinkowe
edytujPoza liczbami całkowitymi wielokrotnie będziemy chcieli wykonywać obliczenia na liczbach rzeczywistych. Standardowo komputery używają tak zwanych liczb zmiennoprzecinkowych (ang. floating point numbers, floats), do przybliżenia liczb rzeczywistych. Są one przybliżone, i mogą reprezentować tylko pewien skończony podzbiór wszystkich liczb rzeczywistych (podobnie jak typy całkowite), ale w większości zastosowań, jest to przybliżenie wystarczające w praktycznych zastosowaniach.
Język D posiada kilka typów zmiennoprzecinkowych. W większości przypadków używa się typu double, ponieważ jest on dużo dokładniejszy niż float.
typy | opis (wg IEEE 754 oraz specyfikacji D) | rozmiar (typ.sizeof) | bity | mantysa | cecha | znak | typ.init | dokładność dziesiętna |
---|---|---|---|---|---|---|---|---|
float | Liczba zmiennoprzecinkowa pojedynczej precyzji | 4 | 32 | 23 | 8 | 1 | float.nan | ok. 7 cyfr znaczących |
double | Liczba zmiennoprzecinkowa podwójnej precyzji | 8 | 64 | 52 | 11 | 1 | double.nan | ok. 16 cyfr znaczących |
real | Największa dostępna precyzja na danej architekturze (np. extended double) | 10/12 | 80 | ? | ? | ? | real.nan | około 20, gwarantowane około 16 |
W przeciwieństwie do liczb całkowitych, liczby zmienne zmiennoprzecinkowe (i zespolone) nie są inicjalizowane zerem, tylko wartością NaN (ang. Not a Number - nie liczba). Jest to wartość, która nie ma sensu. Ma ona tą cechę, że jeśli się użyje jej w jakiejkolwiek operacji, wynik będzie również miał wartość NaN (ponieważ przynajmniej jeden argument był NaN) - mówimy, że NaN się propaguje. Wartości NaN powstaje czasami kiedy obliczamy wyrażenia nie zawierające NaN. Na przykład 0.0/0.0, daje w wyniku NaN, ponieważ wyrażenie to matematycznie nie jest określone, podobnie obliczenie pierwiastka z liczby ujemnej, czy obliczenie funkcji odwrotnej do sinusa, dla argumentu większego niż 1. Z powodu propagacji NaN-ów, wystarczy jeden taki błąd w trakcie obliczeń, a i tak pojawi się on zapewne w wynikach. Tak więc obecność NaN w wynikach świadczy albo o błędach w algorytmie, nie uwzględnianiu problemów związanych ze skończoną dokładnością, albo brakiem inicjalizacji zmiennych. W przypadku zmiennych rzeczywistych, język D zakłada, że lepiej pozostawić to programiście.
float a; // a ma wartość NaN
Literały zmiennoprzecinkowe można zapisywać na kilka sposobów:
double a = 1.0; // normalnie: 1.0
double b = 1.; // 1.0
double c = .5; // 0.5
double d = 1.0e3; // styl naukowy: 1.0 * 10^3 = 1000.0
double e = -5.1e-5; // -0.000051
double f = 5e11; // dużo
Podobnie jak literały całkowite, literały zmiennoprzecinkowe mogą używać znaku _ do rozdzielania różnych części:
double f = 1_000_000_000_000.0; // 1 bilion, 1.0e12;
Standardowo literały są typu double. Jeśli chcemy aby literał był typu float należy dopisać małą lub dużą literę f na końcu, a aby był typu real to należy dopisać dużą literę L.
float z = 1.0f;
real x = 1.213172387162837162876178362L;
Literały rzeczywiste mogą być wprowadzane w systemie szesnastkowym, podobnie jak literały całkowite. Używa się do tego na początku 0x, a do wpisania eksponenty używa się małej lub dużej litery p (ponieważ litera e należy do zbioru liczb szesnastkowych).
double p = 0x1.07F1EEA12ADA6p512; // wykładnik jest dziesiętny!
Jeśli chcemy użyć wartości NaN bezpośrednio, bez przypisywania tworzenia nowej zmiennej, możemy użyć atrybutu nan każdego z powyższych typów, np
import std.math : sin;
/* Reszta kodu */
float y = sin(float.nan);
Zmienna y, będzie zawierać też wartość float.nan, ponieważ funkcja sin propaguje tą wartość.
Typy urojone i zespolone
edytuj ifloat, idouble, ireal // urojone (ang. imaginary)
cfloat, cdouble, creal // zespolone (ang. complex)
D zawiera wbudowane w język zmienne urojone i zespolone. Jeśli nie wiesz czym są liczby urojone i zespolone, możesz zignorować ten podpunkt, ale zachęcamy do poczytania o liczbach zespolonych na Wikipedii, lub podręczniku do matematyki.
Typy urojone mają analogiczną dokładność, jak typy rzeczywiste, ale reprezentują liczby na osi urojone, a nie rzeczywistej. Typy zespolone to para zmiennej rzeczywistej i urojonej o danych precyzjach, a więc zajmują dwa razy więcej miejsca w pamięci. W przypadku zmiennych zespolonych, dostęp do części rzeczywistej i urojonej odbywa się przez dopisanie, .re lub .im na końcu.
Literały urojone powstają podobnie jak literały rzeczywiste, z tym że na samym końcu dopisujemy literę i. Literały zespolone to suma literału rzeczywistego oraz urojonego rozdzielone znakiem + albo -. Na przykład:
ifloat a = 3.1i;
cfloat b = 1.0 - 2.0i;
Przykłady prostych obliczeń:
float a = 4.2;
ifloat b = 2.0i;
ifloat c = a*b; // 8.2i
cfloat d = 2.0 + 4.0i;
cfloat e = d*c; // -32.8 + 16.4i
float f = e.re; // -32.8
float g = e.im; // 16.4;
Podobnie jak liczby zmiennoprzecinkowe zmienne urojone są inicjalizowane wartością NaN (np. ifloat.init = float.init * 1.0fi), a zmienne zespolone otrzymują taką wartość dla obu części (np. cdouble.init = double.init + double.init * 1.0i).
ifloat a; // Re(a) ma wartość NaN
cfloat b; // b.re i b.im mają wartość NaN
Typ void
edytujTyp void (ang. pustka), to tak naprawdę brak typu.
Używa się go na przykład do wskazania, że funkcja nie zwraca żadnych rezultatów (czyli jest formalnie procedurą), albo jako void*, może służyć jako wskaźnik do bliżej nie określonych danych. Użycie void* nie jest zalecane. Więcej o wskaźnikach w kolejnych rozdziałach.
void odswiez() { // procedura, która coś robi,
// lecz nie zwraca wyników;
... // więcej o funkcjach w odpowiednim rozdziale
}
void* s = VideoMemmoryAddress(); // wskaźnik na "coś" w pamięci -
// dosyć niebezpieczna konstrukcja!
Typu void można również użyć do wykonania operacji arytmetycznych bez zapisywania jej do jakiejkolwiek zmiennej poprzez rzutowanie, na przykład:
1+2; // BŁĄD: 1+2 powinno zostać zapisane do jakiejś zmiennej
// lub przekazane do funkcji
cast(void)(1+2); // OK
Więcej o rzutowaniach (bardziej sensownych) na końcu rozdziału.
Modyfikatory
edytujInnymi ważnymi modyfikatorami stałych poza const są modyfikatory volatile, static, auto oraz extern.
auto
edytujModyfikator auto służy do automatycznego wnioskowania typu, na przykład:
int a = 5;
auto x = a+4; // tworzy zmienną typu int, ponieważ a+5 (int+int) jest typu int
auto y = x/2.0; // tworzy zmienną typu double ponieważ x/2.0 (int/double) jest typu double
modyfikator ten niezmiernie się przydaje kiedy wyrażenie po prawej stronie jest skomplikowanego typu (w typach czasami można się pogubić), i kłopotliwe było by ich jawne wypisywanie (również ponieważ ewentualna modyfikacja typów zmusza do zmiany wszystkich ich wystąpienia oraz wiedzy jaki to ma być typ). Modyfikator ten w żaden sposób nie wpływa na szybkość wykonywania kodu, ponieważ typy są wnioskowane w czasie kompilacji (D jest językiem statycznie typowanym)
scoped
edytujNiegdyś w D istniał modyfikator scope, na dzień dzisiejszy jego zadanie przejął szablon std.typecons.scoped, dalej scoped.
Uwaga!
|
Uwaga!
|
Ponieważ w języku D obsługa pamięci jest zazwyczaj automatyczna i jest wykonywana przez tzw. garbage collector (ang. odśmiecacz pamięci), to po wyjściu z zasięgu zmienna jest "zapominana" przez program, lecz nadal istnieje (nie jest już dostępna). Garbage collector cyklicznie sprawdza pamięć w poszukiwaniu takich zmiennych i je usuwa. Czasami jednak chcemy aby zmienna została zniszczona w momencie wyjścia z zasięgu w sposób automatyczny (zmienne te są tworzone zwykle na stosie), a w przypadku obiektów został wywołany natychmiast destruktor - a nie jak w przypadku garbage collectora, w bliżej nie określonej przyszłości. Służy temu właśnie szablon scoped. Przykład:
import std.typecons;
// ...
void moja_funkcja() {
auto x = new scoped!X(); // utworzenie zmiennej x będąc obiektem typu X
// ...
} // zniszczenie zmiennej x i wywołanie destruktora
przy braku szablonu scoped zmienna stała by się niedostępna, jednak nadal by istniała w pamięci, a jej destruktor nie został by wywołany.
static
edytujModyfikator static oznacza wiele różnych rzeczy w zależności od kontekstu, i co innego, niż w językach C czy C++.
Tutaj tylko napiszemy, że w kontekście zmiennej wewnątrz funkcji, modyfikator static, powoduje że mimo wyjścia z zasięgu zmienna nie jest niszczona, i przy następnym wejściu w dany zasięg, zmienna będzie posiadała poprzednią wartość.
void kwadrat(int x) {
static int ilosc_wykonan = 0; // lub poprostu static int ilosc_wykonan; albo static ilosc_wykonan = 0;
ilosc_wykonan = ilosc_wykonan + 1;
writefln("Funkcja kwadrat została wykonana ", ilosc_wykonan, " razy do tej pory").
int k = x*x;
return k;
}
Powyższa funkcja, przy pierwszy wykonaniu, zaincjalizuje zmienna ilosc_wykonan wartością 0. Następnie powiększy ją o jeden, do wartości 1, i wyświetli jej wartość. Przy następnym wykonaniu, inicjalizacja zostanie pominięta, a powiększenie o jeden, spowoduje przypisanie wartości 2. Tak więc zmienna ilosc_wykonan służy tutaj za prosty licznik ilości wywołań funkcji kwadrat. Zmienna k, będzie za każdym razem inicjalizowana na nowo, ponieważ nie jest statyczna.
Słowo static, może być ponadto użyte przed metodami, funkcjami, importami modułów, i pętlami oraz wyrażeniami warunkowymi. Są to zastosowania specjalne, o których wspomnimy dalej w podręczniku.
extern
edytujModyfikator extern (z ang. zewnętrzny) w D ma rzadkie zastosowanie. Służy on do poinformowania kompilatora o istnieniu danego identyfikatora i jego typu, ponieważ na przykład znajduje się w bibliotece o której kompilator nie wie. Ponieważ jednak język D ma rozbudowany system modułów oraz umożliwia użycie symboli przed ich definicją/deklaracją w większości przypadków nie trzeba stosować tego modyfikatora (zwykle jest to potrzebne w C, przy używaniu bibliotek i plików nagłówkowych). Modyfikator ten przydaje się przy współpracy z innymi językami programowania (umożliwia on zmian tzw. konwencji wywołań funkcji). W rozdziale Łączenie z innymi językami poznamy jego zastosowania.
Nadmienić należy jedynie, że zastosowanie modyfikatora extern nie powoduje alokacji pamięci dla zmiennej, a jedynie poinformowanie kompilatora. Taka konstrukcja jest więc jedynie deklaracją, nie zaś definicją. W D deklaracja i definicja to jedno i to samo, ponieważ odbywa się w tej samej linii - z tego powodu w całym tym rozdziale odwoływaliśmy się do tego faktu jako deklaracja. Formalnie poprawniej było by nazwać wszystkie konstrukcje z tego rozdziału właśnie definicjami. Definicja konstruuje zmienną (zajmuje miejsce i przydziela jakąś wartość), natomiast deklaracja jedynie mówi o typie. Oczywiście definicja jest jednocześnie deklaracją, lecz nie odwrotnie.
W języku D używa się jedynie definicji. Deklaracje są z natury niepotrzebne, ponieważ system modułów umożliwia kompilatorowi poznanie wszystkich potrzebnych identyfikatorów. |
volatile
edytujCzasami się zdarza, że kompilator optymalizując kod widzi, iż w kilku kolejnych instrukcjach zmienna jest często wykorzystywana, więc ma taką samą wartości można ją trzymać w rejestrze (który jest szybszy niż pamięć), bo się przyda w fragmencie kodu który jest często wywoływany (np. pętla). Dodatkowo kompilator może założyć, że zmienna się nie zmienia poza miejscami w których do niej zapisujemy. Jednak takie założenie może być błędne, np. jeśli zmienna może być modyfikowana przez inny proces (w aplikacji wielowątkowej, albo znajduje się w pamięci współdzielonej) czy też jest to wartość odczytywana z jakiegoś portu urządzenia. Modyfikator volatile (ang. ulotny) nakaże za każdym razem odczyt tej zmiennej z pamięci.
volatile int temperatura_z_czujnika;
register
edytujW języku D nie ma modyfikatora register.
W językach takich jak C czy C++ to słowo kluczowe jest wskazówką dla kompilatora, aby umieścić zmienna w rejestrze procesora, który jest dużo szybszy od pamięci. W języku D zadanie optymalnego ułożenia zmiennych jest pozostawiona kompilatorowi. Jest ku temu kilka powodów: aktualnie dostępne algorytmy przydziału rejestrów są wystarczająco dobre, w językach C/C++ słowo to i tak było często ignorowane ponieważ nie zawsze jest możliwe spełnienie żądań programisty, aktualne procesory maja coraz więcej rejestrów, zawsze można napisać ręcznie kod asemblerowy w wydajnościowo krytycznej sekcji kodu.
Rzutowania jawne i niejawne
edytujRzutowanie polega na zmianie zmiennej jednego typu na drugiego. Może być ono jawne lub niejawne w zależności od tego czy programista zapisze jakiś fragment kodu zajmujący się tym, czy też wszystko obsłuży kompilator. Rzutowanie niejawne ma bardzo prostą postać:
int a = 2; double d = a; //d = 2, przypisaliśmy do typu zmiennoprzecinkowego wartość całkowitą, ale kompilator się tym zajął.
Rzutowanie jawne jest za to niezbędne przy konwersji double → int, dzięki temu programista musi świadomie potwierdzić że wie o utracie części ułamkowej:
double d = 2.9; int a = cast(int) d; //a = 2
Informacje o rzutowaniu instancji klas itd. będą opisane w rozdziale o obiektach w D. |