D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

duyarlık: [precision], sayının belirgin basamak (hane) adedi
infinity: [infinity], sonsuzluk
kırpılma: [truncate], sayının virgülden sonrasının kaybedilmesi
nitelik: [property, attribute], bir türün veya nesnenin bir özelliği
sırasızlık: [unordered], sıra ilişkisi olmama durumu
taşma; üstten veya alttan: [overflow veya underflow], değerin bir türe sığamayacak kadar büyük veya küçük olması
... bütün sözlük



İngilizce Kaynaklar


Diğer




Kesirli Sayılar

Tamsayıların ve aritmetik işlemlerin oldukça kolay olduklarını, buna rağmen yapılarından kaynaklanan taşma ve kırpılma gibi özellikleri olduğunu gördük.

Bu bölümde de biraz ayrıntıya girmek zorundayız. Eğer aşağıdaki listedeki herşeyi bildiğinizi düşünüyorsanız, ayrıntılı bilgileri okumayıp doğrudan problemlere geçebilirsiniz:

Kesirli sayı türleri çok daha kullanışlıdırlar ama onların da mutlaka bilinmesi gereken özellikleri vardır. Kırpılma konusunda çok iyidirler, çünkü zaten özellikle virgülden sonrası için tasarlanmışlardır. Belirli sayıda bitle sınırlı oldukları için taşma bu türlerde de vardır ancak alabildikleri değer aralığı tamsayılarla karşılaştırıldığında olağanüstü geniştir. Ek olarak, tamsayı türlerinin taşma durumunda sessiz kalmalarının aksine, kesirli sayılar "sonsuzluk" değerini alırlar.

Önce kesirli sayı türlerini hatırlayalım:

Tür Bit Uzunluğu İlk Değeri
float 32 float.nan
double 64 double.nan
real en az 64, veya donanım sağlıyorsa
daha fazla (örneğin, 80)
real.nan
Kesirli tür nitelikleri

Kesirli türlerin nitelikleri tamsayılardan daha fazladır:

Kesirli sayı türlerinin diğer nitelikleri daha az kullanılır. Bütün nitelikleri dlang.org'da Properties for Floating Point Types başlığı altında bulabilirsiniz.

Yukarıdaki nitelikleri birbirleriyle olan ilişkilerini görmek için bir sayı çizgisine şöyle yerleştirebiliriz:

   +     +─────────+─────────+   ...   +   ...   +─────────+─────────+     +
   │   -max       -1         │         0         │         1        max    │
   │                         │                   │                         │
-infinity               -min_normal          min_normal               infinity

İki özel sonsuzluk değeri hariç, yukarıdaki çizginin ölçeği doğrudur: min_normal ile 1 arasında ne kadar değer ifade edilebiliyorsa, 1 ile max arasında da aynı sayıda değer ifade edilir. Bu da min_normal ile 1 arasındaki değerlerin son derece yüksek doğrulukta oldukları anlamına gelir. (Aynı durum eksi taraf için de geçerlidir.)

.nan

Açıkça ilk değeri verilmeyen kesirli sayıların ilk değerlerinin .nan olduğunu gördük. .nan değeri bazı anlamsız işlemler sonucunda da ortaya çıkabilir. Örneğin şu programdaki ifadelerin hepsi .nan sonucunu verir:

import std.stdio;

void main() {
    double sıfır = 0;
    double sonsuz = double.infinity;

    writeln("nan kullanan her işlem: ", double.nan + 1);
    writeln("sıfır bölü sıfır      : ", sıfır / sıfır);
    writeln("sıfır kere sonsuz     : ", sıfır * sonsuz);
    writeln("sonsuz bölü sonsuz    : ", sonsuz / sonsuz);
    writeln("sonsuz eksi sonsuz    : ", sonsuz - sonsuz);
}

.nan'ın tek yararı bir değişkenin ilklenmemiş olduğunu göstermek değildir. İşlem sonuçlarında oluşan .nan değerleri sonraki hesaplar sırasında da korunurlar ve böylece hesap hatalarının erkenden ve kolayca yakalanmalarına yardım ederler.

Kesirli sayıların yazımları

Bu üç türün niteliklerine bakmadan önce kesirli sayıların nasıl yazıldıklarını görelim. Kesirli sayıları 123 gibi tamsayı şeklinde veya 12.3 gibi noktalı olarak yazabiliriz.

Ek olarak, 1.23e+4 gibi bir yazımdaki e+, "çarpı 10 üzeri" anlamına gelir. Yani bu örnek 1.23x104'tür, bir başka deyişle "1.23 çarpı 10000"dir ve ifadenin değeri 12300'dür.

