D.ershane D Programlama Dili
Ali Çehreli

çıkarsama: [deduction], derleyicinin kendiliğinden anlaması
çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
evrensel: [global], evrensel isim alanında tanımlanmış, erişimi kısıtlanmamış
kapsam: [scope], küme parantezleriyle belirlenen bir alan
mikro işlemci: [CPU], bilgisayarın beyni
program yığıtı: [program stack], belleğin kısa ömürlü değişkenler için kullanılan bölgesi
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
statik: [static], derleme zamanında belirli olan
yaşam süreci: [object lifetime], bir değişkenin veya nesnenin tanımlanmasından işinin bitmesine kadar geçen süre
yazmaç: [register], mikro işlemcinin en temel iç depolama ve işlem birimi
... bütün sözlük



İngilizce Kaynaklar

Diğer



Bellek Yönetimi

Bu bölümde çöp toplayıcıyı tanıtacağım ve ondan alınan bellek bölgelerine değişkenlerin nasıl yerleştirilebildiklerini göstereceğim.

Şimdiye kadarki programlarda hiç bellek yönetimiyle ilgilenmek zorunda kalmadık. D, programların büyük bir çoğunluğu açısından bellek yönetimi gerektirmeyen bir dildir. Burada anlatılanlara büyük olasılıkla hiç ihtiyaç duymayacaksınız. Buna rağmen D gibi sistem dillerinde alt düzey bellek işlemlerini yönetmek gerekebilir.

Önceki bazı bölümlerde olduğu gibi, aşağıda kısaca yalnızca değişken yazdığım yerlerde yapı ve sınıf nesneleri de dahil olmak üzere her türden değişkeni kastedeceğim.

Bellek

Bellek hem programın kendisini hem de kullandığı verileri barındırır. Bu yüzden diğer bilgisayar kaynaklarından daha önemlidir. Bu kaynak temelde işletim sistemine aittir. İşletim sistemi belleği programların ihtiyaçları doğrultusunda paylaştırır. Her programın kullandığı bellek, o programın belirli zamanlardaki ihtiyaçları doğrultusunda artabilir veya azalabilir. Belirli bir programın kullandığı bellek, o program sonlandığında tekrar işletim sistemine geçer.

Bellek, değişkenlerin akılda tutulmak için kullanıldığı bir defter gibi düşünülebilir. Her değişken bellekte belirli bir yere yazılır. Her değişkenin değeri gerektikçe aynı yerden okunur ve kullanılır. Yaşamı sona eren değişkenlerin yerleri daha sonradan başka değişkenler için kullanılmak üzere geri alınır.

Bellekle ilgili deneyler yaparken değişkenlerin adres değerlerini veren & işlecinden yararlanabiliriz. Örneğin, aşağıdaki programdaki iki değişkenin adresleri & işleciyle yazdırılıyor:

import std.stdio;

void main()
{
    int i;
    int j;

    writeln("i: ", &i);
    writeln("j: ", &j);
}

Not: Adresler programın her çalıştırılışında büyük olasılıkla farklı olacaktır. Ek olarak, adres değerini edinmiş olmak, normalde mikro işlemci yazmacında yaşayacak olan bir değişkenin bile bellekte yaşamasına neden olur.

Çıktısı:

i: 7FFF2B633E28
j: 7FFF2B633E2C

Adreslerin son hanelerine bakarak i'nin bellekte j'den hemen önce bulunduğunu görebiliyoruz: 8'e int'in büyüklüğü olan 4'ü eklersek on altılı sayı düzeninde C elde edilir.

Çöp toplayıcı

D programlarındaki dinamik değişkenler çöp toplayıcıya ait olan bellek bölgelerinde yaşarlar. Yaşamları sona eren değişkenler çöp toplayıcının işlettiği bir algoritma ile sonlandırılırlar. Bu değişkenlerin yerleri tekrar kullanılmak üzere geri alınır. Bu işleme aşağıda bazen çöp toplama, bazen de temizlik diyeceğim.

Çöp toplayıcının işlettiği algoritma çok kabaca şöyle açıklanabilir: Program yığıtı ile ilgili olan bütün bölgeler taranır ve o bölgelerdeki değişkenler aracılığıyla doğrudan veya dolaylı olarak erişilebilen bütün bellek bölgeleri belirlenir. Erişilebilen bölgelerin hâlâ kullanımda olduklarına karar verilir. Diğer bellek bölgelerindeki değişkenlerin sonlandırıcıları işletilir ve o bellek bölgeleri sonradan başka değişkenler için kullanılmak üzere geri alınır.

Temizlik işlemlerinin hangi sırada işletildikleri belirsizdir. Örneğin, nesnelerin referans türündeki üyeleri kendilerini barındıran nesneden daha önce sonlanmış olabilirler. Bu yüzden, referans türünden olan üyelerin sonlandırıcı işlevler içinde kullanılmaları hatalıdır. Bu, sonlanma sıralarının kesin olarak belirli olduğu C++ gibi bazı dillerden çok farklıdır.

Temizlik işlemleri, boş yerin azalmaya başlaması gibi nedenlerle ve önceden kestirilemeyecek zamanlarda işletilebilir. Temizlik işlemleri devam ederken yeni yer ayrılması çöp toplama düzeneğinde karışıklık yaratabileceğinden, programa ait olan bütün iş parçacıkları temizlik sırasında kısa süreliğine durdurulabilirler. Bu yüzden programın tutukluk yaptığı hissedilebilir.

