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:
- Bin kere 0.001 eklemek 1 eklemekle aynı şey değildir
==
veya!=
mantıksal ifadelerini kesirli sayı türleriyle kullanmak çoğu durumda hatalıdır- Kesirli sayıların ilk değerleri 0 değil,
.nan
'dır..nan
değeriyle işlem yapmak anlamlı değildir; başka bir değerle karşılaştırıldığında.nan
ne küçüktür ne de büyük. - Artı yöndeki taşma değeri
.infinity
, eksi yöndeki taşma değeri de eksi.infinity
'dir
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:
.stringof
türün okunaklı ismidir.sizeof
türün bayt olarak uzunluğudur; türün kaç bitten oluştuğunu hesaplamak için bu değeri bir bayttaki bit sayısı olan 8 ile çarpmak gerekir-
.max
"en çok" anlamına gelen "maximum"un kısaltmasıdır; türün alabileceği en büyük değerdir. Kesirli türlerde.min
bulunmaz; türün alabileceği en düşük değer için.max
'ın eksi işaretlisi kullanılır. Örneğin,double
türünün alabileceği en düşük değer-double.max
'tır. -
.min_normal
, türün normal duyarlığı ile ifade edebildiği sıfıra en yakın değerdir. (Duyarlık kavramını aşağıda göreceğiz.) Tür aslında daha küçük değerler de ifade edebilir ama o değerlerin duyarlığı türün normal duyarlığının altındadır ve hesaplanmaları daha yavaştır. Bir kesirli sayı değerinin eksi.min_normal
ile artı.min_normal
aralığında olması durumuna (0 hariç) alttan taşma denir. -
.dig
"basamak sayısı" anlamına gelen "digits"in kısaltmasıdır; türün kaç basamak duyarlığı olduğunu belirtir -
.infinity
"sonsuz" anlamına gelir; taşma durumunda kullanılan değerdir.
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 | false | false | true | false | evet |
!= | eşit değildir | true | true | false | true | evet |
> | büyüktür | true | false | false | false | hayır |
>= | büyüktür veya eşittir | true | false | true | false | hayır |
< | küçüktür | false | true | false | false | hayır |
<= | küçüktür veya eşittir | false | true | true | false | hayı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
- Yukarıda bin kere 0.001 ekleyen programı
float
yerinedouble
(veyareal
) 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.
- Ö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.
- 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.