İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat