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 duruma nasıl bir davranış sergileyecekler?

Bir programlama dilini geliştirmekle uğraşan kişiler genellikle tanımsız bir davranışın olmamasına dikkat eder ve dili buna göre oluştururlar. Bizim bile oturup kolayca düşünebildiğimiz bu durumu elbette onlar da düşünmüş ve bir çözüm üretmişlerdir. C dilinde belli bir türdeki ifadeyi (buna değişkenler, sabitler ve bunların operatörlerle birleşimi de dahil) 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çesi 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.

Ö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? Öncelikle C’deki temel veri türleri arasındaki büyüklük hiyerarşisini gösteren şu figüre bakalım:

Figür 1: C’de Temel Veri Türlerinin Hiyerarşisi

Burada temsili olarak göstersem de her makinede bu türler arasındaki büyüklük ilişkisi bu şekilde olmaz. Büyüklük ilişkileri ile tür dönüşümleri arasında yakın bir ilişki vardır. Çünkü burada eğer büyük veri tipi küçük veri tipine atanırsa bir veri kaybına neden olabilir. Ö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?

Açıkladığımda bu sayının çıkmasının ne kadar mantıklı olduğunu anlayacaksınız. Öncelikle burada negatif sayıları karıştırmamak için işaretsiz tamsayı türlerini ele aldım. Benim bilgisayarımda unsigned long türü 8 bayt, yani 64 bit yer kaplarken; unsigned int türü 4 bayt, yani 32 bit yer kaplamaktadır. Buradaki sonucun bütün nedeni budur. Şimdi size arkaplanda ne döndüğünü açıklayayım. Öncelikle 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. C dili daha önce bahsettiğim Little Endian ile baytları belleğe yerleştirdiğine göre 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 sayi2 = sayi1 ataması kapalı tür dönüşümüne örnekti. Burada biz özellikle 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 ne oluyor? Böylelikle değişkenin değeri korunarak tür dönüşümü yapılmış oluyor. Bu nedenle önce türler arası büyüklük ilişkisini gösterdim. 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” kouylarak long türüne uydurulur. Buradaki olay Two’s Complement (İkiye Tümleme) konusuna dayanmaktadır ki tekrar söylüyorum bu konuyu burada anlatmayacağım.

Ö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ü, fornksiyonun tanımlanan 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. Şimdi de açık tür dönüşümlerinden bahsedeyim. Bu tip dönüşümlerde programcı kendi isteği ile tür dönüşümünü aktif hale getirir. Peki bu nasıl yapılır? Öncelikle tür dönüştürme operatöründen (type casting operator) bahsedelim. Bu operatör bildiğimiz parantez operatörüne benzer. Fakat parantez operatörünün içerisine tür bilgisi yazıldığında ondan ayrılır. Şu şekilde gösterelim:

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

Burada gördüğünüz “(double)” opeartörü bir tür dönüştürme operatörüdür. Yani bu operatör “(tür)” şeklinde tanımlanır. Bu tür temel veri türlerinden veya ileride göreceğimiz kullanıcı tanımlı veri türlerinden biri olabilir. 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. Yani önce kapalı daha sonra açık tür dönüşümü gerçekleştirilecekti. 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ölüm 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.

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", %d);

Burada işretsiz sayi2 tamsayı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 tamsayılarda pozitif sayıların “0” ile ve negatif sayıların “1” ile başladığını bildiğinizi varsayıyorum. Fakat işaretsiz tamsayı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 tamsayı, 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 tamsayıya dönüştürüldüğünde de bazı sorunlar çıkar. En büyük anlamlı bitin işaretli tamsayılarda özel anlama geldiğini ve hesaplamaya katılmadığını biliyorsunuz. İşaretsiz tamsayılarda buna dikkat edilmediğinden, eğer bir işaretsiz tamsayıyı işaretli tamsayı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. Böylelikle aynı boyutlu işaretsiz ve işaretli tamsayıların birbirilerine dönüşümdeki sıkıntıları anladığınızı umuyorum.

Şimdi iki önemli kavramdan bahsedip hemen örneğe geçeceğim. Her veri türünün kapasitesi vardır. Bu kapasitelere 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 overflow (taşma) adı verilir. Eğer negatif olarak çok düşük bir değer verilirse buna da underflow (alttan taşma) 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 overflow 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 tamsayı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? Sanırım şuan 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 underflow 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.

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.

// Code 2.4.1: Code2_4_1.c
// Tür Dönüşümü Örneği (Sıcaklık Birimleri)

#include <stdio.h>

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

	float sonucFahrenheit = (kelvin) * (float)9/5 - 459.67;
	printf("Girdiginiz sicakligin Fahrenheit karsiligi: %f\n", sonucFahrenheit);

	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. 13. 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 tamsayılar, ondalıklı sayılardan her zaman düşük seviyede yer aldığından tamsayı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 butrada yine bir kapalı tür dönüşümü söz konusudur. Çünkü C dilinde ondalıklı sabit sayıların sonunda bir sonek 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 trü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.

16. ve 17. 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. 19. 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üşümlrinin 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

Bu arada yorum satırları ile ilgili bir hatırlatma yapmama izin verin. Ben normalde örneklerde “//” yorum satırını kullanmaktayım. Fakat bu tip yorum satırları C90 satandartlarını destekleyen ve daha üstünü desteklemeyen derleyicilerde sorun yaratır. Eğer bunlardan dolayı sorun yaşıyorsanız yorum satırlarını kaldırmayı veya çoklu yorum satırı olan “/**/” kullanmayı deneyebilirsiniz.

4.6 9 votes
Article Rating
Subscribe
Bildir
guest

0 Yorum
Inline Feedbacks
View all comments