Normalde programcının çöp toplayıcının işine karışması gerekmese de çöp toplayıcının temizlik işlemlerinin hemen işletilmeleri veya ertelenmeleri sağlanabilir.

Çöp toplayıcının olanakları core.memory modülünde tanımlanmıştır.

Temizlik işlemleri

Temizlik işlemleri programın ilerideki belirsiz bir zamanda durdurulmasına neden olmak yerine daha uygun olduğu düşünülen bir zamanda GC.collect() ile başlatılabilir:

import core.memory;

// ...

    GC.collect();        // temizlik başlatır

Bazen, programın işleyişinin hız açısından hassas olduğu noktalarda çöp toplayıcının işlemesi istenmeyebilir. GC.disable() temizlik işlemlerini erteler, GC.enable() da tekrar etkinleştirir:

    GC.disable();

// ... tutukluk hissedilmeden işlemesi gereken işlemler ...

    GC.enable();

Ancak, temizlik işlemlerinin kesinlikle işletilmeyecekleri garanti değildir: Çöp toplayıcı belleğin çok azaldığını farkettiği durumlarda boş yer bulmak için yine de işletebilir.

Çöp toplayıcı boş kalan bellek bölgelerini işletim sistemine geri vermez ve ileride oluşturulacak olan değişkenler için elinde tutmaya devam eder. Bunun bir sorun oluşturduğunun bilindiği programlarda boş bellek bölgeleri GC.minimize() ile işletim sistemine geri verilebilir:

    GC.minimize();
Değişkenler ve bellek bölgeleri

Yaşam süreçleri açısından üç çeşit değişken vardır:

Bu üç çeşit değişken farklı bellek bölgelerinde yaşarlar.

Otomatik değişkenler için program yığıtı

Bazı değişkenlerin bellekteki yerleri derleyicinin oluşturduğu kodlar tarafından program yığıtında ve otomatik olarak ayrılır. Program yığıtı mikro işlemcinin sunduğu bir olanaktır ve değişkenler için yer ayırma konusunda en hızlı düzenektir.

Aşağıdaki listedeki değişkenler otomatiktir:

Otomatik değişkenlerin bellekte bir arada bulunduklarını adres değerlerinin birbirlerine yakın olmalarına bakarak görebiliriz:

import std.stdio;

struct Yapı
{
    int üye;
}

void main()
{
    int yerel;
    Yapı yapıNesnesi;

    writeln("yerel: ", &yerel);
    writeln("nesne: ", &yapıNesnesi);
    writeln("üye  : ", &yapıNesnesi.üye);
}

Çıktısı, adreslerin birbirlerine oldukça yakın olduklarını gösteriyor:

yerel: 7FFF9FD6AFB8
nesne: 7FFF9FD6AFBC
üye  : 7FFF9FD6AFBC

Yapı nesnesinin ve tek üyesinin aynı adreste bulunduğunu da görüyorsunuz. Yapı değişkenleri için üyelerinden başka yer ayrılmasına gerek yoktur. İlk üyesinin adresi, her zaman için nesnenin de adresidir.

O değişkenlerin adres değerlerinden anlaşıldığına göre, main() çalışmaya başladığında program yığıtı kabaca 7FFF9FD6A000 adresi yakınlarındaymış.

Aynı örneği yapı yerine sınıf kullanacak biçimde değiştirelim:

import std.stdio;

class Sınıf
{
    int üye;
}

void main()
{
    int yerel;
    auto sınıfDeğişkeni = new Sınıf;

    writeln("yerel   : ", &yerel);
    writeln("değişken: ", &sınıfDeğişkeni);
    writeln("üye     : ", &sınıfDeğişkeni.üye);
}

Bu sefer yerel değişken ve sınıf değişkeninin aynı bölgede, new ile oluşturulan sınıf nesnesinin ise çok farklı bir yerde bulunduğu görülüyor:

yerel   : 7FFF2D7E6FE0
değişken: 7FFF2D7E6FE8
üye     : 7FF2C0566F30

Bu, sınıf değişkeni ile sınıf nesnesi kavramlarının farklı olmalarındandır. Sınıf değişkeni yığıtta olduğu halde, new ile ayrılmış olduğu için nesne dinamik bellek bölgesindedir.

Dinamik değişkenler için dinamik bellek bölgesi

Dinamik değişkenler new anahtar sözcüğü ile oluşturulan değişkenlerdir. new'ün aslında her türden değişken oluştururken kullanılabildiğini Göstergeler dersinde görmüştük.

new; sınıflarda oluşturulmuş olan nesneye erişim sağlayan bir sınıf değişkeni, diğer türlerde ise oluşturulmuş olan değişkene erişim sağlayan bir gösterge döndürür:

    Sınıf sınıfDeğişkeni = new Sınıf;     // sınıf değişkeni
    int * intGöstergesi = new int;        // gösterge
    Yapı * nesneGöstergesi = new Yapı;    // gösterge

Dinamik değişkenler çöp toplayıcıya aittir. Onların programcı tarafından sonlandırılmaları gerekmez. Artık kendisine hiçbir yolla erişilemeyen dinamik değişkenler çöp toplayıcı tarafından ilerideki belirsiz bir zamanda sonlandırılırlar. (Not: İstendiğinde nesneler destroy() ile de sonlandırılabilirler. Bunu aşağıda göstereceğim.)

Sonlandırılmış olan dinamik değişkenlerin yerleri de yine ilerideki belirsiz bir zamanda geri verilir.

Modülde veya belirli bir türde tek olan değişkenler için statik bellek bölgesi

Modüllerin, yapıların ve sınıfların bazı üyelerinden yalnızca bir tane bulunabilir:

Bu çeşit değişkenler static this() isimli kurucu işlevler içinde kurulurlar. Bu kurucu işlevler modüldeki diğer işlevler çalışmaya başlamadan önce işletilirler.

Bu çeşit değişkenler program çalıştığı sürece geçerliliklerini korurlar ve main'den çıkıldıktan sonra sonlandırılırlar. Eğer tanımlanmışsa, bu tür değişkenlerin sonlandırılmaları sırasında static ~this() isimli sonlandırıcı işlev işletilir:

import std.stdio;

int modüldeTek;

static this()
{
    writeln("modülün static this'i işletiliyor");
    modüldeTek = 42;
}

static ~this()
{
    writeln("modülün static ~this'i işletiliyor");
}

void main()
{
    writeln("main'e girildi");
    // ...
    writeln("main'den çıkılıyor");
}

Çıktısı:

modülün static this'i işletiliyor
main'e girildi
main'den çıkılıyor
modülün static ~this'i işletiliyor

Bütün bir yapı veya sınıf türü için tek olan değişkenler de static belirteci ile tanımlanırlar. Onların kurulmaları ve sonlandırılmaları da o türün static this ve static ~this işlevleri ile sağlanır. Bu işlevler, o türden kaç nesne oluşturulduğundan bağımsız olarak yalnızca bir kere işletilirler.

static üye örneklerini daha önce Yapılar dersinde görmüştük. Buradaki örnekte ise bir sınıf tanımlayalım.

static üyelere hem değişken ismi ile değişken.üye yazarak hem de türün ismi ile Sınıf.üye yazarak erişilebilir:

import std.stdio;

class Sınıf
{
    static int sınıftaTek;

    static this()
    {
        writeln("Sınıf'ın static this'i işletiliyor");
    }

    static ~this()
    {
        writeln("Sınıf'ın static ~this'i işletiliyor");
    }
}

void main()
{
    writeln("main'e girildi");

    auto birinci = new Sınıf;
    auto ikinci = new Sınıf;

    writeln("birinci.sınıftaTek: ", &birinci.sınıftaTek);
    writeln("ikinci.sınıftaTek : ", &ikinci.sınıftaTek);
    writeln("Sınıf.sınıftaTek  : ", &Sınıf.sınıftaTek);

    writeln("main'den çıkılıyor");
}

Çıktısı:

Sınıf'ın static this'i işletiliyor
main'e girildi
birinci.sınıftaTek: 7F5EEDF95770
ikinci.sınıftaTek : 7F5EEDF95770
Sınıf.sınıftaTek  : 7F5EEDF95770
main'den çıkılıyor
Sınıf'ın static ~this'i işletiliyor

Programda iki nesne oluşturulmuş olmasına rağmen hem onların üyesi olarak erişilen hem de sınıfın ismiyle erişilen sınıftaTek üyesinden yalnızca bir tane bulunmaktadır. Bunu her üç yolla yazdırılan adres değerinin aynı olmasından anlayabiliyoruz.

Ek olarak, static this'in ve static ~this'in nesne adedinden bağımsız olarak yalnızca bir kere işletildikleri görülüyor.

Modülde veya bir yapı veya sınıf türünde tek olan değişkenler statik bellek bölgesinde bulunurlar. Bunu yine adres değerlerinin yakın olmalarına bakarak görmeye çalışalım:

import std.stdio;

int modüldeTek;

class Sınıf
{
    int normalÜye;
    static int sınıftaTek;
}

void main()
{
    int yerel;
    auto nesne = new Sınıf;

    writeln("yerel           : ", &yerel);
    writeln("nesne.normalÜye : ", &nesne.normalÜye);
    writeln("Sınıf.sınıftaTek: ", &Sınıf.sınıftaTek);
    writeln("modüldeTek      : ", &modüldeTek);
}

Bu programın çıktısı, modülde veya bir türde tek olan iki değişkenin bellekte yan yana durduklarını gösteriyor:

yerel           : 7FFFC9C096D0
nesne.normalÜye : 7F4E5E0DFF30
Sınıf.sınıftaTek: 7F4E5E1D9774  
modüldeTek      : 7F4E5E1D9770  
Bellekten yer ayırmak

Değişkenlerin yerleştirilecekleri belleği kendimiz belirlemek isteyebiliriz. Bellekten bu amaçla yer ayırmanın çeşitli yolları vardır.

Belirli sayıda bayttan oluşan bir bellek bölgesi sabit uzunluklu bir dizi olarak ayrılabilir:

    ubyte[100] yer;                     // 100 baytlık yer

Yukarıdaki dizi 100 baytlık bellek bölgesi olarak kullanılmaya hazırdır. Bazen bu bölgenin uybte gibi bir türle ilgisi olması yerine, hiçbir türden olması istenebilir. Bunun için eleman türü olarak void seçilebilir. void türü herhangi bir değer alamadığından, böyle dizilerin = void ile ilklenmeleri şarttır.

    void[100] yer = void;               // 100 baytlık yer

D programları C kütüphanelerini çağırabildiklerinden C'nin bellek ayırmaya yarayan std.c.stdlib.malloc işlevinden de yararlanılabilir:

import std.c.stdlib;
// ...
    void * p = malloc(100);             // 100 baytlık yer

Hiçbir türden nesne ile bağıntısı olmayan bu adresin türü de void*'dir. void* türünün her türü gösterebilen gösterge türü olduğunu Göstergeler dersinde görmüştük. Böyle bir adres türünden nasıl yararlanılabildiğini aşağıda GC.calloc()'u tanıtırken göstereceğim.

malloc()'un döndürdüğü bellek bölgesi sıfırlanmış değildir.

std.c.stdlib.malloc'un bir sorunu, çöp toplayıcının onun ayırdığı bellekten haberinin olmamasıdır. Çöp toplayıcı onun ayırmış olduğu bölgenin içindeki referanslara normalde bakmaz ve o referansların eriştirdikleri nesneleri gerekenden daha erken sonlandırabilir. Çöp toplayıcının std.c.stdlib.malloc ile ayrılmış olan bellek bölgesinden haberinin olması için GC.addRange() çağrılır. Bellek std.c.stdlib.free ile geri verilirken de GC.removeRange()'in çağrılması unutulmamalıdır.

Yukarıdakilerden daha güvenli olan yöntem, belleği core.memory.GC.calloc ile ayırmaktır. calloc()'un ayırdığı bellek bölgesi önceden sıfırlanmış olduğundan calloc()'un kullanımı daha güvenlidir. Yine de bu bölgeler için de GC.addRange() ve GC.removeRange() çağırmak gerekebilir.

calloc() bellekten kaç bayt istendiğini parametre olarak alır ve ayırdığı bellek bölgesinin başlangıç adresini döndürür:

import core.memory;
// ...
    void * yer = GC.calloc(100);        // 100 baytlık yer

void* türü herhangi başka bir türün göstergesine dönüştürülebilir:

    int * intYeri = cast(int*)yer;

Çoğunlukla o ara adım atlanır ve calloc()'un döndürdüğü adres istenen türe doğrudan dönüştürülür:

    int * intYeri = cast(int*)GC.calloc(100);

Öylesine seçmiş olduğum 100 gibi hazır değerler yerine, türün uzunluğu ile kendileri için yer ayrılmakta olan nesnelerin adedi çarpılır:

    // 10 int için yer ayır
    int * yer = cast(int*)GC.calloc(int.sizeof * 10);

Sınıflarda sınıf değişkeninin uzunluğu sınıf nesnesinin uzunluğundan farklıdır. .sizeof sınıf değişkeninin uzunluğudur; sınıf nesnesinin uzunluğu ise Tür Nitelikleri dersinde gördüğümüz classInstanceSize ile öğrenilir:

    // 10 Sınıf nesnesi için yer ayır
    Sınıf * yer =
        cast(Sınıf*)GC.calloc(
            __traits(classInstanceSize, Sınıf) * 10);

Çöp toplayıcının bellek ayıran iki işlevi daha vardır: C'nin malloc()'unun eşdeğeri olan GC.malloc() ve bellek bloğu ile ilgili bilgi de döndüren GC.qalloc(). Ben bu bölümde GC.calloc()'u kullanmaya devam edeceğim.

İstenen büyüklükte bellek ayrılamadığında core.exception.OutOfMemoryError türünde bir hata atılır:

    void * yer = GC.calloc(10_000_000_000);

O kadar belleği bulunmayan bir sistemdeki çıktısı:

core.exception.OutOfMemoryError

Çöp toplayıcıdan ayrılmış olan bellek bölgeleri GC.free() ile geri verilir:

    GC.free(yer);

Ayrılmış olan bellek bölgesinde yer kalmayabilir. Böyle bir belleğin uzatılması mümkündür. GC.realloc(), daha önce çöp toplayıcıdan alınmış olan bellek göstergesini ve istenen yeni uzunluğu parametre olarak alır ve yeni uzunlukta bir yer döndürür:

    void * eskiYer = GC.calloc(100);
// ...
    void * yeniYer = GC.realloc(eskiYer, 200);

realloc() gerçekten gerekmedikçe yeni yer ayırmaz:

realloc() işlevini kullanan bir örneği biraz aşağıda göstereceğim.

Ayrılan belleğin temizlik işlemlerinin belirlenmesi

Ayrılan bölgedeki bayt değerleri tesadüfen ilgisiz başka değişkenlerin adreslerine karşılık gelebilirler. Öyle bir durumda çöp toplayıcı hâlâ kullanımda olduklarını sanacağından, aslında yaşamları sona ermiş bile olsa o başka değişkenleri sonlandırmaz.

Böyle bir belleğin gerçekten başka değişken referansları taşımadığı bilindiğinde o bölgenin taranmasını önlemek için bellek ayrılırken GC.BlkAttr.NO_SCAN belirteci kullanılır:

    int * intYeri = cast(int*)GC.calloc(100,
                                        GC.BlkAttr.NO_SCAN);

Yukarıdaki bellek bölgesine yerleştirilecek olan int değerlerinin tesadüfen başka değişkenlerin adreslerine eşit olmaları böylece artık sorun oluşturmaz.

Çöp toplayıcı, kendisinden ayrılmış olan bellekteki nesnelerin yaşam süreçlerinin artık programcının sorumluluğuna girdiğini düşünür. Bu yüzden bu bölgedeki nesnelerin sonlandırıcılarını normalde işletmez. Çöp toplayıcının sonlandırıcıları yine de işletmesi istendiğinde GC.BlkAttr.FINALIZE belirteci kullanılır:

        Sınıf * yer =
            cast(Sınıf*)GC.calloc(
                __traits(classInstanceSize, Sınıf) * 10,
                GC.BlkAttr.FINALIZE);
Göstergeyi dilime dönüştürmek

D'nin Başka Dizi Olanakları dersinde anlatmadığım bir olanağı, art arda bulunduklarından emin olunan elemanların başlangıç adresinin bir D dilimine dönüştürülebilmesidir. Böyle bir göstergeden D dilimi oluşturan söz dizimi şudur:

    elemanların_ilkini_gösteren_gösterge[0 .. eleman_adedi];

Buna göre, GC.calloc()'un art arda baytlardan oluştuğunu bildiğimiz dönüş değerini 100 bayttan oluşan bir void[] dilimine şöyle dönüştürebiliriz:

    void[] dilim = GC.calloc(100)[0..100];

Benzer biçimde, 10 adet int için yer ayıran yukarıdaki koddan 10 elemanlı bir int[] dilimi şöyle elde edilir:

    int * yer = cast(int*)GC.calloc(int.sizeof * 10);
    int[] dilim = yer[0..10];
realloc() örneği

realloc()'un kullanımını göstermek için dizi gibi işleyen çok basit bir yapı tasarlayacağım. Çok kısıtlı olan bu yapıda yalnızca eleman ekleme ve elemana erişme olanakları bulunuyor. D dizilerinde olduğu gibi bu yapının da kapasitesi var; eklenecek olan elemanlar için hazırda yer bulunduruyor. Bu yer tükendiğinde ise kapasite realloc() ile arttırılıyor:

struct Dizi(T)
{
    T * yer;          // elemanların bulunduğu yer
    size_t kapasite;  // toplam kaç elemanlık yer olduğu
    size_t uzunluk;   // eklenmiş olan eleman adedi

    ~this()
    {
        writeln("Belleği geri veriyoruz");
        GC.free(yer);
    }

    /*
     * Belirtilen numaralı elemanı döndürür
     */
    @property T eleman(size_t numara)
    {
        enforce(numara < uzunluk, "İndeks hatası");

        return *(yer + numara);
    }

    /*
     * Elemanı dizinin sonuna ekler
     */
    void ekle(T eleman)
    {
        writefln("%s numaralı eleman ekleniyor", uzunluk);

        if ((yer is null) || (uzunluk == kapasite)) {
            /* Yeni eleman için yer yok; kapasiteyi arttırmak
             * gerekiyor. */
            size_t yeniKapasite =
                kapasite + (kapasite / 2) + 1;
            kapasiteArttır(yeniKapasite);
        }

        /* Elemanı en sona yerleştiriyoruz */
        *(yer + uzunluk) = eleman;
        ++uzunluk;
    }

    void kapasiteArttır(size_t yeniKapasite)
    {
        writefln("Kapasite artıyor: %s -> %s",
                 kapasite, yeniKapasite);

        size_t bellekUzunluğu = yeniKapasite * T.sizeof;
        yer = cast(T*)GC.realloc(
            yer, bellekUzunluğu, GC.BlkAttr.NO_SCAN);

        kapasite = yeniKapasite;
    }
}

Yukarıdaki kullanımda da görüldüğü gibi, realloc()'un da üçüncü parametresi GC.BlkAttr belirteçleri içindir.

Bu dizinin kapasitesi yaklaşık olarak %50 oranında arttırılıyor; örneğin, 100 elemanlık yer tükendiğinde yeni kapasite 151 oluyor. Yeni kapasite hesaplanırken eklenen 1 değeri, başlangıç durumunda sıfır olan kapasite için özel bir işlem gerekmesini önlemek içindir. Öyle olmasaydı, sıfırın %50 fazlası da sıfır olacağından kapasite hiç artamazdı.

Bu yapıyı double türünde elemanlarla şöyle deneyebiliriz:

import std.stdio;
import core.memory;
import std.exception;

// ...

void main()
{
    auto dizi = Dizi!double();

    size_t adet = 10;

    foreach (i; 0 .. adet) {
        double elemanDeğeri = i * 1.1;
        dizi.ekle(elemanDeğeri);
    }

    writeln("Bütün elemanlar:");

    foreach (i; 0 .. adet) {
        write(dizi.eleman(i), ' ');
    }

    writeln();
}

Çıktısı:

0 numaralı eleman ekleniyor
Kapasite artıyor: 0 -> 1
1 numaralı eleman ekleniyor
Kapasite artıyor: 1 -> 2
2 numaralı eleman ekleniyor
Kapasite artıyor: 2 -> 4
3 numaralı eleman ekleniyor
4 numaralı eleman ekleniyor
Kapasite artıyor: 4 -> 7
5 numaralı eleman ekleniyor
6 numaralı eleman ekleniyor
7 numaralı eleman ekleniyor
Kapasite artıyor: 7 -> 11
8 numaralı eleman ekleniyor
9 numaralı eleman ekleniyor
Bütün elemanlar:
0 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 
Belleği geri veriyoruz
Değişkenleri belirli bir yerde kurmak

new ifadesi iki işlem gerçekleştirir:

  1. Bellekten nesnenin sığacağı kadar yer ayırır.
  2. Nesnenin kurucu işlevini çağırır.

Bu işlemlerden birincisinin GC.calloc() ve başka işlevlerle gerçekleştirilebildiğini yukarıda gördük. Bir sistem dili olan D, normalde otomatik olarak işletilen ikinci adımın da programcı tarafından belirlenmesine izin verir.

Nesnelerin belirli bir adreste kurulması için "yerleştir" anlamına gelen std.conv.emplace() kullanılır.

emplace()'in kullanımını göstermeden önce türlerin hizalama birimi kavramını anlatmam gerekiyor.

Türlerin .alignof niteliği

