İçeriğe geç
academia.sh

Ders 10 / 13

Fonksiyonlar

Bir çağrının başka bir birimdeki tanıma bağlanmasının bağlayıcı aşamasında çözülmesi; bildirim-tanım ayrımı, parametre aktarımının her zaman kopya olması ve dönüş değerinin tipinin çağıran tarafın yorumunu belirlemesi.

Yayın Güncelleme

Bu derste

  • Bir çağrının başka birimdeki tanıma bağlanmasının bağlayıcı aşamasında çözüldüğünü ölçer
  • Bildirim ile tanımın ayrılmasını ve parametre aktarımının her zaman kopya olduğunu gösterir
  • Dönüş değerinin tipinin çağıranın yorumunu nasıl belirlediğini örnekle ayırt eder
İçindekiler

Denetim Akışı dersi, hep aynı işlevin içinde kalan bir soruyla ilgilendi: hangi dal seçilecek. Bu ders o sınırı kaldırıyor ve bir çağrının işlev sınırını nasıl geçtiğini ölçüyor. Bir işlev çağrıldığında üç ayrı soru birden çözülüyor: çağrının hangi tanıma bağlanacağı, argümanların işleve nasıl ulaştığı ve dönen değerin nasıl yorumlanacağı — üçü de aynı anda, ama farklı gerekçelerle karara bağlanan sorular. İlki bağlayıcıya aittir, çünkü birden çok birim gerektirir; öbür ikisi ise derleyicinin tek bir birime bakarak verdiği kararlardır — bir çağrının tam olarak nereye bağlanacağını bilmese bile, derleyici argümanların nasıl kopyalanacağını ve dönüşün nasıl yorumlanacağını bildirimden çıkarabilir.

Programlama Temelleri kursu (M01/K02) fonksiyon kavramını Python’la zaten kurmuştu; orada bir işlevin çağrılabilmesi için önce tanımlanmış olması yeterliydi ve bildirim-tanım ayrımı yoktu. C’nin karşılığı burada ölçülüyor: bir çağrı, tanımı hiç görmeden, yalnız bir bildirime dayanarak derlenebiliyor — tanımın kendisi başka bir birimde durabiliyor ve iki tarafı birbirine bağlayan adım ayrı bir aşamaya ait. Bu derste ölçülen üç karar — bağlanma, aktarım, dönüş — bir işlevin çağrılma yüzeyinin üç ayrı bileşeni ve her biri kendi aşamasında, birbirinden bağımsız olarak sabitleniyor.

Bağlayıcı: Başka Birimdeki İşlev

Aşağıdaki kesit, Derleme Aşamaları dersinde tanıtılan çekirdeğin bir parçasıdır: bir işlev bir birimde çağrılıyor, tanımı ise başka bir birimde duruyor.

// asama.h — baska birimlerde tanimli adlarin bildirimi (kesit: islev bildirimi)
#ifndef ASAMA_H
#define ASAMA_H

/* Bu islev baska bir birimde tanimli: cagrisi baglama asamasinda cozulur. */
int kimlik(void);

#endif
// secim.c — ikinci birim: islevin tanimi burada durur
#include "asama.h"

int kimlik(void) {
    return 7;
}
// asama.c — baska birimdeki islevin baglanmasi (kesit)
#include <stdio.h>
#include "asama.h"

int main(void) {
    printf("%s %d\n", "baska birimdeki islev", kimlik());
    return 0;
}
baska birimdeki islev 7

TS8. asama.c derlenirken kimlikin gövdesi hiçbir yerde görünmüyor — derleyici yalnız asama.hdeki bildirimi görüyor ve bu bildirimden, çağrının doğru sayıda ve doğru tipte argüman aldığını, dönüş tipinin int olduğunu denetleyip nesne dosyasına bir “bu ada ihtiyacım var” notu bırakıyor. secim.c ayrı ayrı derleniyor ve kendi nesne dosyasına kimlikin gerçek gövdesini yazıyor. İki nesne dosyası bir araya getirildiğinde bağlayıcı, asama.cnin bıraktığı notu secim.cnin sağladığı gövdeyle eşleştiriyor — bu eşleşme olmadan çalıştırılabilir bir program hiç oluşmaz. Kapsam ve ömür bir birimin içinde kalan sorulardı; bir çağrının hangi gövdeye bağlanacağı ise ancak birimler bir araya geldiğinde, bağlayıcı aşamasında çözülüyor.

Bu üç aşamalı akış — önce iki birim ayrı ayrı derlenir, sonra bağlayıcı ikisini birleştirir — Derleme Aşamaları dersinde ölçülen genel düzenin işlevlere uygulanmış hâli. asama.cnin derlenmesi secim.cnin var olup olmadığından habersiz ilerler; derleyici yalnız asama.hdeki bildirime bakarak “bu isimde, bu imzada bir işlev bir yerlerde tanımlı olmalı” varsayımıyla çalışır ve bu varsayımı doğrulamak bağlayıcının işidir. Eğer secim.c hiç derlenmemiş ya da kimlik tanımı yanlışlıkla silinmiş olsaydı, asama.cnin derlenmesi yine sorunsuz tamamlanırdı — hata ancak bağlama adımında, bağlayıcı kimlik adını hiçbir nesne dosyasında bulamadığında ortaya çıkardı. Bu, bir derleme hatasıyla bir bağlama hatasının neden farklı aşamalarda, farklı zamanlarda ortaya çıkabildiğini gösteriyor.

Bu düzenek, Değişkenler, Kapsam ve Ömür dersinde ölçülen extern değişken bağlanmasıyla aynı ailededir: orada bir adın hangi depolama alanına, burada bir adın hangi gövdeye karşılık geldiği bağlayıcıda çözülüyor. Fark, işlevlerin varsayılan olarak dış bağlantılı olması — bir değişkeni paylaşmak için extern yazmak gerekirken, bir işlev static yazılmadıkça zaten birimler arasında görünür ve çağrılabilir durumdadır. kimlik işlevi static olmadığı için secim.cdeki tanımı asama.cden görülebiliyor; işlev static kimlik(void) diye tanımlansaydı, kendi birimine kapanır ve asama.cnin derlenmesi, bağlayıcının aynı adı bulamaması yüzünden başarısız olurdu.

Bu, static anahtar sözcüğünün işlevler için de değişkenlerdekiyle aynı işlevi gördüğünü doğruluyor: kapsamı dosya sınırına daraltıyor ve bağlayıcının o adı başka birimlere göstermesini engelliyor. Bir işlevi static bildirmek, o işlevin yalnız kendi biriminin iç işleyişine ait bir yardımcı olduğunu, dışarıya bir arayüz olarak sunulmadığını kaynakta açıkça işaretler; bu, bir birimin kendi iç ayrıntılarını gizleyebilmesinin işlevler düzeyindeki karşılığı.

Bildirim ile Tanımın Ayrılması

Bir bildirim, derleyiciye bir işlevin imzasını (adı, parametre tipleri, dönüş tipi) söyler; bir tanım ise gövdeyi de içerir. asama.hdeki int kimlik(void); bir bildirimdir — noktalı virgülle biter, gövde taşımaz. secim.cdeki int kimlik(void) { return 7; } ise bir tanımdır. Bir işlev, farklı birimlerde (aynı başlık dosyası aracılığıyla) istenildiği kadar çok kez bildirilebilir, ama tanımı tam olarak bir kez bulunmalıdır — aksi hâlde bağlayıcı aynı ada birden fazla gövde bulur ve bu bir çelişkidir. Bu ayrım, önceki derste değişkenler için ölçülen bildirim-tanım ayrımının işlevlere uygulanmış hâli: extern int birim_sayaci; bir değişken bildirimiyken int birim_sayaci = 2; tanımdı; burada da aynı örüntü, gövde yerine yalnız imza taşıyan bir bildirimle tekrar ediyor.

Bir işlev bildirimi olmadan, yalnız çağrılma biçimine bakarak bir çağrının doğru olup olmadığını denetlemek mümkün değildir; derleyici, kaç parametre beklendiğini ve hangi tipte olduklarını yalnız bildirimden öğrenir. asama.hnin asama.c tarafından dahil edilmesi tam olarak bunu sağlıyor: asama.c derlenirken kimlikin gövdesini hiç görmüyor, ama parametresiz ve int döndüren bir işlev olduğunu biliyor ve çağrıyı buna göre derliyor. Bildirim, bu yüzden yalnız bir belge değil — derleyicinin çağrıyı doğru biçimde üretebilmesi için ihtiyaç duyduğu asgari bilgi.

Parametre Aktarımı: Her Zaman Kopya

C’de bir işleve geçirilen her argüman, işlevin kendi parametresine kopyalanır. İşlev parametresinde yapılan bir değişiklik, çağıran taraftaki değişkeni hiçbir zaman etkilemez.

// kopya.c — parametre aktarimi her zaman kopyadir, cagiranin degiskenini etkilemez
#include <stdio.h>

static void ikiye_katla(int deger) {
    deger = deger * 2;
    printf("%s %d\n", "islev icinde deger", deger);
}

int main(void) {
    int sayi = 5;
    ikiye_katla(sayi);
    printf("%s %d\n", "cagiran taraftaki sayi", sayi);
    return 0;
}
islev icinde deger 10
cagiran taraftaki sayi 5

Ölçüm iki satır yazdırıyor: işlev içindeki deger, çarpma sonrası 10; çağıran taraftaki sayi ise hâlâ 5. ikiye_katla çağrıldığında, sayinın değeri (5) deger adlı yeni bir depolama alanına kopyalanıyor; degerin içindeki değişiklik yalnız o kopyayı etkiliyor, maindeki sayiya hiç dokunmuyor. İşlev döndüğünde deger ortadan kalkıyor — Değişkenler, Kapsam ve Ömür dersinde ölçülen otomatik ömrün bir örneği — ve sayi hiç değişmemiş hâliyle kalıyor. Bu davranış, işlevin aldığı parametrenin türü ne olursa olsun (tam sayı, kayan nokta, yapı) aynıdır: C’de aktarım her zaman değer aktarımıdır, kaynaktaki değişkenin kendisi hiçbir zaman doğrudan işleve geçmez.

Bu kopyalama, kaç parametre olursa olsun aynı şekilde işler ve işlevin gövdesinin uzunluğundan bağımsızdır: deger adı hem çağıranın sayisinden hem de işlevin kendi gövdesinden ayrı bir depolama alanına karşılık gelir, işlev çağrıldığı anda oluşturulur ve döndüğünde ortadan kalkar. Bu, Değişkenler, Kapsam ve Ömür dersinde ölçülen otomatik ömür kuralının parametrelere de uygulandığını gösteriyor: bir parametre, tıpkı gövde içinde bildirilen sıradan bir yerel değişken gibi davranır — yalnız ilk değeri çağıran taraftan gelir. Çağıran tarafın değişkenini gerçekten değiştirmek isteyen bir kaynak yazarı, C’de bunu değeri değil, o değerin adresini geçirerek yapar; ama adresin kendisi bu derste basılmıyor ve bu konu, bir kursun tamamına ayrılmış işaretçiler konusunun parçası.

Dönüş Değerinin Tipi

Bir işlevin dönüş tipi, gövde içinde hangi ifadenin hesaplandığından bağımsız olarak, çağıranın o değeri nasıl yorumlayacağını belirler.

// donus.c — donus degerinin tipi, cagiran tarafin degeri nasil yorumladigini belirler
#include <stdio.h>

static int oranla(int pay, int payda) {
    double gercek_oran = (double)pay / (double)payda;
    return (int)(gercek_oran * 100);
}

int main(void) {
    int yuzde = oranla(1, 3);
    printf("%s %d\n", "int donus tipiyle yuzde", yuzde);
    return 0;
}
int donus tipiyle yuzde 33

oranla içinde gercek_oran bir double ve 1 / 3 tam sayı bölmesi yerine kayan nokta bölmesiyle hesaplanıyor (0.333...). Ama işlevin dönüş tipi int olarak bildirildiği için, return ifadesindeki değer — 33.333... — çağırana ulaşmadan önce inte dönüştürülüyor ve kesirli kısım atılıyor: 33. Bu dönüşüm, işlev gövdesinin neresinde hangi tipin kullanıldığına bakılmaksızın, yalnız işlevin bildirilen dönüş tipine bakılarak uygulanıyor — tıpkı bileşik atamada sonucun sol tarafın tipine zorlanması gibi, burada da sonuç işlevin imzasında yazılı tipe zorlanıyor. oranla(1, 3) ve oranla(1, 4) gibi farklı çağrılar farklı gerçek oranlar üretse de, ikisi de aynı return ifadesinden, aynı inte daraltma kuralından geçer — hangi girdiyle çağrıldığına bakılmaksızın, dönüşüm kuralının kendisi değişmez.

Bu dönüşümün yeri önemlidir: gercek_oran * 100 ifadesi hâlâ oranlanın gövdesi içinde, double genişliğinde hesaplanıyor; inte daraltma yalnız return ifadesinin değeri işlevin dışına çıkarken, tam olarak işlev sınırında uygulanıyor. Çağıran taraf (main), oranlanın içinde hangi ara tiplerin kullanıldığını hiç bilmez ve bilmesi de gerekmez — yalnız bildirimdeki dönüş tipini (int) görür ve gelen değeri o tipte bir değişkene (yuzde) atar. Bu, işlev sınırının bir tür kapsülleme sağladığını gösteriyor: gövde içindeki hesaplama hangi ara tipleri kullanırsa kullansın, dışarıya sızan tek bilgi, imzada yazılı dönüş tipidir.

Sınırlayıcı Ölçüm

Bir çağrı, çağrılan işlevin bildirimiyle uyumlu olduğu sürece güvenle derlenir ve bağlanır — yukarıdaki dogru.c örneği bunu gösteriyor.

// asama.h — cagrinin bicimini derleyiciye bildiren tam bildirim
#ifndef ASAMA_H
#define ASAMA_H

int kimlik(void);

#endif
// secim.c — tanim, bildirimle ayni imzayi tasir
#include "asama.h"

int kimlik(void) {
    return 7;
}
// dogru.c — bildirimle tanim ayni imzada oldugunda cagri guvenle baglanir
#include <stdio.h>
#include "asama.h"

int main(void) {
    printf("%s %d\n", "dogru bildirimli cagri", kimlik());
    return 0;
}
dogru bildirimli cagri 7

Bu ortamda, bildirimi hiç görünmeyen bir işlevi çağırmayı deneyen bir kaynak derleyici tarafından doğrudan reddediliyor — derleyici, bir çağrının biçimini denetleyebilmek için en azından bir bildirime ihtiyaç duyuyor ve onsuz kaynağı derlemeyi durduruyor. Ama bu, bütün uyuşmazlıkları derleyicinin yakaladığı anlamına gelmiyor. Bir bildirim, yalnız o bildirimin bulunduğu birimde denetlenir; derleyici asama.cyi derlerken secim.cnin içine hiç bakmaz, secim.cdeki gerçek tanımın bildirimle gerçekten eşleştiğini doğrulayan bir mekanizma yoktur. C’nin bağlayıcısı da sembol adına bakar, tipe bakmaz — iki birim aynı isimde ama farklı parametre tipleriyle bir işlevi bildirip tanımlasa, bağlayıcı bunları yine de birbirine bağlar. Böyle bir çağrı, derleneni ve bağlananı çalıştırdığında tanımsız davranışa düşer, çünkü çağıran taraf argümanları bir biçimde hazırlar, çağrılan taraf onları başka bir biçimde okur; bu kursun genel kuralınca böyle bir uyumsuzluk kod bloğunda denenmiyor. Ders yalnız yukarıdaki doğru bildirimli kurguyu koşturuyor: bildirim çağrının biçimini derleyiciye söyler, ama tanımla gerçekten eşleştiğini garanti eden şey, kaynak yazarının kendi tutarlılığıdır — ne derleyici ne bağlayıcı bunu birimler arasında denetler.

Bu sınırın pratik sonucu, bir başlık dosyasının neden tek bir doğruluk kaynağı olarak tutulduğudur. kimliki çağıran her birim asama.hyi dahil ettiği ve secim.c de aynı başlığı dahil ettiği sürece, bildirim ile tanım aynı kaynaktan geldiği için birbirleriyle otomatik olarak tutarlı kalır — kaynak yazarı iki farklı yerde aynı imzayı elle iki kez yazmak zorunda kalmaz. Bir birim başlığı dahil etmeyip işlevin imzasını kendi kaynağında elden yazsaydı, iki bildirim kaynakta ayrı ayrı durur ve biri değiştirildiğinde öbürünün fark edilmeden eskimesi mümkün olurdu — tam da yukarıda anlatılan riskin kaynağı. Başlık dosyasını paylaşmak, bu riski ortadan kaldırmaz ama en azından imzanın bir kez yazılmasını, dolayısıyla en az bir kaynaktan gelen tutarlılığı garanti eder.

Özet

  • Bir çağrının hangi tanıma bağlanacağı, birimler bir araya geldiğinde bağlayıcı tarafından çözülür; derleyici yalnız bildirimden imzayı denetler, gövdeyi görmez.
  • Bir bildirim yalnız imzayı (ad, parametre tipleri, dönüş tipi) taşır; bir tanım gövdeyi de içerir. Bir işlev çok kez bildirilebilir, ama tam olarak bir kez tanımlanır.
  • Parametre aktarımı her zaman değer aktarımıdır: işlev içinde yapılan bir değişiklik, çağıranın kendi değişkenini hiçbir zaman etkilemez.
  • Bir işlevin dönüş değeri, gövde içindeki ara hesaplamaların tipinden bağımsız olarak, işlevin bildirilen dönüş tipine dönüştürülür.
  • Bir bildirim ile o adın gerçek tanımı arasındaki uyum, ne derleyici ne bağlayıcı tarafından birimler arasında denetlenir; bu tutarlılığı korumak kaynak yazarının işi.
  • static bir işlev kendi birimine kapanır ve bağlayıcı tarafından başka hiçbir birime gösterilmez; işlevler için de değişkenlerdeki gizleme kuralı aynen geçerlidir.

Sonraki Adım

Bu derste ölçülen kararların hepsi, bir işlevin çağrılma biçimiyle ilgiliydi: hangi tanıma bağlandığı, argümanın nasıl aktarıldığı, dönüş değerinin nasıl yorumlandığı. Diziler ve Dizgiler dersi bu zemine, bir verinin uzunluğunun ne zaman belli olduğu sorusunu ekliyor: bir dizinin eleman sayısı kaynakta sabit bir ifadeden derlemede üretilebilirken, bir dizginin uzunluğu ancak koşumda, sonlandırıcı bayta ulaşılana kadar taranarak öğrenilebiliyor — ve bu derste ölçülen “parametre her zaman kopyadır” kuralı, bir dizi işleve geçtiğinde ilginç bir istisnaya dönüşecek: bir dizi adı işleve geçirildiğinde, kopyalanan şey dizinin kendisi değil, dizinin ilk elemanının adresi olacak — ve bu, uzunluk bilgisinin neden ayrıca taşınması gerektiğinin de kaynağı.

İ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