Ders 09 / 15
Nesne Benzeri Tasarım
Opak bir yapı ve yapı içi işlev işaretçileriyle kurulan nesne benzeri arayüzlerde, iki tutamacın aynı nesneyi paylaştığının ve bir metot çağrısının da bu paylaşımı değiştirmediğinin ölçümü.
Yayın Güncelleme
Bu derste
- Opak bir yapı ve erişim işlevleriyle bilgi gizleyen bir nesne arayüzü kurar
- Yapı içi işlev işaretçileriyle metot çağrısı izlenimi veren bir arayüz gerçekler
- İki tutamacın aynı nesneyi paylaştığını ve metot dispatch'inin bunu değiştirmediğini ölçer
İçindekiler
C’nin sınıfı yok, ama bir “nesne” izlenimi iki araçla kurulabiliyor: bir opak yapı
(iç alanları yalnız tanımlandığı .c dosyasında görünen bir tip) ve içine gömülü işlev
işaretçileri (bir “metot” çağrısı gibi okunan, aslında bir veri alanı olan çağrılabilir
alanlar). Bu ders ikisini de kuruyor ve aynı soruyu soruyor: bir “nesne”ye iki tutamaçtan
erişildiğinde, birinden yapılan bir değişiklik ötekinden görünüyor mu? Yanıt yine
ortak — opaklık ve metot çağrısı sözdizimi, altta yatan paylaşımı değiştirmiyor,
yalnız onu okuma biçimini değiştiriyor.
Opak Yapı: Bilgi Gizleme ile Bir Nesne Arayüzü
struct Nesne’nin tanımı yalnız nesne.c içinde; nesne.h’de yalnız bir ileri
bildirim (struct Nesne;) var. Bu, başlığı dahil eden bir dosyanın deger alanına
doğrudan erişemeyeceği, yalnız nesne_ayarla/nesne_oku işlevleri üzerinden
konuşabileceği anlamına geliyor — nesnenin “genel arayüzü” bu iki işlev.
// nesne.h — opak nesne tipi ve genel arayuzu #ifndef NESNE_H #define NESNE_H struct Nesne; struct Nesne *nesne_olustur(int baslangic); void nesne_ayarla(struct Nesne *n, int deger); int nesne_oku(const struct Nesne *n); void nesne_serbest(struct Nesne *n); #endif
// nesne.c — opak tipin gerceklemesi
#include <stdlib.h>
#include "nesne.h"
struct Nesne {
int deger;
};
struct Nesne *nesne_olustur(int baslangic) {
struct Nesne *n = malloc(sizeof *n);
if (n == NULL) {
return NULL;
}
n->deger = baslangic;
return n;
}
void nesne_ayarla(struct Nesne *n, int deger) {
n->deger = deger;
}
int nesne_oku(const struct Nesne *n) {
return n->deger;
}
void nesne_serbest(struct Nesne *n) {
free(n);
}
nesne_serbest‘in ne zaman çağrılacağı ayrı bir soru — önceki kursun (**C’de Bellek ve
İşaretçiler**, K02) sorduğu “bu nesneyi kim bırakacak” sorusu, burada da nesne_olustur’u
çağıranın üstlendiği bir sözleşme. Bu ders o soruyu değil, iki tutamacın aynı nesneyi
paylaşıp paylaşmadığını ölçüyor.
İki Tutamacın Aynı Nesneyi Paylaşması
n ve ikinci_tutamac aynı struct Nesne * değerini taşıyor — ikisi de aynı nesneye
işaret ediyor. Opak tip yalnız deger alanına doğrudan erişimi engelliyor;
nesne_ayarla/nesne_oku üzerinden erişim hâlâ aynı nesneye ulaşıyor.
// iki_tutamac.c — ayni nesneye iki tutamactan erisim
#include <stdio.h>
#include "nesne.h"
int main(void) {
struct Nesne *n = nesne_olustur(1);
struct Nesne *ikinci_tutamac = n;
printf("%s %d\n", "olusturuldugundaki deger", nesne_oku(n));
nesne_ayarla(ikinci_tutamac, 42);
printf("%s %d\n", "ikinci tutamactan ayarlanan degerin n uzerinden okunmasi", nesne_oku(n));
printf("%s %d\n", "n ile ikinci_tutamac ayni nesneyi mi gosteriyor", (int)(n == ikinci_tutamac));
nesne_serbest(n);
return 0;
}
olusturuldugundaki deger 1 ikinci tutamactan ayarlanan degerin n uzerinden okunmasi 42 n ile ikinci_tutamac ayni nesneyi mi gosteriyor 1
VY10. ikinci_tutamac = n ataması bir nesne kopyalamıyor, bir işaretçiyi
kopyalıyor — sonuçta ikisi de aynı malloc bloğunun adresini taşıyor, tıpkı bağlı
listedeki ikinci = bas->sonraki atamasında olduğu gibi (Bağlı Yapılar dersi).
nesne_ayarla(ikinci_tutamac, 42) çağrısı deger alanını değiştiriyor ve bu değişiklik
nesne_oku(n) ile okunuyor: 42. Opak tip burada hiçbir şeyi değiştirmiyor — dışarıdan
n->deger yazılamaz olması, n ile ikinci_tutamac’ın aynı nesneyi paylaştığı gerçeğini
gizlemiyor, yalnız o paylaşıma nasıl erişileceğini (yalnız işlevler üzerinden) sınırlıyor.
Son satır bunu adres basmadan doğruluyor: n == ikinci_tutamac karşılaştırması 1.
Sınıf ortak.
İşlev İşaretçileriyle Metot Çağrısı İzlenimi
İkinci yaklaşım bilgiyi gizlemiyor, “metotları” doğrudan yapının içine gömüyor: artir ve
deger_oku alanları birer işlev işaretçisi ve sayac_nesnesi_hazirla onları somut
işlevlere bağlıyor. Çağıran taraf s.artir(&s, 5) yazdığında bu, C++’taki s.artir(5)
çağrısına benzeyen bir sözdizimi üretiyor — ama artir bir metot değil, bir veri alanı.
// sayac_nesne.h — yapi ici islev gostergeleriyle metot cagrisi izlenimi
#ifndef SAYAC_NESNE_H
#define SAYAC_NESNE_H
struct SayacNesnesi {
int adet;
void (*artir)(struct SayacNesnesi *, int);
int (*deger_oku)(const struct SayacNesnesi *);
};
void sayac_nesnesi_hazirla(struct SayacNesnesi *s, int baslangic);
#endif
// sayac_nesne.c — islev gostergelerinin baglanmasi
#include "sayac_nesne.h"
static void sayac_artir(struct SayacNesnesi *s, int miktar) {
s->adet = s->adet + miktar;
}
static int sayac_deger_oku(const struct SayacNesnesi *s) {
return s->adet;
}
void sayac_nesnesi_hazirla(struct SayacNesnesi *s, int baslangic) {
s->adet = baslangic;
s->artir = sayac_artir;
s->deger_oku = sayac_deger_oku;
}
// yontem_cagrisi.c — iki tutamacin ayni yapi ici islevleri cagirmasi
#include <stdio.h>
#include "sayac_nesne.h"
int main(void) {
struct SayacNesnesi s;
sayac_nesnesi_hazirla(&s, 0);
struct SayacNesnesi *birinci_tutamac = &s;
struct SayacNesnesi *ikinci_tutamac = &s;
birinci_tutamac->artir(birinci_tutamac, 5);
printf("%s %d\n", "birinci tutamacla artirdiktan sonra ikinciden okunan",
ikinci_tutamac->deger_oku(ikinci_tutamac));
ikinci_tutamac->artir(ikinci_tutamac, 3);
printf("%s %d\n", "iki tutamacla art arda artis sonrasi deger", s.deger_oku(&s));
return 0;
}
birinci tutamacla artirdiktan sonra ikinciden okunan 5 iki tutamacla art arda artis sonrasi deger 8
VY11. birinci_tutamac ve ikinci_tutamac ikisi de &s‘i taşıyor — aynı yerel
değişkenin adresi, tıpkı bir önceki ölçümdeki n/ikinci_tutamac çiftinde olduğu gibi.
birinci_tutamac->artir(birinci_tutamac, 5) çağrısı s.adet‘i 0’dan 5’e çıkarıyor ve
ikinci_tutamac->deger_oku(ikinci_tutamac) bunu görüyor: 5. s.artir ile
s.deger_oku birer işlev işaretçisi olduğu için birinci_tutamac->artir ile
ikinci_tutamac->artir de aynı işaretçiyi okuyor — çağrılan işlev ikisinde de
sayac_artir. Metot çağrısı gibi görünen sözdizimi, s.adet’e erişimi hiç
değiştirmiyor; artir ve deger_oku sadece hangi işlevin çağrılacağını veri olarak
taşıyor. Sınıf yine ortak: iki tutamaç aynı yapıyı paylaşıyor, aradaki fark yalnız
erişim sözdiziminde.
Paylaşılan Yöntem Tablosu
SayacNesnesi’nin her örneği kendi artir/deger_oku alanlarını taşıyor — aynı
işlevlere işaret etseler de, her örnek bu iki işaretçinin kendi kopyasını saklıyor.
Alternatif bir tasarım örnekleri tek bir paylaşılan yöntem tablosuna bağlıyor: her
örnek yalnız tabloya bir işaretçi taşıyor, tablonun kendisi tüm örnekler arasında tek.
// sayac_vtable.h — paylasilan yontem tablosuyla nesne tasarimi
#ifndef SAYAC_VTABLE_H
#define SAYAC_VTABLE_H
struct SayacYonetici;
struct SayacYontemleri {
void (*artir)(struct SayacYonetici *, int);
int (*deger_oku)(const struct SayacYonetici *);
};
struct SayacYonetici {
int adet;
const struct SayacYontemleri *yontemler;
};
extern const struct SayacYontemleri SAYAC_YONTEMLERI;
void sayac_yonetici_hazirla(struct SayacYonetici *s, int baslangic);
#endif
// sayac_vtable.c — tek paylasilan yontem tablosunun tanimi
#include "sayac_vtable.h"
static void sayac_artir_v(struct SayacYonetici *s, int miktar) {
s->adet = s->adet + miktar;
}
static int sayac_deger_oku_v(const struct SayacYonetici *s) {
return s->adet;
}
const struct SayacYontemleri SAYAC_YONTEMLERI = { sayac_artir_v, sayac_deger_oku_v };
void sayac_yonetici_hazirla(struct SayacYonetici *s, int baslangic) {
s->adet = baslangic;
s->yontemler = &SAYAC_YONTEMLERI;
}
// paylasilan_tablo.c — iki ornegin adeti ayri, yontem tablosu ortak mi
#include <stdio.h>
#include "sayac_vtable.h"
int main(void) {
struct SayacYonetici birinci;
struct SayacYonetici ikinci;
sayac_yonetici_hazirla(&birinci, 0);
sayac_yonetici_hazirla(&ikinci, 100);
birinci.yontemler->artir(&birinci, 5);
ikinci.yontemler->artir(&ikinci, 5);
printf("%s %d\n", "birinci ornegin adet degeri", birinci.yontemler->deger_oku(&birinci));
printf("%s %d\n", "ikinci ornegin adet degeri", ikinci.yontemler->deger_oku(&ikinci));
printf("%s %d\n", "iki ornegin yontem tablosu ayni adresi mi gosteriyor",
(int)(birinci.yontemler == ikinci.yontemler));
return 0;
}
birinci ornegin adet degeri 5 ikinci ornegin adet degeri 105 iki ornegin yontem tablosu ayni adresi mi gosteriyor 1
VY17. birinci.adet ile ikinci.adet birbirinden bağımsız — her örnek kendi int
alanını taşıyor, biri değişince öteki etkilenmiyor. Ama birinci.yontemler ile
ikinci.yontemler aynı adresi taşıyor: &SAYAC_YONTEMLERI, dosya kapsamında tek bir
kere tanımlanmış const bir nesne. SayacNesnesi tasarımında her örnek artir ve
deger_oku alanlarının kendi kopyasını tutuyordu (aynı işleve işaret etseler de, iki
ayrı bellek konumunda); burada örnekler tablonun kendisini değil, ona giden tek bir
işaretçiyi paylaşıyor. Bir yapı içinde iki farklı sınıf aynı anda görülebiliyor: adet
alanı için örnekler birbirinden kopya gibi bağımsız, yontemler’in gösterdiği tablo
için ise ortak — tablo tek bir nesne ve tüm örnekler ona aynı adresle bağlanıyor.
Bir Kopyalama İşleviyle Bağımsız Bir Nesne Üretmek
Şimdiye kadarki iki örnek (tutamaç kopyalama, paylaşılan yöntem tablosu) hep ortak
sınıfını üretti: bir işaretçi kopyalandığında ya da paylaşıldığında, altta yatan yapı
tek kaldı. Ama nesne.h’nin genel arayüzü zaten bağımsız bir nesne üretmek için yeterli
— nesne_oku ile mevcut değeri okuyup nesne_olustur ile o değerden yeni bir nesne
kurmak, opak tipin iç alanına hiç dokunmadan bir derin kopya üretiyor.
// nesne_klon.h — opak nesnenin genel arayuzuyle bagimsiz kopyasini kuran islev #ifndef NESNE_KLON_H #define NESNE_KLON_H #include "nesne.h" struct Nesne *nesne_klonla(const struct Nesne *kaynak); #endif
// nesne_klon.c — klonlama yalniz genel arayuzu kullaniyor, ic alana dokunmuyor
#include "nesne_klon.h"
struct Nesne *nesne_klonla(const struct Nesne *kaynak) {
return nesne_olustur(nesne_oku(kaynak));
}
// nesne_klon_demo.c — klon n'den bagimsiz, n degisince klon etkilenmiyor
#include <stdio.h>
#include "nesne_klon.h"
int main(void) {
struct Nesne *n = nesne_olustur(7);
struct Nesne *klon = nesne_klonla(n);
printf("%s %d\n", "klonun baslangictaki degeri", nesne_oku(klon));
nesne_ayarla(n, 99);
printf("%s %d\n", "n degistirildikten sonra klonun degeri", nesne_oku(klon));
printf("%s %d\n", "n ile klon ayni nesneyi mi gosteriyor", (int)(n == klon));
nesne_serbest(n);
nesne_serbest(klon);
return 0;
}
klonun baslangictaki degeri 7 n degistirildikten sonra klonun degeri 7 n ile klon ayni nesneyi mi gosteriyor 0
VY19. nesne_klonla, n’nin işaret ettiği yapıya hiç dokunmuyor — yalnız
nesne_oku(kaynak) ile mevcut değeri okuyup nesne_olustur ile yeni bir malloc
bloğu kuruyor. Sonuçta klon, n’den ayrı bir adres taşıyor: son satır bunu
n == klon karşılaştırmasının 0 dönmesiyle doğruluyor. nesne_ayarla(n, 99)
çağrısı yalnız n’nin işaret ettiği bloğu değiştiriyor; klon’un işaret ettiği blok
etkilenmiyor, nesne_oku(klon) hâlâ 7 dönüyor. Bu, dersin ilk iki örneğinin tam
tersi: ikinci_tutamac = n bir işaretçiyi kopyalayıp aynı yapıyı paylaştırıyordu
(ortak), nesne_klonla ise genel arayüzü kullanarak yeni bir yapı kuruyor ve
sonucu kopya sınıfına taşıyor — iki nesne artık birbirinden bağımsız, biri
değişince öteki değişmiyor. Fark kodun biçiminde değil: ikisi de işaretçi atıyor,
ikisi de opak tipin arkasında çalışıyor; fark, sağ taraftaki ifadenin var olan bir
adresi mi taşıdığı yoksa yeni bir ayırmadan mı geldiği.
Sınırlayıcı Ölçüm
Bir “nesne” arayüzünün biçimi — opak bir tip mi, yapı içi işlev işaretçileri mi, sıradan
işlevler mi — altta yatan paylaşımı belirlemiyor. nesne_ayarla(ikinci_tutamac, 42) ile
birinci_tutamac->artir(birinci_tutamac, 5) farklı görünüyor (biri sıradan bir işlev
çağrısı, öteki yapı alanı üzerinden bir çağrı gibi okunuyor), ama ikisi de aynı ilkeye
dayanıyor: bir işaretçiyle erişilen bir yapıyı değiştirmek, o yapıya erişen her işaretçiden
görünür. Bir arayüzün “nesne benzeri” görünmesi — bilgi gizleme, metot çağrısı sözdizimi —
paylaşımı gizlemiyor ya da ortadan kaldırmıyor, yalnız paylaşılan yapıya nasıl
erişileceğini biçimlendiriyor. Paylaşımın kendisi hâlâ yalnız işaretçilerin eşitliğiyle
(n == ikinci_tutamac, birinci_tutamac == ikinci_tutamac) ölçülebiliyor.
Özet
- Bir opak tip (
struct Nesne;ileri bildirimi) iç alanlara doğrudan erişimi engelliyor, ama nesneyi işaret eden iki tutamaç arasındaki paylaşımı değiştirmiyor; yapı içine gömülü işlev işaretçileri de (artir,deger_oku) bir metot çağrısı sözdizimi veriyor ama birer veri alanı olarak kalıyor. Paylaşım yalnız işaretçi eşitliğiyle ölçülebiliyor. - İki tutamaç aynı nesneyi/yapıyı gösterdiğinde, birinden yapılan bir değişiklik ötekinden (hangi arayüz biçimi kullanılırsa kullanılsın) görünüyor — sınıf ortak.
- Bir nesneyi kim bırakacağı (K02’nin sorusu) ile bir değişikliğin kim tarafından görüleceği (bu dersin sorusu) ayrı eksenler.
- Örneklerin kendi işlev işaretçilerini taşıması ile tek bir paylaşılan tabloya işaretçiyle bağlanması farklı sonuçlar veriyor: birincisinde her örnek kendi kopyasını tutuyor, ikincisinde tablo tek ve tüm örnekler ona aynı adresle erişiyor.
- Genel arayüzü (yalnız
nesne_olustur/nesne_oku) kullanarak yeni bir nesne kurmak işaretçi kopyalamaktan farklı: sonuç kopya sınıfına giriyor, iki nesne birbirinden bağımsız kalıyor.
Sonraki Adım
Bu dersin iki örneğinde de kaynak yönetimi basitti: tek bir nesne_serbest çağrısı ya da
hiç ayırma. Bir sonraki ders, Kaynak Temizleme Deyimleri, birden fazla kaynağın (birden
fazla ayırma, birden fazla açılan tutamaç) bir işlev içinde sırayla açılıp hata durumunda
doğru sırayla kapatılması gereken durumları ölçüyor — orada sınıf gözlenmeyen: bir
kaynak, hiçbir okuyucuya ulaşmadan, hata yolunda kapatılıp kayboluyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.