C’de Tür Dönüşümleri

Şu ana kadar değişkenlerin ve sabitlerin nasıl tanımlanacağını ve onlara nasıl değer atayacağımızı gördük. Ayrıca belli türden değişken, sabit ve ifadeleri ekrana nasıl yazacağımızı ve kullanıcıdan belli türde verileri nasıl alacağımızı gördük. Şimdiye kadar genellikle aynı türden olan ifadeleri birbirlerine atamaya dikkat ettik. Ekrana çıktı verme veya kullanıcıdan girdi alma fonksiyonlarını kullanırken de format belirteçlerini uygun türde vermeye dikkat ettik ve kullanıcıdan uygun türde veriler aldığımızı varsaydık.

Peki ya işler her zaman istediğimiz gibi gitmezse ne olacak? Yani float türünden bir değişkene int türünden bir değer atandığında veya "%d" ile ekrana double türünden bir değer basılmak istendiğinde C derleyicileri bu durumda nasıl bir davranış sergilerler?

C dilinde belli bir türdeki ifadeyi başka bir türe dönüştürmeye tür dönüşümü (type conversion) adı verilir. Eğer derleyici bu işlemi kapalı bir şekilde otomatik yapıyorsa buna kapalı tür dönüşümü (implicit type conversion) adı verilirken, programcının bunu kendi eliyle yapması açık tür dönüşümü (explicit type conversion) olarak adlandırılır. Bunların dışında Türkçe karşılığı yine "tür dönüşümü" anlamına gelen "type casting" kavramı vardır. Bu kavram da bir nevi açık tür dönüşümünü ifade etmektedir. Biz burada tür dönüşümlerini açık ve kapalı tür dönüşmü olarak isimlendireceğiz. “type casting” ve “type conversion” arasındaki farklar için internette daha fazla araştırma yapabilirsiniz.

Kapalı Tür Dönüşümleri

Öncelikle kapalı tür dönüşümlerine ve derleyicinin derleme sırasında hangi durumlarda kapalı tür dönüşümü uyguladığına bakalım. Kapalı tür dönüşümleri, siz hiç bir şey belirtmeden bir türü farklı bir türe atadığınızda gerçekleşir. Eğer birbirlerine dönüşebilen iki türü birbirine atadıysanız bir sorun çıkmayacaktır. Ama derleyicinin dönüştürmesi mümkün olmayan iki türü birbirine atadığınızda derleyici hata verecektir.

Peki biz neye göre dönüşüp dönüşmeyeceğini anlayacağız? C standartlarında türlerle ilgili tam olarak boyut bilgisi verilmese de tam sayı ve kayan noktalı sayı türleri arasındaki ilişki şu şkeilde belirtilmektedir:

long long ≥ long ≥ int ≥ short ≥ signed char
long double > double > float

Elbette her makinede bu türlerin bayt türünden büyüklükleri değişebilir. Genellikle bir türden kendisi ile aynı büyüklükte veya daha büyük olan başka bir türe veri atandığında bir problem çıkmaz. Ancak büyük veri tipinden küçük veri tipine atama olduğunda problemler çıkabilir. Örneğin şöyle bir atamayı düşünelim:

unsigned long sayi1 = 999999999999999995;
unsigned int sayi2 = sayi1;

Burada unsigned long türününün bilgisayarda 8 bayt yer kapladığını, unsigned int türünün ise 4 bayt yer kapladığını varsayalım. Siz de kendi bilgisayarınızda bu türlerin ne kadar yer kapladığını öğrenmek için sizeof operatöründen yararlanabilirsiniz. Buradaki sayi1 değişkenini ekrana yazdırdığımızda ona atadığımız değerin ekrana basıldığını görürsünüz. Fakat sayi2 değişkenini ekrana yazdırdığımızda "2.808.348.667" gibi bir sayı görürsünüz. Bu da ne demek oluyor?

Şimdi size arkaplanda ne döndüğünü açıklayayım. Hesaplamalar için işletim sisteminizde bulunan hesap makinesini "Programlama" modunda kullanabilirsiniz. "999.999.999.999.999.995" sayısının 64 bitlik bir alanda değeri şudur:

0000 1101 1110 0000 1011 0110 1011 0011 1010 0111 0110 0011 1111 1111 1111 1011

