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 (birint, ikiint’lik birNokta) uygundur; büyük ya da pahalı kopyalanan tipler için (üçüncü derstekiTampongibi 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.tasibunu 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üzdenconst 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 == &pdoğ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.
constbir 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.