İçeriğe geç
academia.sh

Ders 06 / 14

Referanslar

Bir referans parametresinin çağıranın kendi nesnesiyle çalıştığını, hiçbir kopya üretmediğini ölçer.

Yayın Güncelleme

Bu derste

  • Bir referans parametresinin çağıranın kendi nesnesiyle çalıştığını ölçer
  • Değer, referans ve const referans arasında ne zaman hangisinin seçileceğini uygular
  • const referansın geçici bir nesneye bağlanabildiğini, sıradan referansın bağlanamadığını ayırt eder
İçindekiler

Önceki ders bir nesnenin kopyalandığında kendi belleğini aldığını ölçtü. Bu ders tam tersi bir soruyu ölçer: bir işlev bir nesneyi referansla alırsa, hiç kopya üretilir mi, yoksa işlev doğrudan çağıranın kendi nesnesiyle mi çalışır? Ölçüm sınıfı kendisi: tasi(Nokta& n) işlevine p verildiğinde, işlevin içindeki n ile çağırandaki p aynı nesnedir — &p == &n doğrudur, aralarında hiçbir kopyalama adımı yoktur.

C’de bir işleve bir yapıyı “referansla” aktarmanın tek yolu bir işaretçi geçirmekti: tasi(&p), işlev içinde (*ptr).x ya da ptr->x ile erişim. Bu, çağıran tarafında & operatörünün elle yazılmasını, işlev içinde de her erişimde bir yıldızlamanın (*) ya da ok işlecinin gerekmesini zorunlu kılardı — ve ptr’nin nullptr olma ihtimali her zaman vardı. C++’ta referans bu iki sorunu birden kaldırır: çağıran tarafında tasi(p) yazılır, & gerekmez; işlev içinde n.x yazılır, yıldızlama gerekmez; ve bir referans kurulmadan bırakılamayacağı için nullptr durumu dilin kendisi tarafından engellenir.

Referans Nedir

Bir referans (reference), var olan bir nesneye verilen ikinci bir addır — yeni bir nesne değildir, yeni bir bellek adresi de almaz. Nokta& r = p; yazıldığında r, p’nin kendisidir; r üzerinden yapılan her okuma ve yazma doğrudan p’nin belleğine ulaşır. Bu, bir işaretçiden (Nokta* ptr = &p;) farklıdır: işaretçi ayrı bir nesnedir, kendi belleğine sahiptir ve p’nin adresini tutar; referans ise p’nin kendisiyle aynı şeydir, ayrı bir bellek konumu işgal etmez (derleyici çoğu zaman referansı bir işaretçi gibi koda çevirir, ama dilin kendi modelinde referans yeni bir nesne değildir).

Bu ayrımın pratik sonucu şudur: bir işaretçi ptr->x yazmadan önce geçerli bir nesneye işaret ettiğini garanti etmez, elle denetlenmesi gerekebilir (if (ptr != nullptr)). Bir referans için böyle bir denetim yoktur ve gerekmez — dil, bir referansın her zaman geçerli bir nesneye bağlı olduğunu varsayar (bu varsayımı kasıtlı olarak kıran, örneğin geçersiz bir işaretçiyi yıldızlayıp bir referansa bağlayan kod, tanımsız davranış üretir, ama bu dersin kapsamındaki hiçbir kod böyle bir şey yapmaz). Bu yüzden referans alan bir işlev imzası, işaretçi alan bir imzadan daha az savunma kodu gerektirir — parametrenin var olduğu baştan kabul edilir.

// nokta.hpp — referansla aktarim olcumu icin basit bir yapi
#ifndef NOKTA_HPP
#define NOKTA_HPP

struct Nokta {
    int x;
    int y;
};

#endif

Referansla Aktarılan Yapı

// referans-olcumu.cpp — tasi(Nokta&) cagiranin kendi nesnesiyle mi calisiyor
#include <cstdio>

#include "nokta.hpp"

void tasi(Nokta& n, Nokta* cagiranAdresi) {
    std::printf("tasi icindeki n cagiranla ayni nesne mi %s\n",
                &n == cagiranAdresi ? "evet" : "hayir");
    n.x += 10;
    n.y += 10;
}

int main() {
    Nokta p{1, 2};

    tasi(p, &p);

    std::printf("tasidan sonra p.x=%d p.y=%d\n", p.x, p.y);
    return 0;
}
tasi icindeki n cagiranla ayni nesne mi evet
tasidan sonra p.x=11 p.y=12

SK12. tasi(Nokta& n, Nokta* cagiranAdresi) argümanını referansla alır; tasi(p, &p) çağrıldığında n, p’nin kendisidir — &n == cagiranAdresi (yani &n == &p) doğrudur. n.x += 10; satırı n üzerinden yazsa da, n ile p aynı nesne olduğu için bu yazma doğrudan p.x’i değiştirir; tasi döndükten sonra p.x ve p.y zaten güncellenmiştir (11 ve 12) — main hiçbir dönüş değeri almadan, hiçbir atama yapmadan çağıranın nesnesi değişmiş olarak devam eder. Bu, bir önceki dersin kopya sınıfının tam karşıtıdır: orada b, a’dan adres olarak farklıydı; burada n, p ile aynı adrestir — hiçbir kopyalama adımı çalışmaz, çünkü referans argümanı aktarmak kopyalayan yapıcıyı hiç çağırmaz.

Değer, Referans, const Referans: Ne Zaman Hangisi

Bir işlev bir nesneyi argüman olarak alırken üç seçenek vardır, ve her biri farklı bir niyeti kodlar:

  • Değerle (Nokta n): işlev kendi kopyasıyla çalışır, çağıranın nesnesini hiçbir biçimde etkilemez. Küçük, ucuz kopyalanan tipler için (bir int, iki int’lik bir Nokta) uygundur; büyük ya da pahalı kopyalanan tipler için (üçüncü dersteki Tampon gibi kendi belleğini yöneten bir sınıf) her çağrıda gereksiz bir kopya üretir.
  • Referansla (Nokta& n): işlev çağıranın nesnesiyle doğrudan çalışır, onu değiştirebilir. tasi bunu yapar — imzası okuyana “bu işlev verdiğin nesneyi değiştirebilir” der.
  • const referansla (const Nokta& n): işlev çağıranın nesnesine kopyasız erişir ama onu değiştiremez — n.x += 10; burada bir derleme hatası olurdu. Bu, “büyük bir nesneyi ucuza oku, ama değiştirme” niyetini kodlar; önceki derslerdeki kopyalayan yapıcı ve kopyalayan atamanın argümanı de tam bu yüzden const Tampon&dı.

İmza okunarak niyet anlaşılır: void yaz(const Nokta& n) çağıranın nesnesini yalnız okur; void tasi(Nokta& n) değiştirir; void kopyala(Nokta n) hiç dokunmaz, kendi kopyasında çalışır.

Bu üç seçenek arasında “her zaman referans kullan, daha hızlı” gibi bir kısayol yoktur. int gibi küçük, tek bir makine kelimesine sığan bir tipi referansla almak genellikle hiçbir şey kazandırmaz — bir referansı geçirmek de (derleyicinin ürettiği koda göre) genellikle bir adres geçirmek anlamına gelir, ve bir adres de tipik olarak bir int ile aynı büyüklüktedir; üstelik referans alan bir işlev artık çağıranın nesnesini değiştirebileceği için, salt-okunur bir kullanım bile yanlışlıkla bir yazmaya açık hâle gelir (bu risk const int& ile kapanır, ama o zaman da kazanç kalmaz). Değerle almak küçük tipler için hem daha basit hem çoğunlukla eşit hızlıdır. Referans ve const referansın asıl kazancı, kopyalaması pahalı olan tipler için ortaya çıkar — üçüncü ve beşinci derslerdeki Tampon gibi kendi belleğini ayıran sınıflar, ya da çok sayıda üyesi olan büyük yapılar.

Referans Parametreler ve Dönüş Değerleri

Referans yalnız parametrelerde değil, dönüş tiplerinde de kullanılabilir. Nokta& ilki(Nokta dizi[]) gibi bir işlev, dizinin ilk elemanını bir kopya olarak değil, kendisi olarak döndürebilir — çağıran ilki(d) = Nokta{0, 0}; yazarak dizinin ilk elemanını doğrudan değiştirebilir, çünkü dönüş değeri d[0]‘ın kendisidir. Bu ders dönüş değeri referansı koşturmaz (bu, operator[] gibi bindirilmiş işleçlerin tipik kullanım biçimidir ve K05’te işlenir), ama parametre referanslarıyla aynı ilkeye dayanır: bir referans döndürmek, yeni bir nesne kurmadan var olan bir nesneye erişim vermektir.

Referans Argümanlar Üye İşlevlerde

Bu ilke üye işlevler için de aynı biçimde işler. Beşinci dersteki Tampon::operator= imzası Tampon& operator=(const Tampon& digeri) idi — digeri parametresi burada da çağıranın kendi nesnesidir, b = a; yazıldığında operator=’in içindeki digeri, a ile aynı adrestir. operator=’in digeri.veri_’yi okuduğu her satır, doğrudan a’nın belleğini okur; eğer digeri değerle alınsaydı (Tampon digeri olarak), her b = a; çağrısı önce bir Tampon kopyası kurar, operator= bu kopyayla çalışırdı — bu hem gereksiz bir kopyalayan yapıcı çağrısı hem gereksiz bir yıkıcı çağrısı demektir. const Tampon& seçimi burada da aynı niyeti taşır: a’yı kopyasız oku, değiştirme.

Referans Yeniden Bağlanamaz

Bir işaretçi kurulduktan sonra başka bir nesneyi göstermek üzere değiştirilebilir: ptr = &baska;. Bir referans için bu mümkün değildir — Nokta& r = p; yazıldıktan sonra r = baska; bir yeniden bağlama değildir, baskanın değerini r’nin (yani p’nin) üzerine atar. Referans kurulduğu anda hangi nesneye bağlı olacağı sabitlenir ve bu bağ referansın ömrü boyunca değişmez — bu yüzden bir referans kurulurken ilklendirilmek zorundadır, boş bir referans (işaretçinin nullptr’ı gibi) yoktur.

const Referansın Geçiciye Bağlanması

// gecici-baglama-olcumu.cpp — const referans geciciye baglanabiliyor mu
#include <cstdio>

#include "nokta.hpp"

void yaz(const Nokta& n) {
    std::printf("yaz icinde x=%d y=%d\n", n.x, n.y);
}

int main() {
    Nokta p{1, 2};
    yaz(p);

    const Nokta& r = Nokta{3, 4};
    std::printf("gecici uzerinden x=%d y=%d\n", r.x, r.y);

    yaz(Nokta{5, 6});
    return 0;
}
yaz icinde x=1 y=2
gecici uzerinden x=3 y=4
yaz icinde x=5 y=6

SK13. const Nokta& r = Nokta{3, 4}; satırı bir geçici nesneye (Nokta{3, 4}, adı olmayan, ifadenin ortasında kurulan bir değer) const bir referans bağlar. Normalde bir geçicinin ömrü onu kuran ifadenin sonunda biter, ama bir const referansa bağlandığında C++ standardı bu geçicinin ömrünü referansın ömrüne kadar uzatır — r geçerli olduğu sürece Nokta{3, 4} de geçerlidir, bu yüzden r.x ve r.y okumaları tanımsız davranış değildir. Aynı bağlama Nokta& r = Nokta{3, 4}; (const olmadan) yazılsaydı derlenmezdi: sıradan bir referans bir geçiciye bağlanamaz, çünkü geçici üzerinde yazma yapılabilir olması (r.x = 99; gibi) çağıranın hiç göremeyeceği, hemen sonra yok olacak bir nesneyi değiştirmek anlamına gelirdi — dil bunu anlamsız bularak baştan engeller. yaz(Nokta{5, 6}) çağrısı da aynı kuralı kullanır: yaz’ın parametresi const Nokta& olduğu için geçici doğrudan bağlanabilir, parametre sıradan Nokta& olsaydı bu çağrı derlenmezdi.

Referans ve Bellek Düzeni

M11/K01’de bir yapının bellek düzeni — üye sırası, hizalama ve dolgu — işlenmişti. Bir referans o dersteki anlamda “yeni bir bellek düzeni” kurmaz — Nokta& r = p; yazıldığında r’nin kendi bir sizeof değeri ya da kendi bir bellek düzeni yoktur, çünkü r yeni bir nesne değildir, p’nin başka bir adıdır. Bir referans üyesi olan bir sınıf (Nokta& referans_; gibi bir üye) bu kuralın istisnası gibi görünebilir — böyle bir üye genellikle bir işaretçi büyüklüğünde yer kaplar, çünkü derleyici onu öyle koda çevirir — ama bu bir uygulama detayıdır, dilin kendisi referansın “nerede” tutulduğunu tanımlamaz; yalnız hangi nesneye bağlı olduğunu tanımlar.

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

Bu dersin ölçtüğü ifade kendisi sınıfındadır — ama yalnız bazı durumlarda. const Nokta& alan bir parametre hem kendisi hem geçici olabilir; imzanın kendisi hangisi olduğunu söylemez, yalnız çağrı anındaki argüman belirler.

// kendisi-mi-gecici-mi.cpp — const referans parametresi hangi durumda hangisi
#include <cstdio>

#include "nokta.hpp"

void yaz(const Nokta& n) {
    std::printf("yaz icinde x=%d y=%d\n", n.x, n.y);
}

int main() {
    Nokta p{7, 8};

    yaz(p);
    yaz(Nokta{9, 10});

    std::printf("p degismedi mi %s\n", (p.x == 7 && p.y == 8) ? "evet" : "hayir");
    return 0;
}
yaz icinde x=7 y=8
yaz icinde x=9 y=10
p degismedi mi evet

SK14. yaz(p) çağrısında n, p’nin kendisidir — kendisi sınıfı; yaz(Nokta{9, 10}) çağrısında ise n, ifadenin ortasında kurulan adsız bir geçiciye bağlıdır — geçici sınıfı. İki çağrı da aynı imzayı (const Nokta&) kullanır, aynı işlevi çağırır, ama parametrenin hangi sınıfa girdiği yalnız çağrı anındaki argümanın kendisiyle belirlenir. const olması bu iki durumda da işlevin n üzerinden yazma yapmasını engeller — bu yüzden p üçüncü satırda değişmeden kalır, yaz çağrısının n kendisi mi geçici mi olduğunun p’nin durumu üzerinde hiçbir etkisi yoktur. Bir işlev imzasına bakarak “bu parametre kendisi mi” sorusuna kesin cevap vermek mümkün değildir — imza yalnız değiştirilebilir mi sorusuna (referans mı const referans mı) cevap verir; kendisi mi geçici mi sorusunun cevabı her çağrıda yeniden belirlenir.

Bu sınır, sınıf tasarlarken düşünülmesi gereken bir sonuç doğurur: bir işlevin const Nokta& parametresi kabul ettiğini görmek, o işlevin her zaman kalıcı bir nesneyle çağrılacağını garanti etmez. İşlev gövdesi, aldığı referansın ömrünün çağrı ifadesinin sonunda bitebileceğini varsaymalıdır — örneğin n’nin adresini bir yere saklayıp işlev döndükten sonra kullanmak (Nokta* p = &n; return p; gibi), n bir geçiciye bağlıysa sarkan bir işaretçi (dangling pointer) üretir. Bu ders böyle bir kodu koşturmaz; yalnız sınırı belirtir. Bir sonraki dersteki operator+’ın neden bir referans değil yeni bir nesne döndürdüğü de aynı sınırdan gelir: işlecin ürettiği toplam, işlecin kendi gövdesinde kurulan yerel bir değerdir, işlev döndükten sonra da yaşaması gerekiyorsa referansla değil, değerle döndürülmelidir.

Özet

  • Bir referans var olan bir nesneye verilen ikinci bir addır; yeni bir nesne kurmaz, yeni bir adres almaz.
  • Referansla alınan bir argüman çağıranın kendisidir — hiçbir kopyalama adımı çalışmaz, &n == &p doğrudur.
  • Değer, referans ve const referans arasındaki seçim niyeti kodlar: kopyala-ve-dokunma, değiştir, ya da kopyasız-oku-ama-değiştirme.
  • Bir referans kurulduktan sonra başka bir nesneye yeniden bağlanamaz.
  • const bir referans bir geçiciye bağlanabilir (geçicinin ömrü uzar); sıradan bir referans bağlanamaz.
  • const Nokta& alan bir parametre çağrıya göre hem kendisi hem geçici olabilir; imza tek başına bunu söylemez.

Sonraki Adım

Bu ders bir referansın kopyasız, doğrudan çağıranın nesnesiyle çalıştığını ölçtü; bir sonraki ve son ders bir işlecin (operator+) neden yeni bir geçici döndürdüğünü, ve operator+=’in neden bunun yerine *this’i referansla döndürdüğünü ölçer.

İ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