D.ershane D Programlama Dili
Ali Çehreli

duyarlık: [precision], sayının belirgin hane sayısı
infinity: [infinity], sonsuzluk
kırpılma: [truncate], sayının virgülden sonrasının kaybedilmesi
nan: [nan], "not a number", geçerli bir sayı gösterimi değil
nitelik: [property], 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:

Not: "Türün alabileceği en küçük değer", .min değildir; .max'ın eksi işaretlisidir: örneğin -double.max.

Diğer niteliklere bu aşamada gerek olduğunu düşünmüyorum; bütün nitelikleri 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

Yukarıdaki kesikli çizginin ölçeğinin doğru olduğunu vurgulamak istiyorum: 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);
}
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 şu 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 büyük değeri          : ", float.max);
    writeln("En küçü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 büyük değeri          : ", double.max);
    writeln("En küçü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 büyük değeri          : ", real.max);
    writeln("En küçü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 büyük değeri          : 3.40282e+38
En küçük değeri          : -3.40282e+38

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

Tür ismi                 : real
Duyarlık                 : 18
En küçük normalize değeri: 3.3621e-4932
En büyük değeri          : 1.18973e+4932
En küçük değeri          : -1.18973e+4932
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 mertebesinde. Yani 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...0000222507. (Daha doğru olarak, son bölümü şöyledir: ...222507385850720.)

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ığı çok 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 duyarlıklı olduğu için 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;

    // Bu döngünün 1000 kere tekrarlandıktan sonra 1 değerine
    // ulaşacağını düşünürüz:
    while (sonuç < 1) {
        sonuç += 0.001;
    }

    // Bakalım doğru mu...
    if (sonuç == 1) {
        writeln("Beklendiği gibi 1");

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

Bunun nedeni; 0.001 değerinin de tam olarak ifade edilemeyen bir değer olması ve bu değerdeki hata miktarının sonucu her toplamda etkilemesidir. Sonuçtan da anlaşılacağı gibi döngü 1001 kere tekrarlanmıştır.

Kesirli sayı karşılaştırmaları

Tamsayılarda şu karşılaştırma işleçlerini kullanıyorduk: eşitlik (==), eşit olmama (!=), küçüklük (<), büyüklük (>), küçük veya eşit olma (<=), büyük veya eşit olma (>=). Kesirli sayılarda başka karşılaştırma işleçleri de vardır.

Kesirli sayılarda geçersiz değeri gösteren .nan da bulunduğu için, onun diğer 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
!<>=küçük, büyük, eşit değildir falsefalsefalsetrueevet
<>küçüktür veya büyüktür truetruefalsefalsehayır
<>=küçüktür, büyüktür, veya eşittir truetruetruefalsehayır
!<=küçük değildir ve eşit değildir truefalsefalsetrueevet
!<küçük değildir truefalsetruetrueevet
!>=büyük değildir ve eşit değildir falsetruefalsetrueevet
!>büyük değildir falsetruetruetrueevet
!<>küçük değildir ve büyük değildir falsefalsetruetrueevet

Dikkat ederseniz; ! karakteri içeren, yani anlamında "değildir" sözü bulunan bütün işleçlerin .nan ile kullanılması anlamlıdır ve sonuç hep true'dur. .nan'ın geçerli bir değeri göstermiyor olması, çoğu karşılaştırmaya "değil" sonucunu verdirir.

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

Bir kesirli sayı değişkeninin .nan'a eşit olup olmadığı bile == işleci ile karşılaştırılamaz çünkü yukarıdaki tabloya göre sonuç hep false çıkar:

    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. Ö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.
  2. 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.

... çözümler