Değişkenlerin kurulabilecekleri adres değerleri ile ilgili bir kısıtlama vardır: Her tür, ancak belirli bir değere tam olarak bölünebilen adreslerde bulunabilir. Buna o türün hizalama birimi (alignment) denir. Örneğin, int değişkenler her zaman için dörde tam olarak bölünebilen adreslerde bulunurlar: 0, 4, 8, 12, vs.

Türlerin .alignof niteliği hizalama birimini döndürür. Aşağıdaki program bazı türlerin hizalama birimlerini yazdırıyor:

import std.stdio;
import std.conv;
import std.typetuple;

struct BoşYapı
{}

struct Yapı
{
    char c;
    double d;
}

class BoşSınıf
{}

class Sınıf
{
    char karakter;
}

void main()
{
    alias TypeTuple!(char, short, int, long,
                     double, real,
                     string, int[int], int *,
                     BoşYapı, Yapı, BoşSınıf, Sınıf) Türler;

    /* Uzunluğu, hizalama birimi, ve türün ismi */
    writeln("  Uz. Hiz.  Tür\n",
            "=================");

    foreach (Tür; Türler) {
        static if (is (Tür == class)) {
            size_t uzunluk = __traits(classInstanceSize, Tür);

        } else {
            size_t uzunluk = Tür.sizeof;
        }

        writefln("%4s%4s   %s",
                 uzunluk,
                 Tür.alignof,
                 to!dstring(Tür.stringof));
    }
}

Bu programın çıktısı farklı ortamlarda farklı olabilir:

  Uz. Hiz.  Tür
=================
   1   1   char
   2   2   short
   4   4   int
   8   8   long
   8   8   double
  16  16   real
  16   8   string
   8   8   AssociativeArray!(int,int)
   8   8   int*
   1   1   BoşYapı
  16   8   Yapı
  16   8   BoşSınıf
  17   8   Sınıf

Nesneleri belirli adreslerde kurarken hizalama birimlerine uymak gerekir. Bunun örneğini görmek için yukarıdaki 17 bayt uzunluğundaki Sınıf türünün iki nesnesinin bellekte yan yana nasıl durabileceklerine bakalım. Her ne kadar yasal bir adres olmasa da, örneği kolaylaştırmak için birinci nesnenin 0 adresinde bulunduğunu varsayalım. Bu nesneyi oluşturan baytlar 0'dan 16'ya kadar olan adreslerdedir:

     0    1           16
  +----+----+- ... -+----+- ...
  | <- birinci nesne --> |
  +----+----+- ... -+----+- ...

Bir sonraki boş yerin adresi 17 olduğu halde, o adres değeri Sınıf'ın hizalama birimi olan 8'e tam olarak bölünemediği için ikinci nesne orada kurulamaz. İkinci nesnenin 8'e tam olarak bölünebilen bir sonraki adrese, yani 24 adresine yerleştirilmesi gerekir. Aradaki kullanılmayan baytlara doldurma (padding) baytları denir:

     0    1           16   17           23   24   25           30
  +----+----+- ... -+----+----+- ... -+----+----+----+- ... -+----+- ...
  | <- birinci nesne --> | <- doldurma --> | <-- ikinci nesne --> |
  +----+----+- ... -+----+----+- ... -+----+----+----+- ... -+----+- ...

Bir nesnenin belirli bir adresten sonra yasal olarak kurulabileceği ilk adresi elde etmek için şu hesap kullanılabilir:

  (adres + hizalama_birimi - 1)
      / hizalama_birimi * hizalama_birimi

Yukarıdaki hesabın doğru olarak işlemesi için bölme işleminden kalanın gözardı edilmesi şarttır. O yüzden o hesapta tamsayı türleri kullanılır.

Aşağıda emplace()'in örneklerini gösterirken yukarıdaki hesabı uygulayan şu işlevden yararlanacağım:

T * hizalıAdres(T)(T * adayAdres)
out (sonuç)
{
    /* Döndürdüğümüz adresin T.alignof'a gerçekten de tam
     * olarak bölündüğünü denetleyelim. */
    assert((cast(size_t)sonuç % T.alignof) == 0);
}
body
{
    return cast(T*)((cast(size_t)adayAdres + T.alignof - 1)
                    / T.alignof * T.alignof);
}

Yukarıdaki işlev nesnenin türünü şablon parametresinden otomatik olarak çıkarsıyor. Onun void* adresleri ile işleyen yüklemesini de şöyle yazabiliriz:

void * hizalıAdres(T)(void * adayAdres)
{
    return hizalıAdres(cast(T*)adayAdres);
}

Bu işlev, aşağıda emplace() ile sınıf nesneleri oluştururken yararlı olacak.

Son olarak, yukarıdaki işlevden yararlanan yardımcı bir işlev daha tanımlayalım. Bu işlev, nesnenin boşluklarla birlikte kaç bayt yer tuttuğunu döndürür:

size_t boşlukluUzunluk(T)()
{
    static if (is (T == class)) {
        size_t uzunluk = __traits(classInstanceSize, T);

    } else {
        size_t uzunluk = T.sizeof;
    }

    return cast(size_t)hizalıAdres(cast(T*)uzunluk);
}
Yapı nesnelerini belirli bir yerde kurmak

emplace(), nesnenin kurulacağı adresi parametre olarak alır ve o adreste bir nesne kurar. Eğer varsa, nesnenin kurucu işlevinin parametreleri bu adresten sonra bildirilir:

import std.conv;
// ...
    emplace(adres, /* ... kurucu parametreleri ... */);

