Ders 08 / 13
İşleçler
Bir işlecin sonucunun işlenenlerin tipine göre derlemede karara bağlanması; öncelik, kısa devre değerlendirmesi, bileşik atama ve değerlendirme sırasının tanımlı-tanımsız ayrımı üzerinden ölçülmesi.
Yayın Güncelleme
Bu derste
- Bir işlecin sonucunun işlenen tipine göre derlemede seçildiğini ölçerek gösterir
- Öncelik, kısa devre değerlendirmesi ve bileşik atamayı örnekle ayırt eder
- Değerlendirme sırasının hangi durumlarda tanımlı hangi durumlarda tanımsız olduğunu ayırır
İçindekiler
Değişkenler, Kapsam ve Ömür dersi, bir adın nereden göründüğünü ve bir değerin ne kadar yaşadığını ölçtü — ikisi de bir adın durumuyla ilgiliydi. Bu ders bir adım öteye geçiyor: iki değer bir işleçle birleştirildiğinde, sonucu neyin belirlediğini ölçüyor. Yanıt, işlenenlerin değeri değil, tipidir — ve bu seçim, tıpkı önceki derste bir adın hangi bildirime bağlandığı gibi, derlemede karara bağlanıyor. Bu ders dört ayrı işleç ailesini — aritmetik, mantıksal, bitsel ve atama — aynı soruyla ölçüyor: kaynakta yazılan bir işlecin sonucu, ya da o sonuca varmak için hangi alt işlemlerin çalışacağı, koşum başlamadan önce ne kadarı zaten bellidir?
Programlama Temelleri kursu (M01/K02) aritmetik ve mantıksal işleçleri Python’la zaten kurmuştu; orada bir işlecin davranışı çoğunlukla işlenenlerin çalışma zamanındaki değerine bakılarak yorumlanırdı. C’nin karşılığı burada ölçülüyor: bir işlecin hangi işlemi yürüteceği, kaynakta yazılan işlenenlerin tipine bakılarak, işlem hiç çalışmadan önce derlemede sabitleniyor.
İşlecin Tipe Göre Seçimi
Aynı / işleci, işlenenlerinin tipine göre iki farklı işlem yürütür: ikisi de tam sayıysa
tam sayı bölmesi, en az biri kayan noktalıysa kayan nokta bölmesi. Bu seçim veriye değil,
kaynakta yazılan tiplere bakılarak yapılıyor.
// asama.c — islecin tipe gore secimi (kesit)
#include <stdio.h>
int main(void) {
int tam = 7 / 2;
double kesirli = 7 / 2.0;
printf("tam %d kesirli %.1f\n", tam, kesirli);
return 0;
}
tam 3 kesirli 3.5
TS5. Kaynakta yazılan tek fark, 2 ile 2.0 — biri tam sayı sabiti, öbürü kayan
nokta sabiti. Bu fark, hangi bölme işleminin çalışacağını derlemede belirliyor: 7 / 2
tam sayı bölmesi yürütüyor ve kesirli kısmı atıyor, 7 / 2.0 ise 7yi önce doublea
yükseltip kayan nokta bölmesi yürütüyor. 7nin kendisi hiç değişmedi; değişen, aynı /
işlecinin hangi donanım işlemine karşılık geldiği — kaynakta yazılan iki karakterlik
işleç sembolü aynı kalırken, derleyicinin ürettiği makine talimatı işlenenlerin tipine
göre iki farklı yol izliyor. Bu, önceki derste ölçülen tam sayı
yükseltmesiyle aynı ailede bir karar: işleç, işlenenlerin bildirilen tipine bakıyor,
çalışma zamanındaki değerine değil.
Bu kural yalnız bölme işlecine özgü değil; C standardının olağan aritmetik
dönüşümler dediği kural, hemen hemen bütün ikili aritmetik işleçler için aynı şekilde
işler. İki işlenenin tipi farklıysa, “dar” olan taraf “geniş” olana yükseltilir ve işlem
geniş tipte yürütülür — tıpkı Veri Tipleri dersinde char işlenenlerin inte
yükseltilmesi gibi, burada da bir int ile bir double toplandığında int önce
doublea dönüştürülür. Kaynakta tam_sayac + oran gibi bir ifade yazıldığında, hangi
işlenenin hangi yöne dönüştürüleceği iki tarafın bildirilen tipine bakılarak
derlemede belirlenir; hangi değerlerin toplanacağı bu kararı hiç etkilemez.
Öncelik: Kaynaktaki Yapının Derlemede Sabitlenmesi
Bir işlecin ne zaman çalışacağı yalnız tipe değil, kaynaktaki yapıya da bağlıdır. Öncelik kuralları, parantezsiz bir ifadenin hangi alt ifadelerden önce değerlendirileceğini belirler ve bu karar tamamen kaynağın kendisinde yazılıdır.
// oncelik.c — islec onceligi kaynaktaki yapiyi derlemede sabitler
#include <stdio.h>
int main(void) {
int carpma_once = 2 + 3 * 4;
int toplama_once = (2 + 3) * 4;
printf("%s %d\n", "carpma once (parantezsiz)", carpma_once);
printf("%s %d\n", "toplama once (parantezli)", toplama_once);
return 0;
}
carpma once (parantezsiz) 14 toplama once (parantezli) 20
İki ifade de aynı üç sayıyı (2, 3, 4) ve aynı iki işleci (+, *) taşıyor, ama
sonuçları farklı: carpma_once çarpmayı toplamadan önce yürütüyor (2 + 12 = 14),
toplama_once ise parantez sayesinde toplamayı önce yürütüyor (5 * 4 = 20). Bu fark
hiçbir veriye bakmadan, yalnız kaynaktaki parantezlerin varlığına bakılarak derlemede
çözülüyor — koşum başlamadan önce hangi alt ifadenin hangi sırada değerlendirileceği
zaten belli.
Öncelik yalnız hangi işlecin önce çalışacağını değil, aynı önceliğe sahip işleçler yan
yana yazıldığında hangi yönden gruplanacağını da belirler — buna birleşme yönü
denir. Çoğu ikili işleç (+, -, *, /) soldan sağa birleşir: 10 - 3 - 2 ifadesi
(10 - 3) - 2 gibi gruplanır, 10 - (3 - 2) gibi değil ve bu iki gruplama farklı
sonuçlar verir (5 ile 9). Atama işleci ise tam tersi yönde, sağdan sola birleşir:
a = b = 5 ifadesi a = (b = 5) gibi gruplanır, önce bye 5 atanır, sonra o atamanın
sonucu aya atanır. Hem öncelik hem birleşme yönü, koşum hiç başlamadan, yalnız kaynaktaki
işleç dizilişine bakılarak derlemede çözülür.
Kısa Devre: Hangi İşlecin Seçildiği İkinci İşleneni Etkiler
Mantıksal işleçler (&&, ||) bir garanti taşır: sol işlenen sonucu belirlemeye
yetiyorsa, sağ işlenen hiç değerlendirilmez. Bitsel işleçler (&, |) aynı garantiyi
taşımaz — iki işlenen de her zaman değerlendirilir. Hangi garantinin geçerli olduğu,
kaynakta hangi işlecin yazıldığına bakılarak derlemede belirlenir.
// kisadevre.c — hangi islecin secildigi ikinci islenenin calisip calismayacagini derlemede belirler
#include <stdio.h>
static int cagri_sayaci = 0;
static int yan_etkili(void) {
cagri_sayaci++;
return 1;
}
int main(void) {
cagri_sayaci = 0;
int ve_sonucu = (0 && yan_etkili());
printf("%s %d\n", "ve isleci sonrasi cagri sayaci", cagri_sayaci);
cagri_sayaci = 0;
int bitsel_sonucu = (0 & yan_etkili());
printf("%s %d\n", "bitsel islec sonrasi cagri sayaci", cagri_sayaci);
(void)ve_sonucu;
(void)bitsel_sonucu;
return 0;
}
ve isleci sonrasi cagri sayaci 0 bitsel islec sonrasi cagri sayaci 1
TS6. İki çağrı da aynı yan_etkili işlevini, aynı 0 sol işleneniyle kullanıyor,
ama sonuç farklı: && ile sol işlenen zaten yanlış olduğu için sağ işlenen hiç
çalıştırılmıyor ve cagri_sayaci sıfırda kalıyor; & ile ise sol işlenenin değeri
sonucu belirlemeye yetse de, işlecin kendisi böyle bir kısayolu tanımıyor ve
yan_etkili yine de çağrılıyor. Hangi garantinin uygulanacağı, koşum sırasında
cagri_sayacinin gerçekten 0 mı 1 mi olacağını belirleyen bir karar — ama bu kararın
kendisi, hangi işlecin seçildiğine bakılarak, derleme sırasında sabitleniyor. İki
işlecin ikisi de aynı sol işlenenle aynı mantıksal sonucu (0) üretirdi; aralarındaki
fark sonuçta değil, ikinci işlenenin çalışıp çalışmayacağında.
Bu ayrım, yan_etkili gibi bir çağrının yalnızca gerektiğinde çalışmasını sağlamak
istendiğinde önem kazanıyor. Sol işlenen tek başına sonucu belirlemeye yetiyorsa (&&
için yanlış, || için doğru), kısa devre garantisi sağ tarafın hiç çalışmamasını
sağlıyor — bu, gereksiz bir işi atlamanın kaynakta yazılan tek satırla elde edilen bir
yoludur. Bitsel işleçler bu garantiyi taşımadığı için, iki işlenen de her koşulda
çalışır; bu yüzden bitsel işleçler mantıksal bir koşul için değil, gerçekten bit
düzeyinde bir birleştirme gerektiğinde tercih edilir.
Bileşik Atama: Sol Tarafın Tipine Göre Dönüşüm
Bir bileşik atama işleci (+=, |= gibi) sonucu her zaman sol tarafın bildirilen
tipine dönüştürür — sağ tarafın tipi ne olursa olsun.
// atama.c — bilesik atama sonucu sol tarafin tipine gore derlemede donusturulur
#include <stdio.h>
int main(void) {
int tam_sayi = 5;
tam_sayi += 2.7;
printf("%s %d\n", "int uzerinde bilesik atama", tam_sayi);
unsigned char bayrak = 0xF0;
bayrak |= 0x0F;
printf("%s %d\n", "unsigned char uzerinde bilesik atama", bayrak);
int carpan = 6;
carpan *= 2.5;
printf("%s %d\n", "int uzerinde carpimsal atama", carpan);
int sayac = 10;
sayac -= 3;
printf("%s %d\n", "int uzerinde cikarma atamasi", sayac);
return 0;
}
int uzerinde bilesik atama 7 unsigned char uzerinde bilesik atama 255 int uzerinde carpimsal atama 15 int uzerinde cikarma atamasi 7
tam_sayi += 2.7, açılımıyla tam_sayi = tam_sayi + 2.7 gibi çalışır: toplam önce
double genişliğinde hesaplanır (5 + 2.7 = 7.7), ama sonuç tam_sayinın tipi int
olduğu için kesirli kısım atılarak 7ye dönüştürülür. Bu dönüşüm sağ taraftaki 2.7nin
değerine bakılarak değil, sol taraftaki tam_sayinın bildirilen tipine bakılarak
uygulanıyor — sağ taraf 2.1 ya da 2.9 olsaydı da aynı kesme kuralı işlerdi.
bayrak |= 0x0F ise bitsel bir birleştirme: 0xF0 ile 0x0Fin bit düzeyinde birleşimi
0xFF, ondalıkla 255. carpan *= 2.5 aynı kuralı bir kez daha doğruluyor: 6 * 2.5 = 15.0, double genişliğinde tam sayı değerli bir sonuç ve carpanın tipi int
olduğu için hiç kesme kaybı olmadan 15e dönüşüyor — kesmenin her zaman bir kayıp
anlamına gelmediğini, yalnız sonucun sol tarafın tipine zorlandığını gösteriyor.
sayac -= 3 ise iki tarafın da zaten aynı tipte olduğu, dönüşümün görünmediği sıradan
durum. Dört örnekte de sonucun tipi sol tarafın bildirilen tipiyle sabitleniyor; bu,
atama işlecinin de tıpkı bölme işleci gibi tip odaklı çalıştığını gösteriyor.
Sınırlayıcı Ölçüm
İşleç önceliği, kaynaktaki alt ifadelerin hangi sırayla gruplanacağını belirler, ama
bir ifadenin bütün alt bölümlerinin hangi sırayla değerlendirileceğini her zaman
belirlemez. f(a, b) gibi bir çağrıda a ile bnin hangisinin önce hesaplanacağı
belirtilmemiştir; eğer a ve b aynı değişkeni yan etkiyle değiştiriyorsa — örneğin
ikisi de aynı sayacı artırıyorsa — bu, standardın tanımsız davranış olarak
sınıflandırdığı bir durumdur: aynı nesne, aralarında bir sıralama noktası olmadan iki kez
değiştiriliyor. Bu kursun genel kuralınca böyle bir çağrı kod bloğunda denenmiyor.
Yanına konan tanımlı karşılık, virgül işlecidir: , ile ayrılan iki alt ifade her zaman
soldan sağa, aralarında tam bir sıralama noktasıyla değerlendirilir — sol taraf
tamamlanmadan sağ taraf başlamaz.
// sinir.c — virgul isleci sirayi garanti eder, fonksiyon argumanlari garanti etmez
#include <stdio.h>
int main(void) {
int i = 0;
int son_deger = (i++, i++);
printf("%s %d\n", "virgul isleciyle son deger", son_deger);
printf("%s %d\n", "virgul isleciyle i", i);
return 0;
}
virgul isleciyle son deger 1 virgul isleciyle i 2
(i++, i++) ifadesinde sol taraftaki i++ önce tamamen çalışır: inin eski değerini
(0) üretir ve iyi 1e taşır — ama bu değer virgül işleci tarafından atılır. Ardından
sağ taraftaki i++ çalışır: bu kez inin (artık 1 olan) değerini üretir ve iyi
2ye taşır; ifadenin toplam sonucu sağ tarafın ürettiği değer, yani 1. Sonuç
tamamen öngörülebilir ve her koşumda aynı: virgül işleci, fonksiyon çağrısının argüman
listesinin vermediği garantiyi tam olarak veriyor. Aynı i++, i++ deseni bir
fonksiyon çağrısının iki bağımsız değişkeni olarak yazılsaydı (f(i++, i++) gibi),
derleyicinin argümanları hangi sırayla değerlendireceği belirtilmemiş olurdu ve aynı
inin iki kez değiştirilmesi tanımsız davranışa düşerdi — bu ders o çağrıyı
denemiyor, yalnız virgül işlecinin garantili sırasını ölçüyor.
Aynı sınıftaki bir başka klasik örnek i + i++ gibi bir ifadedir: toplama işlecinin iki
işleneni arasında da bir sıralama garantisi yoktur ve sol taraftaki i okunurken sağ
taraftaki i++nin bu okumadan önce mi sonra mı iyi değiştirdiği belirsizdir — aynı
nesne okunuyor ve değiştiriliyor, aralarında bir sıra garantisi olmadan. Bu da bu kursun
genel kuralınca koşturulmuyor. Buradaki ortak nokta, C’nin çoğu işlecinin işlenenleri
arasında hiçbir sıralama garantisi vermediğidir — sıralama yalnız birkaç özel yapı
için (virgül işleci, &&, ||, ?:in koşul kısmı) açıkça garanti edilir. Bir ifadede
aynı değişkeni birden fazla kez, en az biri değiştirerek kullanmak isteyen bir kaynak
yazarı, bu garantili yapılardan birine ya da ayrı ifadelere yaslanmak zorundadır;
aksi hâlde derleyicinin hangi sırayı seçeceği belirsiz kalır.
Bu derste ölçülen kararların tümü aynı soruya bağlanıyor: bir işlecin sonucu, işlenen tiplerine ve kaynaktaki yapıya bakılarak derlemede mi belirleniyor, yoksa işlenenlerin gerçek değerine bakılarak koşumda mı? Bölme, öncelik, kısa devre ve bileşik atama için yanıt derleyiciydi — hangi işlemin çalışacağı ya da hangi sıranın izleneceği, program hiç çalışmadan önce kaynaktan okunabiliyordu. Değerlendirme sırasının tanımsız kaldığı durumlar ise bu kuralın istisnası değil, sınırı: derleyicinin garanti etmediği yerde, kaynak yazarının kendi garantisini kurması gerekiyor.
Özet
- Bir işlecin hangi işlemi yürüteceği, işlenenlerin çalışma zamanındaki değerine değil, kaynakta yazılan tiplerine bakılarak derlemede karara bağlanır — sınıf derleyici.
- İşleç önceliği, parantezsiz bir ifadenin hangi alt ifadelerin önce gruplanacağını kaynağın kendi yapısından, hiçbir veriye bakmadan belirler; birleşme yönü aynı önceliğe sahip işleçler yan yana yazıldığında hangi taraftan gruplanacağını verir.
&&ve||kısa devre garantisi taşır,&ve|taşımaz; hangi garantinin geçerli olduğu, kaynakta hangi işlecin yazıldığına bakılarak derlemede sabitlenir.- Bileşik atama işleçleri, sonucu her zaman sol tarafın bildirilen tipine dönüştürür — bu dönüşüm sağ tarafın değerine değil, sol tarafın tipine bakılarak yapılır.
- Fonksiyon çağrısı argümanlarının değerlendirme sırası belirtilmemiştir ve aynı değişkeni sıralama noktası olmadan iki kez değiştirmek tanımsız davranıştır; bu kursta koşturulmaz. Virgül işleci ise soldan sağa garantili bir sıra taşır ve tanımlı karşılık olarak koşturuldu.
Sonraki Adım
Bu derste ölçülen kararların hepsi tek bir ifadenin sınırları içinde kaldı: bir işlecin
hangi işlemi yürüttüğü, hangi alt ifadenin önce gruplandığı, hangi işlenenin
değerlendirildiği. Bu kararların ortak özelliği, sonucu üreten verinin hiçbir zaman
kararı etkilememesiydi — yalnız kaynakta yazılan tipler ve yapı belirleyiciydi. Denetim
Akışı dersi bu zemine yeni bir soru ekliyor: bir programın hangi dalı izleyeceği
artık kaynakta değil, koşumda üretilen bir veriye bakılarak belirleniyor. Bu derste
carpma_once gibi bir sonucun hangi işlemden geldiği kaynaktan okunabiliyordu; sıradaki
derste bir dalın hangi kod yolunu izleyeceği, ancak program çalışıp veriyi ürettikten
sonra bilinebilecek — kaynağı okuyarak önceden söylenemeyecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.