Daha rahat görebilmeniz için 4’lü gruplar halinde yazdım. En soldaki değer en yüksek anlamlı bit iken, en sağdaki değer en düşük anlamlı bittir. Değerlerin belleğe yerleşme biçimini Little Endian olarak varsayarak, bunları önce 8 bitlik gruplar halinde ve her birinin kaçıncı bayta denk geldiğini belirterek yazalım:

(1111 1011) → 1. Bayt
(1111 1111) → 2. Bayt
(0110 0011) → 3. Bayt
(1010 0111) → 4. Bayt
(1011 0011) → 5. Bayt
(1011 0110) → 6. Bayt
(1110 0000) → 7. Bayt
(0000 1101) → 8. Bayt

Siz bunun gibi bir şeyi 4 baytlık alana atamak istediğinizde, derleyici son 4 baytı silip ilk 4 baytı belleğe yerleştirecektir. Bakalım elimizde ne kalıyor:

(1111 1011) → 1. Bayt
(1111 1111) → 2. Bayt
(0110 0011) → 3. Bayt
(1010 0111) → 4. Bayt

Şimdi de bunları doğru bir sırayla yazalım ve onluk sistemde hangi sayıya denk geldiğini bulalım:

(1010 0111 0110 0011 1111 1111 1111 1011)2 = (2.808.348.667)10

Taban aritmetiği ile veya hesap makinesi ile sonucun aynen bu şekilde çıktığını görebilirsiniz. İşte C dilinde tür dönüştürme mekanizması bu şekilde çalışır. Daha büyük bir veri türünü daha küçük bir veri türüne eşitlediğinizde, artık kısım silinir ve o şekilde eşitleme yapılır.

Yukarıdaki atama kapalı tür dönüşümüne örnekti. Burada biz özellikle "şu türe dönüştür" diye bir şey söylemedik. Derleyici bunu gördüğünde otomatik olarak yukarıda yaptığımız işlemleri yapmaktadır. Burada gördüğümüz kapalı tür dönüşümü atama operatörü ile yapılmıştır. Bir de artimetik operatörler ile yapılan kapalı tür dönüşümleri vardır. Örneğin "+" operatörünün sağ ile sol tarafındaki operandların türleri birbirine eşit değilse, derleyici küçük olan türü büyük olan türe dönüştürecektir. Küçük bir örnek:

int a = 4;
long b = 999999999999999995;
printf("Sonuc: %ld\n", (a+b));
ÇIKTI
Sonuc: 999999999999999999

Gördüğünüz gibi ifadenin sonucu yine long türünde çıktı. Burada derleyici "+" operatörünün sağ tarafındaki ifade ile sol tarafındaki ifadenin aynı türde olmadığını görüp küçük olan türü büyük olan türe dönüştürdü. Küçük olan tür büyük olana dönüştürülünce değer korunmuş olur.

Bu duruma örnek gösterecek olursak 4 baytlık işaretli int türü, 8 baytlık işaretli long türüne dönüştürülmek istendiğinde, int türü eğer negatifse başına 32 adet "1" koyularak long türüne uydurulur. Eğer pozitifse başına 32 tane "0" koyularak long türüne uydurulur. Buradaki olay Two’s Complement (İkiye Tümleme) konusuna dayanmaktadır. Ancak elbette bütün dönüşümler bu şekilde basit mekanizmalar ile çalışmaz.

Özetleyecek olursak derleyicinin bizim isteiğimiz dışında otomatik olarak gerçekleştirdiği tür dönüşümü işlemine kapalı tür dönüşümü adı verilir. C’de çeşitli durumlarda kapalı tür dönüşümleri yapılmaktadır. Bunların 4 tanesini açıklayalım:

  • Aritmetik veya mantıksal (ileriki bölümlerde göreceğiz) operatörlerin sağ ve sol tarafında yer alan operandlar aynı türden değilse, otomatik tür dönüşümü uygulanır ve küçük tür büyük türe dönüştürülür.
  • Atama operatörünün sağ tarafındaki değer sol taraf ile aynı değilse, otomatik tür dönüşümü uygulanır ve sağ taraftaki değer sol taraftakine dönüştürülmeye çalışılır.
  • Fonksiyon çağrılarında yazılan argümanın türü, fonksiyonun parametresinin türü ile aynı değilse otomatik tür dönüşümü uygulanır ve argümanın türü parametrenin türüne dönüştürülmeye çalışılır.
  • Fonksiyonun geri dönüş türü ile return deyimine yazılan ifadenin türü aynı değilse burada da bir otomatik tür dönüşümü mevcuttur.

Bu durumların ilk ikisi ile ilgili örnek yaptım. Son ikisini ise fonksiyonlar konusunu işlerken anlatacağım.

Açık Tür Dönüşümleri

Şimdi de açık tür dönüşümlerinden bahsedelim. Bu tip dönüşümlerde programcı kendi isteği ile tür dönüşümünü uygulamaya çalışır. Öncelikle tür dönüştürme operatöründen (type casting operator) bahsedelim. Bu operatör parantezlerin için tür bilgisi yazılarak kullanılır. Genel kullanımı şöyledir:

(<tür>) <ifade>

Şimdi örnek üzerinden gidelim:

int a = 5;
int b = 6;
double z = (double)a + b;

Burada gördüğünüz (double) opeartörü bir tür dönüştürme operatörüdür. Buradaki "<tür>" yerine temel veri türlerinden veya ileride göreceğimiz kullanıcı tanımlı veri türlerinden biri yazılabilir. Bu operatör ile derleyici tür dönüşümüne zorlanır.

Yukarıdaki örnekte önce int türünden olan a değişkeni double türüne dönüştürülür. Daha sonra "+" operatörünün iki yanı aynı tür olmadığı için ve double türü int türünden büyük olduğu için toplam sonucu double türüne dönüştürülür. Burada hem kapalı hem de açık tür dönüşümünü görmektesiniz. Eğer atama şu şekilde gerçekleşseydi:

z = (double)(a + b);

Bu durumda "a+b" ifadesinden çıkan int türündeki değer double türüne dönüştürülecekti. Burada parantez kullanımının farkını görüyoruz.

Tür dönüştürme operatörünün temel veri türlerinde kullanımı genellikle okunabilirliği arttırmak içindir. Çünkü otomatik tür dönüşümü sırasında derleyici genellikle atanan veya işlem gören iki türün birbirine eşit olmadığı uyarısını size vermektedir. Yani bu atamayı yanlışlıkla yaptığınızı varsayar. Tür dönüştürme operatörü ile bu uyarılardan kurtulabilirsiniz. Yine bu operatörün sık kullanıldığı alan iki tamsayının birbiriyle bölümünde gerçekleşen kesinti (truncation) hatalarını ortadan kaldırmaktır. Örneğin:

int sayi1 = 10;
double sonuc = sayi1 / 3;

Böyle bir işlemde bu hataya rastlamaktasınız. Bu durumda sonuc değişkeninin değeri "3.0" olur. Bunu önlemek için şu şekilde atama yapabilirsiniz:

int sayi1 = 10;
double sonuc = (double) sayi1 / 3;

Burada önce sayi1 değişkeni double türüne çevrilecek, daha sonra bölme işleminde kapalı tür dönüşümü ile int türündeki "3" sabiti double türüne dönüştürülecek ve bölme işlemi sorunsuz bir şekilde yapılacaktır. Peki şöyle olsaydı:

int sayi1 = 10;
double sonuc = (double) (sayi1 / 3);

Burada parantez ve önceliklerin ne kadar önemli bir konu olduğunu görüyoruz. Burada sayi1 değişkeni ve "3" sabiti zaten int türünden olduğundan dönüştürme yapılmadan bölme işlemi gerçekleştirilecek ve 3 değeri çıkacaktır. Daha sonra da bu değer double türüne çevrilip "3.0" değeri sonuc değişkenine atılacaktır.

Bu gibi durumlarda dikkatli olmalısınız. Ben de birçok çalışmamda bunun gibi tür dönüştürme hataları ile uğraşmıştım. Gerçekten çok baş ağırtıcı ve büyük projelerde bulunması zor bir hal alıyor. Bu nedenle parantez operatörünün doğru kullanımına dikkat edin.

İşaretli ve İşaretsiz Tam Sayılar Arasındaki Dönüşüm

Küçük türlerden büyük türlere olan dönüşümde genellikle veri kaybı olmaz. Fakat işaretli tamsayıların işaretsiz tamsayılara dönüştürülmesinde sıkıntılar oluşur. Basit bir işaret kaybı olur gibi düşünebilirsiniz. Fakat durum böyle değildir. Küçük bir örnek verelim:

int sayi1 = -17;
unsigned int sayi2 = sayi1;
printf("%u", sayi2);

Burada işaretsiz sayi2 tam sayısını, yine işaretsiz olarak ekrana basmaya kalktığımızda "4.294.967.279" gibi bir sonuç görürüz. Bu olay yine bitlerin değişiminden kaynaklanır. Two’s Complement (İkiye Tümleyen) mantığından yola çıkarak işaretli tam sayılarda pozitif sayıların "0" ile ve negatif sayıların "1" ile başladığını bildiğinizi varsayıyorum. Fakat işaretsiz tam sayılarda bir sayının "1" veya "0" ile başlaması işareti değiştirmez, yani sayı her zaman pozitiftir. Bu nedenle "1" ile başlayan bir işaretli tam sayı, işaretsize dönüştürüldüğünde en yüksek biti "1" olarak kabul edilecek ve ona göre işlem yapılacaktır.

Yine bir işaretsiz tamsayı işaretli tam sayıya dönüştürüldüğünde de bazı sorunlar çıkar. En büyük anlamlı bitin işaretli tam sayılarda özel anlama geldiğini ve hesaplamaya katılmadığını biliyorsunuz. İşaretsiz tam sayılarda buna dikkat edilmediğinden, eğer bir işaretsiz tam sayıyı işaretli tam sayıya dönüştürmek istersek ve bu sayının en büyük anlamlı biti "1" ise, karşımıza negatif ve bize anlamı gelmeyen bir sayı çıkacaktır.

Tür Dönüşümlerinde Taşma

Her veri türünün bellekte kapladığı bir alan vardır. Bu alanlara göre hangi aralıklarda değer tutabileceklerini önceki bölümlerde söylemiştik. Eğer belli bir veri türünden bir değişkene alanına sığmayacak bir değer verilirse ne olur? Eğer pozitif olarak çok yüksek bir değer verilirse buna taşma (overflow) adı verilir. Eğer negatif olarak çok düşük bir değer verilirse buna da alttan taşma (underflow) adı verilir.

Örneğin 4 baytlık kabul edilen bir int türünden değişken en fazla "2.147.483.647" sayısını alabilir. Biz ona "2.147.483.648" sayısını, yani bir fazlasını verdiğimizde taşma meydana gelir ve bize "-2.147.483.648" yani en düşük değerini geri döndürür. Bu değer yine az önce bahsettiğim bitsel hareketlerden kaynaklanmaktadır. Örneğin 4 baytlık bir alana sahip işaretli bir tam sayıya verilebilecek en büyük ikilik sayı şudur:

0111 1111 1111 1111 1111 1111 1111 1111

İlk bitin işaret biti olduğunu biliyoruz. Öyleyse bu sayıya 1 eklersek ikilik tabanda ne olur? Şuna benzer bir şey olur:

1000 0000 0000 0000 0000 0000 0000 0000

Bu sayı da 4 baytlık bir alanda yazılabilecek en küçük negatif sayıya, yani "-2.147.483.648" sayısına denk gelir. Biz bu değişkene "-2.147.483.649" değerini, yani en küçük sayının 1 eksiğini verdiğimizde ise alttan taşma meydana gelir ve "2.147.483.647" gibi bir sonuç çıkar.

Burada dönen bütün olayları basit taban aritmetiğinden anlayabilirsiniz. Burada ayriyeten taban aritmetiği anlatmadığımı farketmişsinizdir. Bunun nedeni hem programlamaya başlayan birinin bunu bilmesi gerektiği, hem de internette bu konu ile ilgili çok daha güzel kaynaklar bulabileceğinizdendir.

Genel Örnek

Konu ile ilgili tek tek gösterilecek birçok örnek var. Bir veri türünden başka bir veri türüne dönüşümde nasıl bir sonuç ortaya çıkacağını anlattıklarımdan yola çıkarak tahmin edebilirsiniz. Kendiniz denemeler yaparak da sonuçları gözlemleyebilirsiniz. Ben burada bir örnek vererek bölümü sonlandırmayı planlıyorum. Hemen örneğimize geçelim.

#include <stdio.h>

int main()
{
    float fahrenheit, kelvin;

    printf("Lutfen sicakligi Fahrenheit cinsinden girin: ");
    scanf("%f", &fahrenheit);
    float sonuc_celcius = (fahrenheit - 32)/1.8;    
    printf("Girdiginiz sicakligin Celcius karsiligi: %f\n", sonuc_celcius);

    printf("Lutfen sicakligi Kelvin cinsinden girin: ");
    scanf("%f", &kelvin);
    float sonuc_fahrenheit = (kelvin) * (float)9/5 - 459.67;
    printf("Girdiginiz sicakligin Fahrenheit karsiligi: %f\n", sonuc_fahrenheit);

    return 0;
}

