İçeriğe geç
academia.sh

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) == 1 dışında hiçbir boyut değeri basılmaz, yalnız karşılaştırma sonuçları ölçülür.
  • Tam sayı yükseltmesi, char ve short gibi dar tipleri bir işlemden önce inte 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 Renk içindeki MAVI gibi) 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat