Ders 06 / 13
Veri Tipleri
Bir tipin taşıyabileceği değer aralığının, işaretli-işaretsiz ayrımının ve tam sayı yükseltmesinin derleyicide karara bağlanması; sizeof(char) == 1 dışında hiçbir boyutun basılmaması.
Yayın Güncelleme
Bu derste
- Bir tipin taşıdığı değer aralığının derleyicide karara bağlandığını ölçerek gösterir
- Tam sayı yükseltmesini ve işaretli-işaretsiz dönüşümü örnekle ayırt eder
- İşaretsiz taşmanın tanımlı sarmasıyla işaretli taşmanın tanımsızlığını birbirinden ayırır
İçindekiler
Derleyici Uyarıları ve Standartlar dersi, bir uyarının hangi aşamada doğduğunu ve
standardın neyi garanti edip neyi garanti etmediğini ölçtü: sizeof(char) == 1 bir
garantiydi, öteki boyutlar değildi. Bu ders o ayrımın üstüne kuruluyor ve daha temel bir
soruyu ölçüyor: bir tip, taşıyabileceği değerlerin sınırını hangi aşamada belirliyor?
Yanıt derleyicidir. Bir kaynak dosyada int yazıldığında, o adın taşıyabileceği aralık
ne kaynağın kendisinde ne koşumda; birimin çevrildiği anda, hedef için seçilmiş temsille
birlikte karara bağlanır. Bu ders M08/K05’te ölçülen “bir tipin hangi sınıf yolunda
bulunması gerektiği” sorusuyla da, M09/K05’te ölçülen “bir bilginin kaynağı” sorusuyla da
karışmaz — ikisi de bu kursun dışında kalan eksenlerdi; burada ölçülen, bir tipin
taşıdığı değerin hangi aşamada karara bağlandığı ve o kararın sınırlarının nereden
geldiği. Programlama Temelleri kursu (M01/K02) değişken ve tip kavramını Python’la zaten
kurmuştu; orada bir adın hangi değerleri tutabileceği koşum sırasında değişebilirdi, bir
tipin sınırı da ayrıca sorulmazdı. C’de bu iki soru ayrılıyor: tipin kendisi bir sınır
taşıyor ve o sınır kaynakta yazılan tip adıyla birlikte derlemede sabitleniyor. Bir
değişken bildirildikten sonra o değişkenin taşıdığı tip, programın geri kalanı boyunca
değişmez; değer değişebilir, ama değerin hangi aralıkta ve hangi kuralla yorumlanacağını
belirleyen tip sabit kalır. Bu ders o sabitliğin ne anlama geldiğini, dört ayrı kararla —
aralık, yükseltme, işaret ve açık dönüşüm — ölçüyor.
Bu sınırın büyüklüğü hedefe göre değişir ve bu değişkenlik kasıtlıdır: dil, aynı tip adının farklı donanımlarda farklı genişliklerle temsil edilmesine izin vererek her hedefte verimli bir eşleşme kurulmasını sağlar. Ama bu esneklik, bir dersin ölçümlerini o hedefe bağımlı kılmamalı — bir ölçüm burada koşulduğu makinede doğruysa, beş yıl sonra başka bir hedefte de doğru kalmalı. Bu yüzden bu derste hiçbir gerçek boyut değeri yazdırılmıyor; onun yerine iki tipin göreli ilişkisi ölçülüyor. Bir ilişkinin kendisi de hedeften hedefe değişebilir, ama belirli bir hedef için derleyicinin verdiği karar sabittir ve aynı ikili dosya her koşumda aynı ilişkiyi bildirir.
Boyut Birimi ve Karşılaştırmalı Sonuç
Bir tipin taşıdığı aralığın büyüklüğü hedefe bağlıdır ve bu kursta hiçbir yerde
basılmaz — istisna, standardın açıkça garanti ettiği tek satır: sizeof(char) her zaman
1dir, çünkü “bayt” ölçü birimi zaten char üzerinden tanımlanır. Aşağıdaki ölçüm bu
satırı ve iki tipin göreli büyüklüğünü mantıksal bir sonuçla — büyük mü küçük mü,
1/0 — okuyor; hiçbir gerçek boyut değeri yazdırılmıyor.
// asama.c — tipin tasidigi degerin derleyicide karara baglanmasi
#include <stdio.h>
#include <limits.h>
enum Renk { KIRMIZI = 1, YESIL = 2, MAVI = 4 };
static void gozle(const char *ad, int deger) {
printf("%-34s%d\n", ad, deger);
}
int main(void) {
gozle("tip boyutu birimi", (int)sizeof(char));
gozle("numaralandirma sabiti", (int)MAVI);
gozle("int char tan genis mi", (int)(sizeof(int) > sizeof(char)));
gozle("long int tan genis mi", (int)(sizeof(long) > sizeof(int)));
gozle("short int tan dar mi", (int)(sizeof(short) < sizeof(int)));
char kucuk_a = 100, kucuk_b = 100;
int yukseltilmis_toplam = kucuk_a + kucuk_b;
gozle("yukseltilmis toplam", yukseltilmis_toplam);
unsigned char tasan = UCHAR_MAX;
tasan = (unsigned char)(tasan + 1);
gozle("isaretsiz tasma sonucu", tasan);
int eksi = -1;
unsigned int esik = 1;
gozle("isaretsize donunce kucuk mu", (int)((unsigned int)eksi < esik));
return 0;
}
tip boyutu birimi 1 numaralandirma sabiti 4 int char tan genis mi 1 long int tan genis mi 1 short int tan dar mi 1 yukseltilmis toplam 200 isaretsiz tasma sonucu 0 isaretsize donunce kucuk mu 0
tip boyutu birimi satırı standardın garantisini doğruluyor: char her zaman tek bayttır
ve bu, kursun ölçtüğü tek boyut değeri kalacak. Geri kalan üç karşılaştırma satırı ise
gerçek bir sayı vermiyor, yalnız bir ilişkinin sonucunu veriyor — hedefte int chartan
büyük mü, long intten büyük mü, short intten dar mı diye soruyor ve üçüne de 1
(doğru) yanıtı geliyor. TS1. Bu karşılaştırmaların kendisi de derleyicide karara
bağlanıyor: her ilişki hedeften hedefe değişebilir, ama belirli bir birim çevrilirken
sabittir ve koşum boyunca değişmez — aynı ikili dosya her koşumda aynı 1 ya da 0
değerini üretir.
Standart bu tiplerin göreli sırasını da zaten garanti eder: char, short, int,
long ve long long arasındaki genişlik hiçbir zaman geriye doğru olamaz — her biri
kendisinden öncekinin taşıyabildiği her değeri taşıyabilecek genişliktedir. Bu bir ölçüm
değil, dilin kendi belirtimidir; bu ders yalnız belirli bir hedefte bu sıranın katı
mı yoksa eşit mi gerçekleştiğini ölçüyor (burada üçü de katı çıktı). Bazı hedeflerde
int ve long aynı genişlikte olabilir — standart yalnız longın intten dar
olamayacağını garanti eder, kesinlikle daha geniş olacağını değil. Bu derste
ölçülen 1 değeri bu yüzden bu hedefe özgü bir gözlemdir, dilin kendisinin verdiği bir
söz değil.
Tam Sayı Yükseltmesi
numaralandirma sabiti satırı, Derleme Aşamaları dersinde tanıtılan enum Renk
sabitinin bir kesitidir: MAVI değeri 4, kaynakta yazılan sabit ifadeden derlemede
üretiliyor. Aynı ölçümde yukseltilmis toplam satırı farklı bir kararı gösteriyor:
kucuk_a ve kucuk_b birer char, ama toplamları char sınırında değil int
sınırında hesaplanıyor. C standardı, char ve short gibi dar tipleri bir işleme
girmeden önce inte yükseltir — buna tam sayı yükseltmesi denir. 100 + 100
işlemi bir char üzerinde yürütülseydi (tipik olarak -128–127 aralığında) sonuç
sığmazdı; ama işlem int genişliğinde yürüdüğü için 200 sorunsuz çıkıyor. Bu
yükseltme kaynakta görünmez — hiçbir dönüştürme yazılmadı — ama derleyici her char
işlenenini işlemden önce inte taşıyor ve bu karar tamamen tiplerden geliyor, veriden
değil.
TS2. Yükseltme kuralı, işlenen değerin ne olduğuna bakmaksızın uygulanır: kucuk_a
ve kucuk_b 10 ve 20 olsaydı da toplam yine int genişliğinde hesaplanırdı, sonuç
char sınırları içinde kalsa bile. Derleyici, işlemi yürütmeden önce hangi genişlikte
çalışacağına yalnız işlenenlerin tipine bakarak karar veriyor; bu, işleçlerin veriye
göre değil tipe göre davrandığı ilkenin ilk örneği ve İşleçler dersinde derinleşecek.
Yükseltme yalnız aritmetik işleçlerle sınırlı değildir; bir işlev değişken sayıda
bağımsız değişken aldığında (printf bunun standart kitaplıktaki örneğidir) char ve
short bağımsız değişkenler de çağrı öncesinde inte yükseltilir. Buna varsayılan
bağımsız değişken yükseltmesi denir ve yalnız ... kısmına düşen argümanlarda —
prototipin son bildirilen parametresinden sonrasında — işler. gozle bundan farklıdır:
prototipli ve sabit imzalı bir işlevdir, dolayısıyla tasan değişkeni unsigned char
olsa da değer, ikinci parametre int bildirildiği için sıradan bir atama dönüşümüyle
inte taşınır; gozle gövdesindeki %d de bu bildirilen tiple eşleşir. İki mekanizma
ayrıdır: biri yalnız değişken argümanlı çağrıların kuyruğunda, öbürü her prototipli
çağrıda işler. Bir dar tipi doğrudan uyuşmayan bir biçim belirleyiciyle yazdırmak
(unsigned char için %hhu yerine %d kullanmak gibi) printf tarafında çoğu durumda
varsayılan yükseltme sayesinde çalışır, ama bu kırılgan bir uyumdur ve biçim dizeleri
konusu Girdi/Çıktı dersinde ayrıca ele alınacak.
Açık Dönüşümle Sınırlama
Yükseltme örtük bir kuraldır — kaynakta hiçbir işaret bırakmadan işler. Ama kaynak yazarı tam tersini de isteyebilir: geniş bir değeri kasıtlı olarak dar bir tipe sığdırmak. Bu, açık bir dönüşümle (cast) yapılır ve kararın kendisi yine derlemede, tipin adı yazılırken veriliyor.
// daraltma.c — acik donusum, sinirlamayi kaynakta gorunur kilar
#include <stdio.h>
static void gozle(const char *ad, int deger) {
printf("%-34s%d\n", ad, deger);
}
int main(void) {
int genis_deger = 300;
unsigned char daraltilmis = (unsigned char)genis_deger;
gozle("acikca daraltilan deger", daraltilmis);
int ikinci_deger = 600;
unsigned char ikinci_daraltilmis = (unsigned char)ikinci_deger;
gozle("ikinci acikca daraltilan deger", ikinci_daraltilmis);
return 0;
}
acikca daraltilan deger 44 ikinci acikca daraltilan deger 88
300 değeri sekiz bitlik işaretsiz bir tipe sığmıyor, ama dönüşüm yine de tanımlı: aynı
sarma kuralı burada da geçerli ve 300 mod 256 = 44 çıkıyor. 600 için de aynı kural
işliyor ve 88 veriyor. Bu, bir önceki bölümdeki taşmadan farklı bir olay değil, aynı
kuralın kaynakta açıkça istenerek tetiklenmiş hali. Örtük yükseltme kaynakta görünmezdi;
açık dönüşüm ise (unsigned char) yazımıyla görünür — okuyan kişi, bir daraltmanın
kasıtlı olduğunu kaynaktan anlayabilir. İki durumda da hangi kuralın uygulanacağı, işlenen
değere değil, hedef tipin işaretine bakılarak derlemede belirleniyor.
İşaretli-İşaretsiz Ayrımı ve Tanımlı Sarma
Bir tipin işaretli mi işaretsiz mi olduğu, taşıdığı aralığın nereden başladığını
belirler ve bu da derlemede karara bağlanan bir özelliktir. isaretsiz tasma sonucu
satırı, unsigned char tipindeki bir değişkenin üst sınırına (UCHAR_MAX) ulaştıktan
sonra bir artırılmasını ölçüyor: sonuç 0. Bu, bir hata değil — standart, işaretsiz
tipler için taşmayı tanımlı kılar: sonuç, tipin temsil edebileceği değer sayısına göre
mod alınarak “sarar.” 255 + 1, sekiz bitlik işaretsiz bir tipte 256 mod 256 = 0 verir
ve bu her koşumda aynı sonucu üretir.
isaretsize donunce kucuk mu satırı işaretli-işaretsiz ayrımının başka bir yüzünü
gösteriyor: -1 değerindeki işaretli bir tamsayı, işaretsiz bir tiple karşılaştırılmadan
önce işaretsize dönüştürülüyor. Bu dönüşüm de tanımlıdır — negatif bir değer işaretsize
dönüştürüldüğünde, tipin temsil edebileceği değer sayısı eklenerek büyük bir işaretsiz
sayıya taşınır. Sonuç olarak -1, esik değişkenindeki 1den küçük değil, çok daha
büyük görünür ve karşılaştırma 0 (yanlış) döner. Kaynakta yazılan işaretler karşılaştırma
sırasında derleyici tarafından bu şekilde uzlaştırılır; hangi tarafın hangi tipe
dönüştürüleceği veriye değil, iki işlenenin bildirilen tipine bakılarak belirlenir.
Bu uzlaştırma kuralı, işaretsiz tiplerin neden sıklıkla sayaç ve boyut taşımak için
tercih edildiğini de açıklıyor: standart kitaplığın sizeof işleci size_t döndürür ve
bu tip işaretsizdir, çünkü negatif bir boyut kavramsal olarak anlamsızdır. Ama aynı seçim
bir tuzak da taşır. Bir döngü sınırını işaretsiz bir sayaçla geriye doğru yazmak —
sayacı 0dan aşağı indirmeye çalışmak — sayaç işaretsiz olduğu için hiçbir zaman
negatife düşmez, üst sınırdan sarar ve döngü koşulu hep doğru kalır. Bu durum tanımsız
değildir; sarma kuralı tam olarak yukarıdaki ölçümde görüldüğü gibi işler. Ama sonsuz bir
döngüye yol açacağı için bu ders bu senaryoyu koşturmuyor, yalnız işaretsiz sarmanın bir
sınır koşulunda nasıl beklenmedik bir sonuca dönüşebileceğini not ediyor —
davranışın kendisi tanımlı, sonucu yorumlamak kaynağı yazanın işi.
Sınırlayıcı Ölçüm
Taşma davranışı tipin işaretine bağlıdır ve bu bağımlılık simetrik değildir. İşaretsiz
tipler için taşma yukarıda ölçüldüğü gibi tanımlıdır ve sarma her zaman aynı kurala
uyar. İşaretli bir tipte taşma ise tanımsız davranıştır: INT_MAX değerindeki bir
inti bir artırmak, standardın hiçbir garanti vermediği bir duruma düşer — derleyici bu
durumu varsaymadığı için optimizasyonlar beklenmeyen sonuçlar üretebilir, program
çökebilir ya da hiçbir gözle görülür etki olmayabilir. Bu davranış kursun genel
kuralınca kod bloğunda denenmiyor.
Yanına konan tanımlı karşılık, yukarıdaki unsigned char ölçümüdür: aynı “üst sınırdan
bir öteye geçme” senaryosu, işaretsiz bir tiple kurulduğunda tam olarak öngörülebilir bir
sonuç veriyor. İki senaryo da aynı soruyu soruyor — tip sınırının ötesine geçilince ne
olur — ama yanıt yalnız işaretin kendisine bağlı: işaretsizde bir aritmetik kural,
işaretlide bir boşluk. Bir tipi işaretli ya da işaretsiz seçmek, bu yüzden yalnız hangi
değerlerin temsil edilebileceğini değil, sınırın aşılması durumunda programın hangi
kategoriye düştüğünü de belirleyen bir karardır ve bu karar tamamen derlemede, kaynakta
yazılan tip adıyla birlikte veriliyor.
Bu asimetrinin kaynağı, iki temsilin donanımdaki karşılığında yatar. İşaretsiz bir tip, bitlerini doğrudan bir sayının ikili gösterimi olarak okur; sarma, o gösterimin sınırlı genişliğinin doğal bir sonucudur ve her hedefte aynı biçimde işler. İşaretli bir tipte ise negatif değerlerin temsili hedefe bağlı olabilir ve taşma durumunda hangi sonucun doğacağını her temsil için garanti etmek, derleyicinin üst sınıra yakın değerler için yapabileceği bazı optimizasyonları kısıtlardı. Bu yüzden dil, işaretli taşmayı tanımlamak yerine tanımsız bırakmayı seçti — davranışı sabitlemek yerine derleyiciye “bu asla olmayacak” varsayımıyla çalışma özgürlüğü tanıdı.
Bu asimetrinin pratik sonucu, işaretli ve işaretsiz tipler arasında seçim yaparken yalnız “hangi değerler gerekli” sorusunun değil, “sınır aşılırsa ne olmalı” sorusunun da sorulması gerektiğidir. Bir sayaç asla negatif olmayacaksa işaretsiz bir tip, hem üst sınırdan bir bit daha geniş bir aralık kazandırır hem de taşmayı tanımlı bir sarmaya indirger. Bir hesaplama ise ara adımlarda geçici olarak negatife düşebiliyorsa, işaretli bir tip bu geçişi doğal biçimde taşır, ama üst sınıra yaklaşan bir toplamda tanımsız davranışa daha yakın durur. Derleyici bu tercihi kaynak yazarının yerine yapmıyor; yalnız yazılan tip adını okuyup, o adın taşıdığı sınırı ve sınırın aşılma kuralını sabitliyor.
Özet
- Bir tipin taşıdığı değer aralığı derleyicide karara bağlanır; bu kursta
sizeof(char) == 1dışında hiçbir boyut değeri basılmaz, yalnız karşılaştırma sonuçları ölçülür. - Tam sayı yükseltmesi,
charveshortgibi dar tipleri bir işlemden önceinte taşır; bu karar işlenen tiplere bakılarak verilir, taşınan veriye bakılmaz. - İşaretsiz tiplerde taşma tanımlıdır ve tipin değer sayısına göre sarar; işaretli tiplerde taşma tanımsızdır ve bu kursta hiçbir kod bloğunda koşturulmaz.
- İşaretli bir değerin işaretsize dönüştürülmesi de tanımlıdır: negatif bir değer, tipin değer sayısı eklenerek büyük bir işaretsiz sayıya taşınır.
- Numaralandırma sabitleri (
enum RenkiçindekiMAVIgibi) kaynaktaki sabit ifadelerden derlemede üretilir; bu konu Yapılar, Birlikler ve Numaralandırmalar dersinde derinleşecek. - Açık bir dönüşüm (
(unsigned char)gibi), örtük yükseltmenin tersine kaynakta görünür ve aynı sarma kuralına tabidir; hangi kuralın uygulanacağı yine hedef tipin işaretine bakılarak derlemede belirlenir.
Sonraki Adım
Bu derste ölçülen her karar tek bir birimin sınırları içinde kaldı: bir tipin aralığı, bir işlemin genişliği, bir dönüşümün yönü — hepsi aynı dosyaya bakarak, derleyicinin tek başına verebileceği kararlardı. Bu derste ölçülen tipler zaten var olan adlara bağlıydı; bir sonraki soru, o adın kendisinin nereden geldiği ve ne kadar yaşadığıyla ilgili. Bir adın hangi bildirime bağlandığı ve bir değerin ne kadar yaşadığı her zaman aynı aşamada karara bağlanmaz. Değişkenler, Kapsam ve Ömür dersi bu ikisini ayırıyor: bir adın nereden göründüğü derlemede sabitlenirken, bir değerin ne kadar yaşadığı ancak koşum sırasında gözlenebilir — ve ikisi arasındaki fark, aynı adın iki ayrı birimde iki ayrı nesneye düşebildiği bir üçüncü aşamayı da gündeme getirecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.