Ders 12 / 13
Yapılar, Birlikler ve Numaralandırmalar
Yapı alanlarının derlemede yerleşmesi, bir birliğin aynı belleği paylaşması ve numaralandırma sabitlerinin derleme zamanı sabiti olması; birlikte hangi alanın geçerli olduğunu dilin izlemediğinin bir etiket alanıyla gösterilmesi.
Yayın Güncelleme
Bu derste
- Yapı alanlarının derlemede yerleştiğini ve atamanın bütün alanları kopyaladığını ölçer
- Bir birliğin aynı belleği paylaştığını etiketli okumayla güvenle gösterir
- Numaralandırma sabitlerinin derleme zamanı sabiti olduğunu örnekle doğrular
İçindekiler
Diziler ve Dizgiler dersi, aynı tipten değerlerin bir dizi içinde nasıl sıralandığını ölçtü. Bu ders farklı tiplerden değerlerin tek bir ad altında nasıl toplandığını ölçüyor: bir yapı farklı tipleri yan yana yerleştirir, bir birlik aynı belleği farklı yorumlarla paylaştırır, bir numaralandırma ise adlandırılmış sabitler üretir — üçü de kaynakta yazılan bildirimden, derlemede karara bağlanan düzenekler.
Programlama Temelleri kursu (M01/K02) kayıt benzeri veri gruplamasını Python’la zaten kurmuştu; orada alanlar bir sözlük ya da nesne içinde, çalışma zamanında eklenip çıkarılabilen adlardı. C’nin karşılığı burada ölçülüyor: bir yapının hangi alanları taşıyacağı, bu alanların bellekte hangi sırayla duracağı ve bir numaralandırma sabitinin değeri — hepsi kaynakta yazılan bildirimden, program hiç çalışmadan derlemede sabitleniyor. Bu, kursun ölçü ekseninin son durağı: bir kaynağın dört aşamadan geçerek programa dönüştüğü C’nin Konumu dersinde açılan soru, burada üç yeni düzeneğin hangi aşamada karara bağlandığını sorarak kapanıyor.
Yapı: Alanların Derlemede Yerleşmesi
Bir yapı bildirimi, her alanın adını ve tipini kaynakta sabitler; bu bildirimden sonra o
yapı tipindeki her değişken aynı alan düzenini taşır. Diziler ve Dizgiler dersinde
ölçülen bir dizi, aynı tipten değerleri sıralarken, bir yapı farklı tiplerden değerleri
tek bir ad altında toplar — bir noktanın x ve y koordinatları gibi, birlikte anlam
taşıyan ama farklı olabilecek tiplerden değerler. Bir yapı değişkenini bir başkasına
atamak, bütün alanları tek seferde kopyalar.
// yapi.c — yapinin alanlari derlemede yerlesir, atama butun alanlari kopyalar
#include <stdio.h>
struct Nokta {
int x;
int y;
};
int main(void) {
struct Nokta a = {3, 4};
struct Nokta b = a;
b.x = 99;
printf("%s %d\n", "a.x", a.x);
printf("%s %d\n", "a.y", a.y);
printf("%s %d\n", "b.x", b.x);
printf("%s %d\n", "b.y", b.y);
return 0;
}
a.x 3 a.y 4 b.x 99 b.y 4
TS12. struct Nokta bildirimi, her Nokta değişkeninin bir x ve bir y alanı
taşıyacağını, bu alanların adlarını ve tiplerini derlemede sabitliyor — bu yapı hangi
alanları taşıyacağını koşumda değil, kaynakta belirliyor. b = a ataması, anın her iki
alanını da bye kopyalıyor; ardından b.xe yapılan değişiklik yalnız bnin kendi
kopyasını etkiliyor, a.x hâlâ 3. Bu, önceki derste ölçülen parametre kopyalama
kuralının bir yapı için nasıl işlediğini gösteriyor: bir yapı değişkeni bir başkasına
atandığında ya da bir işleve geçirildiğinde, aktarılan şey bütün alanların bir
kopyasıdır — dizinin aksine, yapı işleve geçtiğinde ilk elemanının adresine
dönüşmez, tüm alanlarıyla kopyalanır.
Alanların bellekteki sırası da kaynaktaki bildirim sırasıyla belirlenir: struct Noktanın x alanı her zaman yden önce yerleşir, çünkü bildirimde önce yazılan
alan önce gelir. Bir yapı, kendi içine başka bir yapıyı da alan bulundurabilir —
struct Nokta alanları taşıyan bir struct Cizgi { struct Nokta bas; struct Nokta son; }; gibi — ve bu iç içe geçme de aynı kuralı katmanlı biçimde sürdürür: her iç yapının
kendi alanları, dış yapının kendi alanı içinde, yine bildirim sırasına göre yerleşir. Bu yerleşim kararı, tıpkı bir dizinin eleman sayısı gibi, program hiç
çalışmadan derleyici tarafından sabitlenir — hangi alanın hangi adreste durduğu
koşumdaki veriye hiç bakılmadan, yalnız bildirimin kendisinden belirlenir. Bu kursta
hiçbir gerçek adres ya da bayt uzaklığı basılmıyor, ama bildirim sırasının bir sonucu
olması, yapı tanımının neden hem verinin anlamını hem bellekteki düzenini
aynı anda belirlediğini gösteriyor.
Birlik: Aynı Belleğin Paylaşılması
Bir birlik, birden çok alanı aynı bellek alanında tanımlar — her alan aynı adrese karşılık gelir, farklı bir tip yorumuyla. Birliğin kendisi hangi alanın “geçerli” olduğunu izlemez; bu bilgi, bir etiket alanıyla ayrıca taşınmalıdır.
// birlik.c — birligin ayni alani paylasmasi, etiketli okumayla guvenli kullanilir
#include <stdio.h>
enum Tur { TAM_SAYI, ONDALIK };
struct EtiketliDeger {
enum Tur tur;
union {
int tam;
float ondalikli;
} deger;
};
static struct EtiketliDeger tam_sayi_uret(int v) {
struct EtiketliDeger e;
e.tur = TAM_SAYI;
e.deger.tam = v;
return e;
}
static struct EtiketliDeger ondalik_uret(float v) {
struct EtiketliDeger e;
e.tur = ONDALIK;
e.deger.ondalikli = v;
return e;
}
static void yazdir(struct EtiketliDeger e) {
if (e.tur == TAM_SAYI) {
printf("%s %d\n", "tam sayi olarak okundu", e.deger.tam);
} else {
printf("%s %.2f\n", "ondalik olarak okundu", e.deger.ondalikli);
}
}
int main(void) {
struct EtiketliDeger bir = tam_sayi_uret(42);
struct EtiketliDeger iki = ondalik_uret(3.5f);
yazdir(bir);
yazdir(iki);
return 0;
}
tam sayi olarak okundu 42 ondalik olarak okundu 3.50
TS13. union içindeki tam ile ondalikli, aynı bellek alanını paylaşıyor —
biri yazıldığında öbürünün eski değeri anlamsız hâle geliyor, çünkü ikisi aynı baytları
farklı bir tip merceğinden okuyor. tam_sayi_uret yalnız deger.tamı yazıyor,
ondalik_uret yalnız deger.ondalikliyi; yazdir de her zaman tur alanına bakıp
hangi yorumun geçerli olduğunu ondan öğreniyor ve yalnız o alanı okuyor. Bu disiplin
sayesinde ders hiçbir zaman “yazılmayan” alanı okumuyor — bir için hep tam, iki
için hep ondalikli okunuyor. union EtiketliDeger (deger alanı) kendi başına hangi
üyenin son yazılan olduğunu bilmez; bu bilgiyi taşımak tur alanının ve o alanı
doğru tutmak kaynak yazarının işi.
Bir birliğin kaplayacağı yer de derlemede sabitlenir: standardın koyduğu ölçü, birliğin en büyük alanını taşımaya yetecek kadar yer ayrılmasıdır — çünkü herhangi bir anda herhangi bir alan yazılabilir olmalı. Bu kursta hiçbir gerçek boyut basılmadığı için bu ders bunu ölçmüyor, ama iki bildirimin büyüme biçimi arasındaki farkı belirtmek gerekir: yapı alanları arka arkaya dizer, birlik üst üste bindirir. Bu yüzden birliğe en büyüğünden küçük bir alan eklemek toplam yeri genellikle büyütmez; yapıya eklemek ise alan mevcut dolgunun içine sığmadıkça büyütür. İkisinde de son sözü hizalama söyler: standart hem yapının hem birliğin sonunda adsız dolgu bulunabileceğini belirtir, bu yüzden toplam yer alanların boyutlarından doğrudan okunmaz.
Numaralandırma: Derleme Zamanı Sabiti
Bir numaralandırma bildirimi, adlandırılmış tam sayı sabitleri üretir; bu sabitler kaynakta yazılan sabit ifadelerin kullanılabileceği her yerde kullanılabilir — örneğin bir dizinin uzunluğunu belirlemek için.
// asama.c — numaralandirma sabiti (kesit), derleme zamani sabiti olarak kullanilir
#include <stdio.h>
enum Renk { KIRMIZI = 1, YESIL = 2, MAVI = 4 };
int main(void) {
printf("%s %d\n", "numaralandirma sabiti", (int)MAVI);
int renkler[MAVI];
printf("%s %d\n", "sabitle olusturulan dizi eleman sayisi",
(int)(sizeof renkler / sizeof renkler[0]));
return 0;
}
numaralandirma sabiti 4 sabitle olusturulan dizi eleman sayisi 4
MAVI değeri, enum Renk bildiriminde yazılan 4 sabitinden geliyor ve bu değer
program hiç çalışmadan bellidir. int renkler[MAVI]; bildirimi bunu doğruluyor: MAVI,
Diziler ve Dizgiler dersinde ölçülen sabit ifade katlamasının kabul ettiği türden bir
ifade — bir değişken ya da bir işlev çağrısı olsaydı, dizinin boyutu derlemede
belirlenemezdi. renkler dizisinin eleman sayısı, hiçbir gerçek boyut basılmadan, yine
sizeof oranıyla ölçülüyor ve 4 çıkıyor — MAVI ile birebir aynı sayı, çünkü ikisi de
aynı derleme zamanı sabitinden türüyor.
enum Renk bildirimindeki KIRMIZI = 1, YESIL = 2, MAVI = 4 gibi açık değerler
zorunlu değildir; bir numaralandırma, hiçbir değer yazılmadan da bildirilebilir ve bu
durumda ilk ad 0dan başlayarak, her ad bir öncekinden bir fazla değer alır. Buradaki
bildirim değerleri açıkça yazıyor, çünkü KIRMIZI, YESIL ve MAVI ayrı bit
konumlarını (1, 2, 4) temsil ediyor ve bu, renklerin bir bit maskesiyle
birleştirilebilmesini sağlıyor — KIRMIZI | MAVI gibi bir ifade, iki rengi aynı anda
temsil eden 5 değerini üretebilir. Bu kullanım da yine derlemede sabitlenen bir
özellik: hangi sabitin hangi bit konumuna karşılık geldiği, kaynakta yazılan =
işaretinden geliyor, koşumdaki hiçbir veriye bakılmıyor.
Sınırlayıcı Ölçüm
Bir birlik, hangi alanının en son yazıldığını kendi başına izlemez — bu bilgiyi dil
değil, kaynak yazarı taşımak zorundadır. Yukarıdaki EtiketliDeger yapısı bunu bir
etiket alanıyla çözüyor: tur, degerin hangi üyesinin geçerli olduğunu söylüyor ve
yazdir her zaman önce ture bakıyor.
Eğer bu disiplin bırakılsaydı — yazdir ture hiç bakmadan, örneğin her zaman
deger.ondalikliyi okusaydı — bir değişkeni için (ki tam_sayi_uret yalnız
deger.tamı yazmıştı) okunan bayt örüntüsü bir int olarak yazılmış, bir float
olarak okunmuş olurdu. Bu, birliğin garanti ettiği tek şeyin aynı adresi paylaşmak
olduğunu, hangi tip yorumunun anlamlı olduğunu garanti etmediğini gösteriyor —
yorum uyuşmazlığının sonucu belirsiz bir bit örüntüsüdür ve bu kursun genel kuralınca
kod bloğunda denenmiyor. Yanına konan tanımlı karşılık, yukarıdaki etiketli
okumanın kendisi: her okuma, o birliğe en son hangi üyenin yazıldığını bağımsız bir
alandan (tur) öğrenip yalnız o üyeyi okuyor ve bu ölçüm üç kez koşturulduğunda da
aynı iki satırı üretiyor.
Etiketli okumanın çalışması için, etiketin her zaman ilgili yazmayla birlikte
güncellenmesi gerekiyor — tam_sayi_uret ve ondalik_uret işlevlerinin ikisi de,
kendi alanlarını yazmadan önce turü ayarlıyor. Eğer bir üçüncü işlev deger.tamı
yazıp turü güncellemeyi unutsaydı, o değişkeni okuyan yazdir hâlâ eski tur
değerine bakar ve yanlış üyeyi okurdu — birlik kendi başına bu tutarsızlığı ne
üretebilir ne engelleyebilir, çünkü birliğin kendisi yalnız bellek paylaşımını sağlıyor,
etiketle senkron kalmayı değil. Bu, C’nin birlik düzeneğinin bilinçli bir tasarım
kararını yansıtıyor: dil, alan seçimini izlemenin maliyetini üstlenmiyor — her
yazmada hangi alanın aktif olduğunu ayrıca kaydetmiyor — ve bu sorumluluğu, tıpkı dizi
uzunluğu bilgisinde olduğu gibi, tamamen kaynak yazarına bırakıyor.
Bu, yapı ile birliğin birbirini tamamlayan iki düzenek olduğunu da gösteriyor: yapı her
alan için ayrı bellek ayırıp hepsini aynı anda geçerli kılarken, birlik tek bir
bellek alanını paylaştırıp yalnız birini geçerli kılıyor. EtiketliDeger, bu ikisini
birleştiriyor — dıştaki yapı hem etiketi hem paylaşılan alanı taşıyor ve etiket alanı
sayesinde birliğin izlemediği bilgi yapı düzeyinde tutulmuş oluyor.
Bu birleşim, C’de “etiketli birlik” (tagged union) diye anılan yaygın bir kalıbın temelini oluşturuyor: birden çok olası biçimden yalnız birini taşıyan bir değeri modellemek isteyen bir kaynak yazarı, bu üç düzeneği — yapı, birlik ve numaralandırma — birlikte kullanır. Numaralandırma, hangi biçimin geçerli olduğunu adlandırılmış sabitlerle ifade eder; birlik, o biçimlerin ortak bir bellek alanını paylaşmasını sağlar; yapı, ikisini bir arada, tek bir değer olarak taşır. Bu derste ölçülen üç düzenek, bu yüzden birbirinden bağımsız üç konu değil, aynı kalıbın üç bileşeni.
Özet
- Bir yapının alanları, adları ve tipleriyle birlikte derlemede sabitlenir; bir yapı değişkeni bir başkasına atandığında bütün alanlar tek seferde kopyalanır.
- Bir birlik, birden çok alanı aynı bellek adresinde tanımlar; hangi alanın son yazılan olduğunu dil izlemez, bu bilgiyi kaynak yazarı ayrıca (bir etiket alanıyla) taşımak zorundadır.
- Numaralandırma sabitleri (
enum RenkiçindekiMAVIgibi) derleme zamanı sabitleridir; bir dizinin uzunluğunu belirlemek gibi sabit ifade gerektiren her yerde kullanılabilirler — sınıf derleyici. - Bir birlikte yazılmayan bir üyeyi okumak (etiketsiz, yanlış tip varsayımıyla) baytları başka bir tip olarak yeniden yorumlar; C bunu yasaklamaz ama son yazılan üyeye karşılık gelmeyen baytların değeri belirtilmemiştir ve okunan örüntü bir tuzak temsiliyse davranış tanımsız olur. Bu kursta koşturulmaz; tanımlı karşılık, etiketli okumadır.
- Yapı her alanı ayrı bellekte tutup hepsini birden geçerli kılarken, birlik tek bir bellek alanını paylaştırıp yalnız birini geçerli kılar — ikisi birbirini tamamlayan iki düzenek.
- Numaralandırma, birlik ve yapı bir arada kullanıldığında “etiketli birlik” kalıbını kurar: numaralandırma hangi biçimin geçerli olduğunu adlandırır, birlik biçimlerin belleğini paylaştırır, yapı ikisini tek bir değerde birleştirir.
Sonraki Adım
Bu konuda ölçülen her karar, bir programın tek bir koşumu içinde kaldı: bir tipin
aralığı, bir adın kapsamı, bir işlecin seçimi, bir dalın izlediği yol, bir çağrının
bağlandığı gövde, bir dizinin uzunluğu, bir birliğin geçerli alanı — hepsi ya kaynaktan
ya da o koşumun kendi verisinden okunabiliyordu. Girdi/Çıktı dersi bu zemine son bir
soru ekliyor: bir programın dışarıdan aldığı ya da dışarıya yazdığı veri, hangi
aşamada yorumlanıyor? Bir biçim dizesindeki (format string) her yer tutucu, karşılık
gelen bağımsız değişkenin tipiyle uyuşmak zorunda, ama bu uyuşmayı derleyici yalnız
belirli koşullarda yakalayabiliyor — dilin kendisi bunu garanti etmiyor. Bu derste
ölçülen EtiketliDeger örneğinde etiketi doğru tutmak kaynak yazarının işiydi; sıradaki
derste biçim dizesini argümanlarla uyumlu tutmak da aynı şekilde kaynak yazarının
sorumluluğunda olacak.
Bu konuda ölçülen yedi ders, dört sınıfın (ön işlemci, derleyici, bağlayıcı, koşum) her birine en az bir kez uğradı: Veri Tipleri ve İşleçler derleyicide, Denetim Akışı koşumda, Değişkenler ile Fonksiyonlar bağlayıcıda, Diziler ve Dizgiler ikisinde birden ve bu ders yine derleyicide durdu. Kapanış dersi, bu yedi dersin ölçtüğü kararları tek bir tabloda topluyor ve girdi çıktının kendi payını — biçim dizelerinin koşumda yorumlanmasını — bu tabloya ekliyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.