Eğer e'den sonra gelen değer eksi ise, yani örneğin 5.67e-3 gibi yazılmışsa, o zaman "10 üzeri o kadar değere bölünecek" demektir. Yani bu örnek 5.67/103'tür, bir başka deyişle "5.67 bölü 1000"dir ve ifadenin değeri 0.00567'dir.

Kesirli sayıların bu gösterimlerini türlerin niteliklerini yazdıran aşağıdaki programın çıktısında göreceksiniz:

import std.stdio;

void main() {
    writeln("Tür ismi                 : ", float.stringof);
    writeln("Duyarlık                 : ", float.dig);
    writeln("En küçük normalize değeri: ", float.min_normal);
    writeln("En küçük değeri          : ", -float.max);
    writeln("En büyük değeri          : ", float.max);
    writeln();

    writeln("Tür ismi                 : ", double.stringof);
    writeln("Duyarlık                 : ", double.dig);
    writeln("En küçük normalize değeri: ", double.min_normal);
    writeln("En küçük değeri          : ", -double.max);
    writeln("En büyük değeri          : ", double.max);
    writeln();

    writeln("Tür ismi                 : ", real.stringof);
    writeln("Duyarlık                 : ", real.dig);
    writeln("En küçük normalize değeri: ", real.min_normal);
    writeln("En küçük değeri          : ", -real.max);
    writeln("En büyük değeri          : ", real.max);
}

Programın çıktısı benim ortamımda aşağıdaki gibi oluyor. real türü donanıma bağlı olduğu için bu çıktı sizin ortamınızda farklı olabilir:

Tür ismi                 : float
Duyarlık                 : 6
En küçük normalize değeri: 1.17549e-38
En küçük değeri          : -3.40282e+38
En büyük değeri          : 3.40282e+38

Tür ismi                 : double
Duyarlık                 : 15
En küçük normalize değeri: 2.22507e-308
En küçük değeri          : -1.79769e+308
En büyük değeri          : 1.79769e+308

Tür ismi                 : real
Duyarlık                 : 18
En küçük normalize değeri: 3.3621e-4932
En küçük değeri          : -1.18973e+4932
En büyük değeri          : 1.18973e+4932

Not: double ve real float'tan daha fazla duyralığa sahip oldukları halde, writeln bütün kesirli sayı türlerini 6 basamak duyarlıkla yazdırır. (Duyarlık kavramını aşağıda göreceğiz.)

Gözlemler

ulong türünün tutabileceği en yüksek değerin ne kadar çok basamağı olduğunu hatırlıyor musunuz: 18,446,744,073,709,551,616 sayısı 20 basamaktan oluşur. Buna karşın, en küçük kesirli sayı türü olan float'un bile tutabileceği en yüksek değer 1038 mertebesindedir. Yani şunun gibi bir değer: 340,282,000,000,000,000,000,000,000,000,000,000,000. real'in en büyük değeri ise 104932 mertebesindedir (4900'den fazla basamağı olan bir sayı).

Başka bir gözlem olarak double'ın 15 duyarlıkla ifade edebileceği en düşük değere bakalım:

    0.000...(burada 300 tane daha 0 var)...0000222507385850720
Taşma gözardı edilmez

Ne kadar büyük değerler tutuyor olsalar da kesirli sayılarda da taşma olabilir. Kesirli sayı türlerinin iyi tarafı, taşma oluştuğunda tamsayılardaki taşmanın tersine bundan haberimizin olabilmesidir: taşan sayının değeri "artı sonsuz" için .infinity, "eksi sonsuz" için -.infinity haline gelir. Bunu görmek için şu programda .max'ın değerini %10 arttırmaya çalışalım. Sayı zaten en büyük değerinde olduğu için, %10 arttırınca taşacak ve yarıya bölünse bile değeri "sonsuz" olacaktır:

import std.stdio;

void main() {
    real sayı = real.max;

    writeln("Önce: ", sayı);

    // 1.1 ile çarpmak, %110 haline getirmektir:
    sayı *= 1.1;
    writeln("%10 arttırınca: ", sayı);

    // İkiye bölerek küçültmeye çalışalım:
    sayı /= 2;
    writeln("Yarıya bölünce: ", sayı);
}

O programda sayı bir kere real.infinity değerini alınca yarıya bölünse bile sonsuz değerinde kalır:

Önce: 1.18973e+4932
%10 arttırınca: inf
Yarıya bölünce: inf
Duyarlık (Hassasiyet)

Duyarlık, yine günlük hayatta çok karşılaştığımız ama fazla sözünü etmediğimiz bir kavramdır. Duyarlık, bir değeri belirtirken kullandığımız basamak sayısıdır. Örneğin 100 liranın üçte birinin 33 lira olduğunu söylersek, duyarlık 2 basamaktır. Çünkü 33 değeri sadece iki basamaktan ibarettir. Daha hassas değerler gereken bir durumda 33.33 dersek, bu sefer dört basamak kullanmış olduğumuz için duyarlık 4 basamaktır.

Kesirli sayı türlerinin bit olarak uzunlukları yalnızca alabilecekleri en yüksek değerleri değil; değerlerin duyarlıklarını da etkiler. Bit olarak uzunlukları ne kadar fazlaysa, duyarlıkları da o kadar fazladır.

Bölmede kırpılma yoktur

Önceki bölümde gördüğümüz gibi, tamsayı bölme işlemlerinde sonucun virgülden sonrası kaybedilir:

    int birinci = 3;
    int ikinci = 2;
    writeln(birinci / ikinci);

Çıktısı:

1

Kesirli sayı türlerinde ise virgülden sonrasını kaybetmek anlamında kırpılma yoktur:

    double birinci = 3;
    double ikinci = 2;
    writeln(birinci / ikinci);

Çıktısı:

1.5

Virgülden sonraki bölümün doğruluğu kullanılan türün duyarlığına bağlıdır: real en yüksek duyarlıklı, float da en düşük duyarlıklı kesirli sayı türleridir.

Hangi durumda hangi tür

Özel bir neden yoksa her zaman için double türünü kullanabilirsiniz. float'un duyarlığı düşüktür ama küçük olmasının yarar sağlayacağı nadir programlardan birisini yazıyorsanız düşünerek ve ölçerek karar verebilirsiniz. Öte yandan, real'in duyarlığı bazı ortamlarda double'dan daha yüksek olduğundan yüksek duyarlığın önemli olduğu hesaplarda real türünü kullanmak isteyebilirsiniz.

Her değeri ifade etmek olanaksızdır

Her değerin ifade edilememesi kavramını önce günlük hayatımızda göstermek istiyorum. Kullandığımız onlu sayı sisteminde virgülden önceki basamaklar birler, onlar, yüzler, vs. basamaklarıdır; virgülden sonrakiler de onda birler, yüzde birler, binde birler, vs.

Eğer ifade etmek istediğimiz değer bu basamakların bir karışımı ise, değeri tam olarak ifade edebiliriz. Örneğin 0.23 değeri 2 adet onda bir değerinden ve 3 adet yüzde bir değerinden oluştuğu için tam olarak ifade edilebilir. Öte yandan, 1/3 değerini onlu sistemimizde tam olarak ifade edemeyiz çünkü virgülden sonra ne kadar uzatırsak uzatalım yeterli olmaz: 0.33333...

Benzer durum kesirli sayılarda da vardır. Türlerin bit sayıları sınırlı olduğu için, her değer tam olarak ifade edilemez.

Bilgisayarlarda kullanılan ikili sayı sistemlerinin bir farkı, virgülden öncesinin birler, ikiler, dörtler, vs. diye; virgülden sonrasının da yarımlar, dörtte birler, sekizde birler, vs. diye gitmesidir. Eğer değer bunların bir karışımı ise tam olarak ifade edilebilir; değilse edilemez.

Bilgisayarlarda tam olarak ifade edilemeyen bir değer 0.1'dir (10 kuruş gibi). Onlu sistemde tam olarak 0.1 şeklinde ifade edilebilen bu değer, ikili sistemde 0.0001100110011... diye tekrarlar ve kullanılan kesirli sayı türünün duyarlığına bağlı olarak belirli bir yerden sonra hatalıdır. (Tekrarladığını söylediğim o son sayıyı ikili sistemde yazdım, onlu değil...)

Bunu gösteren aşağıdaki örneği ilginç bulabilirsiniz. Bir değişkenin değerini bir döngü içinde her seferinde 0.001 arttıralım. Döngünün 1000 kere tekrarlanmasının ardından sonucun 1 olmasını bekleriz. Oysa öyle çıkmaz:

import std.stdio;

void main() {
    float sonuç = 0;

    // Bin kere 0.001 değerini ekliyoruz:
    int sayaç = 1;
    while (sayaç <= 1000) {
        sonuç += 0.001;
        ++sayaç;
    }

    if (sonuç == 1) {
        writeln("Beklendiği gibi 1");

    } else {
        writeln("FARKLI: ", sonuç);
    }
}

0.001 tam olarak ifade edilemeyen bir değer olduğundan, bu değerdeki hata miktarı sonucu döngünün her tekrarında etkilemektedir:

FARKLI: 0.999991

Not: Yukarıdaki sayaç, döngü sayacı olarak kullanılmaktadır. Bu amaçla açıkça değişken tanımlamak aslında önerilmez. Onun yerine, daha ilerideki bir bölümde göreceğimiz foreach döngüsü kullanılabilir.

Sırasızlık

Daha önce tamsayılarda göndüğümüz karşılaştırma işleçleri kesirli sayılarda da kullanılır. Ancak, kesirli sayılarda geçersiz değeri gösteren .nan da bulunduğundan, o değerin başka değerlerle küçük büyük olarak karşılaştırılması anlamsızdır. Örneğin, .nan'ın mı yoksa 1'in mi daha büyük olduğu gibi bir soru yanıtlanamaz.

Bu yüzden kesirli sayılarda başka bir karşılaştırma kavramı daha vardır: sırasızlık. Sırasızlık, değerlerden en az birisinin .nan olması demektir.

Aşağıdaki tablo kesirli sayı karşılaştırma işleçlerini gösteriyor. İşleçlerin hepsi ikilidir ve örneğin soldaki == sağdaki şeklinde kullanılır. false ve true içeren sütunlar, işleçlerin hangi durumda ne sonuç verdiğini gösterir.

Sonuncu sütun, ifadelerden birisinin .nan olması durumunda o işlecin kullanımının anlamlı olup olmadığını gösterir. Örneğin 1.2 < real.nan ifadesinin sonucu false çıksa bile, ifadelerden birisi real.nan olduğu için bu sonucun bir anlamı yoktur çünkü bunun tersi olan real.nan < 1.2 ifadesi de false verir.


İşleç

Anlamı
Soldaki
Büyükse
Soldaki
Küçükse
İkisi
Eşitse
En Az Birisi
.nan ise
.nan ile
Anlamlı
==eşittir falsefalsetruefalseevet
!=eşit değildir truetruefalsetrueevet
>büyüktür truefalsefalsefalsehayır
>=büyüktür veya eşittir truefalsetruefalsehayır
<küçüktür falsetruefalsefalsehayır
<=küçüktür veya eşittir falsetruetruefalsehayır

Her ne kadar .nan ile kullanımı anlamlı olsa da, değerlerden birisi .nan olduğunda == işleci her zaman için false üretir. Her iki değer de .nan olduğunda bile sonuç false çıkar:

import std.stdio;

void main() {
    if (double.nan == double.nan) {
        writeln("eşitler");

    } else {
        writeln("eşit değiller");
    }
}

double.nan'ın kendisine eşit olacağı beklenebilir, ancak karşılaştırmanın sonucu yine de false'tur:

eşit değiller
.nan eşitlik karşılaştırması için isNaN()

Yukarıda gördüğümüz gibi, bir kesirli sayı değişkeninin .nan'a eşit olup olmadığı == işleci ile karşılaştırılamaz:

    if (kesirli == double.nan) {    // ← YANLIŞ KARŞILAŞTIRMA
        // ...
    }

O yüzden std.math modülündeki "nan değerinde mi?" sorusunun yanıtını veren isNaN() işlevinden yararlanmak gerekir:

import std.math;
// ...
    if (isNaN(kesirli)) {    // ← doğru karşılaştırma
        // ...
    }

Benzer biçimde, .nan'a eşit olmadığı da != ile değil, !isNaN() ile denetlenmelidir.

Problemler
  1. Yukarıda bin kere 0.001 ekleyen programı float yerine double (veya real) kullanacak biçimde değiştirin:
        double sonuç = 0;
    

    Bu problem, kesirli sayı türlerinin eşitlik karşılaştırmalarında kullanılmasının ne kadar yanıltıcı olabildiğini göstermektedir.

  2. Önceki bölümdeki hesap makinesini kesirli bir tür kullanacak şekilde değiştirin. Böylece hesap makineniz çok daha doğru sonuçlar verecektir. Denerken değerleri girmek için 1000, 1.23, veya 1.23e4 şeklinde yazabilirsiniz.
  3. Girişten 5 tane kesirli sayı alan bir program yazın. Bu sayıların önce iki katlarını yazsın, sonra da beşe bölümlerini. Bu problemi bir sonra anlatılacak olan dizilere hazırlık olarak soruyorum. Eğer bu programı şimdiye kadar öğrendiklerinizle yazarsanız, dizileri anlamanız daha kolay olacak.