Programı çalıştırdığımızda ve belirli girdiler verdiğimizde şöyle bir tablo ile karşı karşıya geliriz:

ÇIKTI
Lutfen sicakligi Fahrenheit cinsinden girin: 33
Girdiginiz sicakligin Celcius karsiligi: 0.555556
Lutfen sicakligi Kelvin cinsinden girin: 17
Girdiginiz sicakligin Fahrenheit karsiligi: -429.070007

Şu ana kadar yazdığımız en işe yarar program bu oldu herhalde… Siz de belli formülleri bu şekilde koda dökerek, kendi formül hesabı yapan programlarınızı yazabilirsiniz. Burada ilk yaptığımız şey kullanıcıdan Fahrenheit biriminde bir sıcaklık değeri almak ve bunu "(F – 32)/1.8" gibi bir formül ile Celcius birimine çevirmektir. sonuc_celcius değişkenini tanımladığımız satırda yer alan formülde float türünden fahrenheit değişkeninden int türündeki "32" sabiti çıkarılıyor. C’de tür dönüşümlerinde tam sayılar, ondalıklı sayılardan her zaman düşük seviyede yer aldığından tam sayılar ondalıklı sayılara çevrilirler. Burada da "32" sayısı ondalıklı sayıya dönüştürülüp işlem yapılır.

Parantez içindeki işlemden float türünde bir sonuç çıkar. Bu sonucu "1.8" sayısına bölmek istediğimizde burada yine bir kapalı tür dönüşümü söz konusudur. Çünkü C dilinde ondalıklı sabit sayıların sonunda bir son ek belirtilmediği sürece double türünde sayıldığını söylemiştik. İşte burada da "1.8" sabit sayısı double türündedir.

Bölmenin sol tarafındaki sonuç ise float türünde çıkmıştı. float türü double türünden daha küçük olduğu için, float türü sorunsuz bir şekilde double türüne dönüştürülür. Daha sonra işlem yapılır ve double türünde sonuç bulunur. Fakat bizim atamak istediğimiz değişken, yani sol taraf değeri float türünden olduğu için, burada yine double türünden float türüne bir dönüşüm söz konusudur. İşte burada büyük türden küçük türe dönüşüm olduğu için bir kayıp yaşanabilir. Şu küçücük formülde bile derleyici tarafından yapılan 3 kapalı tür dönüşümünü böylece görmüş olduk.

Devam eden satırlarda bu sefer kullanıcıdan birimi Kelvin olan bir sıcaklık değeri girilmesi istenmiştir. Kelvin biriminden Fahrenheit birimine dönüşüm için "(9 K)/5 – 459.67" formülü kullanılır. sonuc_fahrenheit* değişkeninin tanımlandığı satırda "9/5" gibi bir işlem "1" gibi bir değerle sonuçlanacağı için bu sayılardan birini el ile ondalıklı sayıya dönüştürüp daha sonra derleyiciye kapalı tür dönüşümü yaptırtmak gerekir.

İşte açık tür dönüşümlerinin kullanımı ve önemi bu örnekten anlaşılmaktadır. Bu dönüşümü yaptıktan sonra "9/5" işlemi bizim istediğimiz sonucu verecek ve Kelvin değeri ile çarpılıp "459.67" değeri sonuçtan çıkarılacaktır.

Böylelikle tür dönüşümlerinin mantığını ve uygulamasını görmüş olduk. Dediğim gibi burada birçok örnek vermek gerekir, fakat ben bunu kendinizin yapması taraftarıyım. Artık bu bölüme kadar temel düzeyde program yazabilecek hale gelmiş olmalısınız. Tek tek kafanıza ne takıldıysa deneyip ekrana basın. Eğer hala bilgisayarınıza bir derleyici ve IDE kurmakta güçlük çekiyorsanız Online derleyicileri deneyebilir ve anında sonuç alabilirsiniz. Ben size şu siteyi öneririm:

https://www.onlinegdb.com/online_c_compiler

4.7 12 votes
Article Rating
Subscribe
Bildir
guest

0 Yorum
Eskiler
En Yeniler Beğenilenler