Yapı nesneleri kurarken türün ayrıca belirtilmesi gerekmez; emplace() hangi türden nesne kuracağını kendisine verilen göstergenin türünden anlar. Örneğin, aşağıdaki emplace() çağrısında öğrenciAdresi'nin türü bir Öğrenci* olduğu için emplace() o adreste bir Öğrenci nesnesi kurar:

        Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
// ...
        emplace(öğrenciAdresi, isim, numara);

Aşağıdaki program, bütün nesneler için gereken miktarda yer ayırıyor ve nesneleri o bölge içindeki hizalı adreslerde kuruyor:

import std.stdio;
import std.string;
import core.memory;
import std.conv;

struct Öğrenci
{
    string isim;
    int numara;

    string toString()
    {
        return format("%s(%s)", isim, numara);
    }
}

void main()
{
    /* Önce bu türle ilgili bilgi yazdırıyoruz. */
    writefln("Öğrenci.sizeof: %#x (%s bayt)",
             Öğrenci.sizeof, Öğrenci.sizeof);
    writefln("Öğrenci.alignof: %#x (%s bayt)",
             Öğrenci.alignof, Öğrenci.alignof);

    string[] isimler = [ "Deniz", "Pınar", "Irmak" ];
    auto toplamBayt =
        boşlukluUzunluk!Öğrenci() * isimler.length;

    /*
     * Bütün Öğrenci nesnelerine yetecek kadar yer ayırıyoruz.
     *
     * UYARI! Bu dilimin eriştirdiği nesneler henüz
     * kurulmamışlardır.
     */
    Öğrenci[] öğrenciler =
        (cast(Öğrenci*)GC.calloc(toplamBayt))
                                        [0 .. isimler.length];

    foreach (int i, isim; isimler) {
        Öğrenci * adayAdres = öğrenciler.ptr + i;
        Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
        writefln("adres %s: %s", i, öğrenciAdresi);

        auto numara = 100 + i;
        emplace(öğrenciAdresi, isim, numara);
    }

    /* Bütün elemanları kurulmuş olduğundan bir Öğrenci dilimi
     * olarak kullanmakta artık bir sakınca yoktur. */
    writeln(öğrenciler);
}

Yukarıdaki program Öğrenci türünün uzunluğunu, hizalama birimini ve her öğrencinin kurulduğu adresi de yazdırıyor:

Öğrenci.sizeof: 0x18 (24 bayt)
Öğrenci.alignof: 0x8 (8 bayt)
adres 0: 7FCF0B0F2F00
adres 1: 7FCF0B0F2F18
adres 2: 7FCF0B0F2F30
[Deniz(100), Pınar(101), Irmak(102)]
Sınıf nesnelerini belirli bir yerde kurmak

Sınıf göstergelerinin nesnenin tam türünden olması gerekmez. Örneğin, Hayvan göstergeleri Kedi nesnelerine de erişim sağlayabilirler. Bu yüzden emplace(), kuracağı nesnenin türünü göstergenin türünden anlayamaz. Asıl türün emplace()'e şablon parametresi olarak bildirilmesi gerekir.

Ek olarak, sınıf nesnelerinin kurulacağı yer void[] türünde bir dilim olarak belirtilir. Bunlara göre sınıf nesneleri kurarken şu söz dizimi kullanılır:

    Tür değişken =
        emplace!Tür(voidDilimi,
                         /* ... kurucu parametreleri ... */);

emplace(), belirtilen yerde bir nesne kurar ve o nesneye erişim sağlayan bir sınıf değişkeni döndürür.

Bunları denemek için bir Hayvan sıradüzeninden yararlanacağım. Bu sıradüzene ait olan nesneleri GC.calloc() ile ayrılmış olan bir belleğe yan yana yerleştireceğim. Alt sınıfları özellikle farklı uzunlukta seçiyorum. Böylece her nesnenin yerinin bir öncekinin uzunluğuna bağlı olarak nasıl hesaplanabileceğini göstereceğim.

interface Hayvan
{
    string şarkıSöyle();
}

class Kedi : Hayvan
{
    string şarkıSöyle()
    {
        return "miyav";
    }
}

class Papağan : Hayvan
{
    string[] sözler;

    this(string[] sözler)
    {
        this.sözler = sözler;
    }

    string şarkıSöyle()
    {
        /* std.algorithm.joiner, belirtilen aralıktaki
         * elemanları belirtilen ayraçla birleştirir. */
        return to!string(joiner(sözler, ", "));
    }
}

Nesnelerin yerleştirilecekleri bölgeyi GC.calloc() ile ayıracağım. Bu bölgedeki nesnelerin sonlandırıcılarının işletilmelerine engel olmamak için FINALIZE belirtecini de kullanıyorum:

    auto kapasite = 10_000;
    void * boşYer = GC.calloc(kapasite, GC.BlkAttr.FINALIZE);

Normalde nesneler kuruldukça o bölgenin tükenmediğinden emin olunması da gerekir. Örneği kısa tutmak için bu konuyu gözardı edeceğim; yalnızca iki nesne kuracağım ve bu iki nesnenin oraya sığacaklarını varsayacağım.

O bölgede önce bir Kedi nesnesi sonra da bir Papağan nesnesi kuracağım:

    Kedi kedi = emplace!Kedi(kediYeri);
// ...
    Papağan papağan =
        emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]);

Dikkat ederseniz Papağan'ın kurucusunun gerektirdiği parametreler nesnenin yerinden sonra belirtiliyorlar.

emplace() çağrılarının döndürdükleri değişkenler bir Hayvan dizisine eklenecekler ve daha sonra bir foreach döngüsünde kullanılacaklar:

    Hayvan[] hayvanlar;
// ...
    hayvanlar ~= kedi;
// ...
    hayvanlar ~= papağan;

    foreach (hayvan; hayvanlar) {
        writeln(hayvan.şarkıSöyle());
    }

Diğer açıklamaları programın içine yazıyorum:

import std.stdio;
import std.algorithm;
import std.conv;
import core.memory;

// ...

void main()
{
    /* Bu bir Hayvan değişkeni dizisidir; Hayvan nesnesi
     * dizisi değildir. */
    Hayvan[] hayvanlar;

    /* On bin baytın bu örnekte yeterli olduğunu
     * varsayıyorum. Normalde nesnelerin buraya gerçekten
     * sığacaklarının denetlenmesi de gerekir. */
    auto kapasite = 10_000;
    void * boşYer = GC.calloc(kapasite, GC.BlkAttr.FINALIZE);

    /* İlk önce bir Kedi nesnesi yerleştireceğiz. */
    void * kediAdayAdresi = boşYer;
    void * kediAdresi = hizalıAdres!Kedi(kediAdayAdresi);
    writeln("Kedi adresi   : ", kediAdresi);

    /* Sınıflarda emplace()'e void[] verildiği için adresten
     * dilim elde etmemiz gerekiyor. */
    size_t kediUzunluğu = __traits(classInstanceSize, Kedi);
    void[] kediYeri = kediAdresi[0..kediUzunluğu];

    /* Kedi'yi o yerde kuruyoruz ve döndürülen değişkeni
     * diziye ekliyoruz. */
    Kedi kedi = emplace!Kedi(kediYeri);
    hayvanlar ~= kedi;

    /* Papağan'ı Kedi nesnesinden bir sonraki uygun adreste
     * kuracağız. */
    void * papağanAdayAdresi = kediAdresi + kediUzunluğu;
    void * papağanAdresi =
        hizalıAdres!Papağan(papağanAdayAdresi);
    writeln("Papağan adresi: ", papağanAdresi);

    size_t papağanUzunluğu =
        __traits(classInstanceSize, Papağan);
    void[] papağanYeri = papağanAdresi[0..papağanUzunluğu];

    Papağan papağan =
        emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]);
    hayvanlar ~= papağan;

    foreach (hayvan; hayvanlar) {
        writeln(hayvan.şarkıSöyle());
    }
}

Çıktısı:

Kedi adresi   : 7F869469E000
Papağan adresi: 7F869469E018
miyav
merrba, aloo

Programın adımlarını açıkça gösterebilmek için bütün işlemleri main içinde ve belirli türlere bağlı olarak yazdım. O işlemlerin iyi yazılmış bir programda yeniNesne(T) gibi bir şablon içinde bulunacaklarını düşünebiliriz.

Nesneyi belirli bir zamanda sonlandırmak

new işlecinin tersi, sonlandırıcı işlevin işletilmesi ve nesne için ayrılmış olan belleğin çöp toplayıcı tarafından geri alınmasıdır. Bu işlemler belirsiz bir zamanda otomatik olarak işletilir.

Bazı durumlarda sonlandırıcı işlevin programcının istediği bir zamanda işletilmesi gerekebilir. Örneğin, açmış olduğu bir dosyayı sonlandırıcı işlevinde kapatan bir nesnenin sonlandırıcısının hemen işletilmesi gerekebilir.

Buradaki kullanımında "ortadan kaldır" anlamına gelen destroy(), nesnenin sonlandırıcı işlevinin hemen işletilmesini sağlar:

    destroy(nesne);

destroy yalnızca sonlandırıcı işlevi işletir. Belleğin gerçekten ne zaman geri verileceği yine de çöp toplayıcının kararına kalmıştır.

Nesneyi çalışma zamanında ismiyle kurmak

Object sınıfının factory() isimli üye işlevi türün ismini parametre olarak alır, o türden bir nesne kurar ve adresini döndürür. factory(), türün kurucusu için parametre almaz; bu yüzden türün parametresiz olarak kurulabilmesi şarttır:

module deneme;

import std.stdio;

interface Hayvan
{
    string ses();
}

class Kedi : Hayvan
{
    string ses()
    {
        return "miyav";
    }
}

class Köpek : Hayvan
{
    string ses()
    {
        return "hav";
    }
}

void main()
{
    string[] kurulacaklar = [ "Kedi", "Köpek", "Kedi" ];

    Hayvan[] hayvanlar;

    foreach (türİsmi; kurulacaklar) {
        hayvanlar ~=
            cast(Hayvan)Object.factory("deneme." ~ türİsmi);
    }

    foreach (hayvan; hayvanlar) {
        writeln(hayvan.ses());
    }
}

O programda hiç new kullanılmadığı halde üç adet Hayvan nesnesi oluşturulmuş ve hayvanlar dizisine eklenmiştir:

miyav
hav
miyav

Object.factory()'ye türün tam isminin verilmesi gerekir. O yüzden yukarıdaki tür isimleri "Kedi" ve "Köpek" gibi kısa olarak değil, modülün ismi ile birlikte "deneme.Kedi" ve "deneme.Köpek" olarak belirtiliyorlar.

factory'nin dönüş türü Object'tir; bu türün yukarıdaki cast(Hayvan) kullanımında olduğu gibi doğru türe açıkça dönüştürülmesi gerekir.

Özet