D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

bellek sızıntısı: [memory leak], artık kullanılmasa bile bellek bölgesinin geri verilmemesi
çağrı yığıtı: [call stack], belleğin kısa ömürlü değişkenler ve işlev çağrıları için kullanılan bölgesi
çalışma ortamı: [runtime], çalışma zamanında dil desteği veren ve her programa otomatik olarak eklenmiş olan program parçası
çıkarsama: [deduction, inference], derleyicinin kendiliğinden anlaması
çökme: [crash], programın hata ile sonlanması
çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
doldurma baytı: [padding byte], değişkenleri hizalamak için aralarına gelen baytlar
evrensel: [global], modül düzeyinde tanımlanmış
hizalama birimi: [alignment], bir türün değişkenlerinin bulunabileceği adres adımı
kapsam: [scope], küme parantezleriyle belirlenen bir alan
mikro işlemci: [CPU], bilgisayarın beyni
sığa: [capacity], yeni elemanlar için önceden ayrılmış olan yer
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
statik: [static], derleme zamanında belirli olan
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
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

Şimdiye kadar yazdığımız programlarda hiç bellek yönetimiyle ilgilenmek zorunda kalmadık çünkü D bellek yönetimi gerektirmeyen bir dildir. O yüzden, 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şlemleri ile ilgilenmek gerekebilir.

Bellek yönetimi çok kapsamlı bir konudur. Bu bölümde yalnızca çöp toplayıcıyı tanıyacağız, çöp toplayıcıdan nasıl bellek ayrıldığını ve belirli bellek bölgelerine değişkenlerin nasıl yerleştirildiklerini göreceğiz. Farklı bellek yönetimi yöntemlerini ve özellikle std.allocator modülünü kendiniz araştırmanızı öneririm. (std.allocator bu kitap yazıldığı sırada henüz deneysel aşamadaydı.)

Ö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 kastediyorum.

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 ihtiyaçlar doğrultusunda programlara paylaştırır. Her programın kullanmakta olduğu 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şken değerlerinin yazı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ılır.

Bellekle ilgili deneyler yaparken değişkenlerin adres değerlerini veren & işlecinden yararlanabiliriz:

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 bir 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

Adreslerdeki tek fark olan son hanelere 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: Çağrı yığıtı da dahil olmak üzere kök olarak adlandırılan bölgeler taranır. O bölgelerdeki değişkenler yoluyla doğrudan veya dolaylı olarak erişilebilen bütün bellek bölgeleri belirlenir ve program tarafından herhangi bir yolla erişilebilen bütün bölgelerin hâlâ kullanımda olduklarına karar verilir. Kullanımda olmadıkları görülen 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. Kökler; her iş parçacığının çağrı yığıtından, bütün evrensel değişkenlerden, ve GC.addRoot veya GC.addRange ile tanıtılmış olan bölgelerden oluşur.

Bazı çöp toplayıcılar kullanımda olan bütün değişkenleri bellekte yan yana dursunlar diye başka yerlere taşıyabilirler. Programın tutarlılığı bozulmasın diye de o değişkenleri gösteren bütün göstergelerin değerlerini otomatik olarak değiştirirler. (D'nin bu kitabın yazıldığı sırada kullandığı çöp toplayıcısı nesne taşıyan çeşitten değildi.)

Hangi bellek bölgelerinde gösterge bulunduğunun ve hangilerinde bulunmadığının hesabını tutan çöp toplayıcılarına hassas (precise) denir. Bunun aksine, her bellek bölgesindeki değerlerin gösterge olduklarını varsayan çöp toplayıcılarına ise korunumlu (conservative) denir. Bu kitabın yazıldığı sırada kullanılan D çöp toplayıcısının yarı korunumlu olduğunu söyleyebiliriz: yalnızca gösterge içeren bellek bölgelerini, ama o bölgelerin tamamını tarar. Bunun bir etkisi, bazı bellek bölgelerinin hiç toplanmayarak bellek sızıntısı oluşturabilmesidir. Yalancı göstergelerin neden olduğu bu durumdan kaçınmak için artık kullanılmadığı bilinen bellek bölgelerinin programcı tarafından açıkça geri verilmesi önerilir.

Temizlik işlemlerinin hangi sırada işletildikleri belirsizdir. Örneğin, nesnelerin referans türündeki (göstergeler dahil) üyeleri kendilerini barındıran nesneden daha önce sonlanmış olabilirler. Bu yüzden, yaşamları çöp toplayıcıya ait olan ve kendileri referans türünden olan üyelerin sonlandırıcı işlevler içinde kullanılmaları hatalıdır. Bu kavram sonlanma sıralarının tam olarak belirli olduğu C++ gibi bazı dillerden 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 işlem sırasında programın tutukluk yaptığı hissedilebilir.

Programcının çöp toplayıcının işine karışması çoğu durumda gerekmese de temizlik işlemlerinin hemen işletilmeleri veya ertelenmeleri gibi bazı işlemler core.memory modülünün olanakları ile sağlanabilir.

Temizlik başlatmak ve ertelemek

Programın tutukluk yapmadan çalışması gereken yerlerde temizlik işlemlerinin ertelenmesi mümkündür. 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 garantili değildir: Çöp toplayıcı belleğin çok azaldığını farkettiği durumlarda boş yer bulmak için yine de işletebilir.

Temizlik işlemleri programın tutukluk yapmasının sorun oluşturmadığının bilindiği bir zamanda programcı tarafından GC.collect() ile başlatılabilir:

import core.memory;

// ...

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

Normalde, çö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();
Bellekten yer ayırmak

Bellekten herhangi bir amaç için bellek bölgesi ayrılabilir. Böyle bir bölge örneğin üzerinde değişkenler kurmak için kullanılabilir.

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çilir ve void türü herhangi bir değer alamadığından böyle dizilerin özel olarak =void ile ilklenmeleri gerekir:

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

Bu bölümde bellek ayırmak için yalnızca core.memory modülündeki GC.calloc işlevini kullanacağız. Aynı modüldeki diğer bellek ayırma işlevlerini kendiniz araştırmak isteyebilirsiniz. Ek olarak, C standart kütüphanesinin olanaklarını içeren core.stdc.stdlib modülündeki calloc() ve diğer işlevler de kullanılabilir.

GC.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* ile gösterilen bir bölgenin hangi tür için kullanılacağı o türün göstergesine dönüştürülerek belirlenebilir:

    int * intYeri = cast(int*)yer;

Ancak, o ara adım çoğunlukla atlanır ve GC.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 kullanmak yerine örneğin türün uzunluğu ile nesne adedi çarpılabilir:

    // 25 int için yer
    int * yer = cast(int*)GC.calloc(int.sizeof * 25);

Sınıf nesnelerinin uzunluğu konusunda önemli bir fark vardır: .sizeof sınıf nesnesinin değil, sınıf değişkeninin uzunluğudur. Sınıf nesnesinin uzunluğu __traits(classInstanceSize) ile öğrenilir:

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

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

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

O kadar bellek ayrılamayan durumdaki çıktısı:

core.exception.OutOfMemoryError

Ayrılan bellek işi bittiğinde GC.free ile geri verilebilir:

    GC.free(yer);

Ancak, açıkça çağrılan free(), sonlandırıcıları işletmez. Sonlanmaları gereken nesnelerin bellek geri verilmeden önce destroy() ile teker teker sonlandırılmaları gerekir. Çöp toplayıcı struct ve class nesnelerini sonlandırma kararını verirken çeşitli etkenleri gözden geçirir. Bu yüzden, sonlandırıcının kesinlikle çağrılması gereken bir durumda en iyisi nesneyi new işleci ile kurmaktır. O zaman GC.free() sonlandırıcıyı işletir.

Daha önce çöp toplayıcıdan alınmış olan bir bellek bölgesinin uzatılması mümkündür. GC.realloc(), daha önce edinilmiş olan adres değerini ve istenen yeni uzunluğu parametre olarak alır ve yeni uzunlukta bir yer döndürür. Aşağıdaki kod önceden 100 bayt olarak ayrılmış olan bellek bölgesini 200 bayta uzatıyor:

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

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

GC.realloc C kütüphanesindeki aynı isimli işlevden gelmiştir. Görevi hem fazla çeşitli hem de fazla karmaşık olduğundan hatalı tasarlanmış bir işlev olarak kabul edilir. GC.realloc'un şaşırtıcı özelliklerinden birisi, asıl bellek GC.calloc ile ayrılmış bile olsa uzatılan bölümün sıfırlanmamasıdır. Bu yüzden, belleğin sıfırlanmasının önemli olduğu durumlarda aşağıdaki gibi bir işlevden yararlanılabilir (bellekNitelikleri parametresinin anlamını biraz aşağıda göreceğiz):

import core.memory;

/* GC.realloc gibi işler. Ondan farklı olarak, belleğin
 * uzatıldığı durumda eklenen baytları sıfırlar. */
void * boşOlarakUzat(
        void * yer,
        size_t eskiUzunluk,
        size_t yeniUzunluk,
        GC.BlkAttr bellekNitelikleri = GC.BlkAttr.NONE,
        const TypeInfo türBilgisi = null) {
    /* Asıl işi GC.realloc'a yaptırıyoruz. */
    yer = GC.realloc(yer, yeniUzunluk,
                     bellekNitelikleri, türBilgisi);

    /* Eğer varsa, yeni eklenen bölümü sıfırlıyoruz. */
    if (yeniUzunluk > eskiUzunluk) {
        import core.stdc.string;

        auto eklenenYer = yer + eskiUzunluk;
        const eklenenUzunluk = yeniUzunluk - eskiUzunluk;

        memset(eklenenYer, 0, eklenenUzunluk);
    }

    return yer;
}

core.stdc.string modülünde tanımlı olan memset() belirtilen adresteki belirtilen sayıdaki bayta belirtilen değeri atar. Örneğin, yukarıdaki çağrı eklenenYer'deki eklenenUzunluk adet baytı 0 yapar.

boşOlarakUzat() işlevini aşağıdaki bir örnekte kullanacağız.

GC.realloc ile benzer amaçla kullanılan GC.extend'in davranışı çok daha basittir çünkü yalnızca yukarıdaki ilk maddeyi uygular: Eski yerin hemen sonrası yeni uzunluğu karşılayamıyorsa hiçbir işlem yapmaz ve bu durumu 0 döndürerek bildirir.

Ayrılan belleğin temizlik işlemlerinin belirlenmesi

Çöp toplayıcı algoritmasında geçen kavramlar ve adımlar bir enum türü olan BlkAttr'ın değerleri ile her bellek bölgesi için ayrı ayrı ayarlanabilir. BlkAttr, GC.calloc ve diğer bellek ayırma işlevlerine parametre olarak gönderilebilir ve bellek bölgelerinin niteliklerini belirlemek için kullanılır. BlkAttr türünün değerleri şunlardır:

Bu değerler Bit İşlemleri bölümünde gördüğümüz işleçlerle birlikte kullanılabilecek biçimde seçilmişlerdir. Örneğin, iki değer | işleci ile aşağıdaki gibi birleştirilebilir:

    const bellekAyarları =
        GC.BlkAttr.NO_SCAN | GC.BlkAttr.NO_INTERIOR;

Doğal olarak, çöp toplayıcı yalnızca kendi ayırdığı bellek bölgelerini tanır ve temizlik işlemleri sırasında yalnızca o bölgeleri tarar. Örneğin, core.stdc.stdlib.calloc ile ayrılmış olan bellek bölgelerinden çöp toplayıcının normalde haberi olmaz.

Kendisinden alınmamış olan bir bölgenin çöp toplayıcının yönetimine geçirilmesi için GC.addRange() işlevi kullanılır. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRange()'in çağrılması gerekir.

Bazı durumlarda çöp toplayıcı kendisinden ayrılmış olan bir bölgeyi gösteren hiçbir referans bulamayabilir. Örneğin, ayrılan belleğin tek referansı bir C kütüphanesi içinde tutuluyor olabilir. Böyle bir durumda çöp toplayıcı o bölgenin kullanımda olmadığını düşünecektir.

GC.addRoot(), belirli bir bölgeyi çöp toplayıcıya tanıtır ve oradan dolaylı olarak erişilebilen bütün nesneleri de yönetmesini sağlar. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRoot() işlevinin çağrılması gerekir.

Bellek uzatma örneği

realloc()'un kullanımını göstermek için dizi gibi işleyen çok basit bir yapı tasarlayalım. Çok kısıtlı olan bu yapıda yalnızca eleman ekleme ve elemana erişme olanakları bulunsun. D dizilerinde olduğu gibi bu yapının da sığası olsun. Aşağıdaki yapı sığayı gerektikçe yukarıda tanımladığımız ve kendisi GC.realloc'tan yararlanan boşOlarakUzat() ile arttırıyor:

struct Dizi(T) {
    T * yer;          // Elemanların bulunduğu yer
    size_t sığa;      // Toplam kaç elemanlık yer olduğu
    size_t uzunluk;   // Eklenmiş olan eleman adedi

    /* Belirtilen numaralı elemanı döndürür */
    T eleman(size_t numara) {
        import std.string;
        enforce(numara < uzunluk,
                format("%s numara yasal değil", numara));

        return *(yer + numara);
    }

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

        if (uzunluk == sığa) {
            /* Yeni eleman için yer yok; sığayı arttırmak
             * gerekiyor. */
            size_t yeniSığa = sığa + (sığa / 2) + 1;
            sığaArttır(yeniSığa);
        }

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

    void sığaArttır(size_t yeniSığa) {
        writefln("Sığa artıyor: %s -> %s",
                 sığa, yeniSığa);

        const eskiUzunluk = sığa * T.sizeof;
        const yeniUzunluk = yeniSığa * T.sizeof;

        /* Bu bölgeye yerleştirilen bayt değerlerinin
         * tesadüfen başka değişkenlerin göstergeleri
         * sanılmalarını önlemek için NO_SCAN belirtecini
         * kullanıyoruz. */
        yer = cast(T*)boşOlarakUzat(
            yer, eskiUzunluk, yeniUzunluk, GC.BlkAttr.NO_SCAN);

        sığa = yeniSığa;
    }
}

Bu dizinin sığasi her seferinde yaklaşık olarak %50 oranında arttırılıyor. Örneğin, 100 elemanlık yer tükendiğinde yeni sığa 151 oluyor. (Yeni sığa hesaplanırken eklenen 1 değeri, başlangıç durumunda sıfır olan sığa için özel bir işlem gerekmesini önlemek içindir. Öyle olmasaydı, sıfırın %50 fazlası da sıfır olacağından sığa 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
Sığa artıyor: 0 -> 1
1 numaralı eleman ekleniyor
Sığa artıyor: 1 -> 2
2 numaralı eleman ekleniyor
Sığa artıyor: 2 -> 4
3 numaralı eleman ekleniyor
4 numaralı eleman ekleniyor
Sığa artıyor: 4 -> 7
5 numaralı eleman ekleniyor
6 numaralı eleman ekleniyor
7 numaralı eleman ekleniyor
Sığa 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 
Hizalama birimi

Değişkenler normalde kendi türlerine özgü bir değerin katı olan adreslerde bulunurlar. Bu değere o türün hizalama birimi denir. Örneğin, int türünün hizalama birimi 4'tür çünkü int değişkenler ancak dördün katı olan adreslerde (4, 8, 12, vs.) bulunabilirler.

Hizalama, hem mikro işlemci işlemlerinin hızlı olması için istenen hem de mikro işlemcinin nesne adresleyebilmesi için gereken bir kavramdır. Ek olarak, bazı değişkenler yalnızca kendi türlerinin hizalama birimine uyan adreslerde iseler kullanılabilirler.

Türlerin .alignof niteliği

Bir türün .alignof niteliği o türün varsayılan hizalama birimini döndürür. Ancak, sınıflarda .alignof sınıf nesnesinin değil, sınıf değişkeninin hizalama birimidir. Sınıf nesnesinin hizalama birimi için std.traits.classInstanceAlignment kullanılmalıdır.

Aşağıdaki program çeşitli türün hizalama birimini yazdırıyor.

import std.stdio;
import std.meta;
import std.traits;

struct BoşYapı {
}

struct Yapı {
    char c;
    double d;
}

class BoşSınıf {
}

class Sınıf {
    char karakter;
}

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

    writeln(" Uzunluk  Hizalama  Tür\n",
            "========================");

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

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

        writefln("%6s%9s     %s",
                 uzunluk, hizalama, Tür.stringof);
    }
}

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

 Uzunluk  Hizalama  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     int[int]
     8        8     int*
     1        1     BoşYapı
    16        8     Yapı
    16        8     BoşSınıf
    17        8     Sınıf

Biraz aşağıda nesnelerin belirli adreslerde de kurulabildiklerini göreceğiz. Bunun güvenle yapılabilmesi için hizalama birimlerinin gözetilmeleri 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'in katı olmadığından ikinci nesne orada kurulamaz. İkinci nesnenin 8'in katı olan bir sonraki adrese, yani 24 adresine yerleştirilmesi gerekir. Aradaki kullanılmayan baytlara doldurma baytları denir:

     0    1           16   17           23   24   25           30
  ┌────┬────┬─ ... ─┬────┬────┬─ ... ─┬────┬────┬────┬─ ... ─┬────┬─ ...
  │<───birinci nesne────>│ <──doldurma───> │<────ikinci nesne────>│
  └────┴────┴─ ... ─┴────┴────┴─ ... ─┴────┴────┴────┴─ ... ─┴────┴─ ...

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

    (adayAdres + hizalamaBirimi - 1)
    / hizalamaBirimi
    * hizalamaBirimi

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ğız:

T * hizalıAdres(T)(T * adayAdres) {
    import std.traits;

    static if (is (T == class)) {
        const hizalama = classInstanceAlignment!T;

    } else {
        const hizalama = T.alignof;
    }

    const sonuç = (cast(size_t)adayAdres + hizalama - 1)
                  / hizalama * hizalama;
    return cast(T*)sonuç;
}

Yukarıdaki işlev nesnenin türünü şablon parametresinden otomatik olarak çıkarsamaktadır. 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 de 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);
}
.offsetof niteliği

Hizalama üye değişkenlerle de ilgili olan bir kavramdır. Üyeleri kendi türlerinin hizalama birimlerine uydurmak için üyeler arasına da doldurma baytları yerleştirilir. Örneğin, aşağıdaki yapının büyüklüğü bekleneceği gibi 6 değil, 12'dir:

struct A {
    byte b;     // 1 bayt
    int i;      // 4 bayt
    ubyte u;    // 1 bayt
}

static assert(A.sizeof == 12);    // 1 + 4 + 1'den daha fazla

Bunun nedeni, hem int üye dördün katı olan bir adrese denk gelsin diye ondan önceye yerleştirilen, hem de bütün yapı nesnesi yapı türünün hizalama birimine uysun diye en sona yerleştirilen doldurma baytlarıdır.

.offsetof niteliği bir üyenin nesnenin başlangıç adresinden kaç bayt sonra olduğunu bildirir. Aşağıdaki işlev belirli bir türün bellekteki yerleşimini doldurma baytlarını .offsetof ile belirleyerek yazdırır:

void nesneYerleşiminiYazdır(T)()
        if (is (T == struct) || is (T == union)) {
    import std.stdio;
    import std.string;

    writefln("=== '%s' nesnelerinin yerleşimi" ~
             " (.sizeof: %s, .alignof: %s) ===",
             T.stringof, T.sizeof, T.alignof);

    /* Tek satır bilgi yazar. */
    void satırYazdır(size_t uzaklık, string bilgi) {
        writefln("%4s: %s", uzaklık, bilgi);
    }

    /* Doldurma varsa miktarını yazdırır. */
    void doldurmaBilgisiYazdır(size_t beklenenUzaklık,
                               size_t gözlemlenenUzaklık) {
        if (beklenenUzaklık < gözlemlenenUzaklık) {
            /* Gözlemlenen uzaklık beklenenden fazlaysa
             * doldurma baytı var demektir. */

            const doldurmaMiktarı =
                gözlemlenenUzaklık - beklenenUzaklık;

            satırYazdır(beklenenUzaklık,
                        format("... %s bayt DOLDURMA",
                               doldurmaMiktarı));
        }
    }

    /* Bir sonraki üyenin doldurma olmayan durumda nerede
     * olacağı bilgisini tutar. */
    size_t doldurmasızUzaklık = 0;

    /* Not: __traits(allMembers) bir türün üyelerinin
     * isimlerinden oluşan bir 'string' topluluğudur. */
    foreach (üyeİsmi; __traits(allMembers, T)) {
        mixin (format("alias üye = %s.%s;",
                      T.stringof, üyeİsmi));

        const uzaklık = üye.offsetof;
        doldurmaBilgisiYazdır(doldurmasızUzaklık, uzaklık);

        const türİsmi = typeof(üye).stringof;
        satırYazdır(uzaklık, format("%s %s", türİsmi, üyeİsmi));

        doldurmasızUzaklık = uzaklık + üye.sizeof;
    }

    doldurmaBilgisiYazdır(doldurmasızUzaklık, T.sizeof);
}

Aşağıdaki program, büyüklüğü yukarıda 12 bayt olarak bildirilen A yapısının yerleşimini yazdırır:

struct A {
    byte b;
    int i;
    ubyte u;
}

void main() {
    nesneYerleşiminiYazdır!A();
}

Programın çıktısı 6 doldurma baytının nesnenin nerelerinde olduğunu gösteriyor. Çıktıda soldaki sütun nesnenin başından olan uzaklığı göstermektedir:

=== 'A' nesnelerinin yerleşimi (.sizeof: 12, .alignof: 4) ===
   0: byte b
   1: ... 3 bayt DOLDURMA
   4: int i
   8: ubyte u
   9: ... 3 bayt DOLDURMA

Doldurma baytlarını olabildiğince azaltmanın bir yolu, üyeleri yapı içinde büyükten küçüğe doğru sıralamaktır. Örneğin, int üyeyi diğerlerinden önceye alınca yapının büyüklüğü azalır:

struct B {
    int i;    // Üye listesinin başına getirildi
    byte b;
    ubyte u;
}

void main() {
    nesneYerleşiminiYazdır!B();
}

Bu sefer yalnızca en sonda 2 doldurma baytı bulunduğundan yapının büyüklüğü 8'e inmiştir:

=== 'B' nesnelerinin yerleşimi (.sizeof: 8, .alignof: 4) ===
   0: int i
   4: byte b
   5: ubyte u
   6: ... 2 bayt DOLDURMA
align niteliği

align niteliği değişkenlerin, kullanıcı türlerinin, ve üyelerin hizalama birimlerini belirler. Parantez içinde belirtilen değer hizalama birimidir. Her tanımın hizalama birimi ayrı ayrı belirlenebilir. Örneğin, aşağıdaki tanımda S nesnelerinin hizalama birimi 2, ve özellikle i üyesinin hizalama birimi 1 olur (hizalama birimi 1, hiç doldurma baytı olmayacak demektir):

align (2)               // 'S' nesnelerinin hizalama birimi
struct S {
    byte b;
    align (1) int i;    // 'i' üyesinin hizalama birimi
    ubyte u;
}

void main() {
    nesneYerleşiminiYazdır!S();
}

int üyenin hizalama birimi 1 olduğunda onun öncesinde hiç doldurma baytına gerek kalmaz ve yapının büyüklüğü üyelerinin büyüklüğü olan 6'ya eşit olur:

=== 'S' nesnelerinin yerleşimi (.sizeof: 6, .alignof: 4) ===
   0: byte b
   1: int i
   5: ubyte u

Ancak, varsayılan hizalama birimleri gözardı edildiğinde programın hızında önemli derecede yavaşlama görülebilir. Ek olarak, yanlış hizalanmış olan değişkenler bazı mikro işlemcilerde programın çökmesine neden olabilirler.

align ile değişkenlerin hizalamaları da belirlenebilir:

    align (32) double d;    // Bu değişkenin hizalama birimi

Ancak, çöp toplayıcı new ile ayrılmış olan nesnelerin hizalama birimlerinin size_t türünün uzunluğunun bir tam katı olduğunu varsayar. Çöp toplayıcıya ait olan değişkenlerin hizalama birimlerinin buna uymaması tanımsız davranışa neden olur. Örneğin, size_t 8 bayt ise new ile ayrılmış olan nesnelerin hizalama birimleri 8'in katı olmalıdır.

Değişkenleri belirli bir yerde kurmak

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

  1. Bellekten nesnenin sığacağı kadar yer ayırır. Bu bellek bölgesi henüz hiçbir nesneyle ilgili değildir.
  2. Nesnenin türünün .init değerini o yere kopyalar ve kurucu işlevini o bellek bölgesi üzerinde işletir. Nesne ancak bu işlemden sonra o bölgeye yerleştirilmiş olur.
  3. Nesne daha sonradan sonlandırılırken kullanılmak üzere bellek bölgesi belirteçlerini ayarlar.

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 olanak verir.

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

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ğundan emplace() o adreste bir Öğrenci nesnesi kurar:

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

Yukarıdaki işlevlerden yararlanan aşağıdaki program bütün nesneleri alabilecek büyüklükte bir bölge 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" ];
    const 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 (i, isim; isimler) {
        Öğrenci * adayAdres = öğrenciler.ptr + i;
        Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
        writefln("adres %s: %s", i, öğrenciAdresi);

        const numara = 100 + i.to!int;
        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 değişkenlerinin nesnenin tam türünden olması gerekmez. Örneğin, Hayvan değişkenleri Kedi nesnelerine de erişim sağlayabilirler. Bu yüzden emplace(), kuracağı nesnenin türünü kendisine verilen göstergenin türünden anlayamaz ve asıl türün emplace()'e şablon parametresi olarak bildirilmesini gerektirir. (Not: Ek olarak, sınıf göstergesi nesnenin değil, değişkenin adresi olduğundan türün açıkça belirtilmesi nesne mi yoksa değişken mi yerleştirileceği seçimini de programcıya bırakmış olur.)

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 yararlanalım. Bu sıradüzene ait olan nesneleri GC.calloc ile ayrılmış olan bir belleğe yan yana yerleştireceğiz. Alt sınıfları özellikle farklı uzunlukta seçerek her nesnenin yerinin bir öncekinin uzunluğuna bağlı olarak nasıl hesaplanabileceğini göreceğiz.

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 sözler.joiner(", ").to!string;
    }
}

Nesnelerin yerleştirilecekleri bölgeyi GC.calloc ile ayıracağız:

    const sığa = 10_000;
    void * boşYer = GC.calloc(sığa);

Normalde, nesneler kuruldukça o bölgenin tükenmediğinden de emin olunması gerekir. Örneği kısa tutmak için bu konuyu gözardı edelim ve kurulacak olan iki nesnenin on bin bayta sığacaklarını varsayalım.

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

    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 varsayalım.
     * Normalde nesnelerin buraya gerçekten sığacaklarının da
     * denetlenmesi gerekir. */
    const sığa = 10_000;
    void * boşYer = GC.calloc(sığa);

    /* İ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ğinden adresten
     * dilim elde etmek 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 sonraki ilk 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;

    /* Nesneleri kullanıyoruz. */
    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 bulunmalarını bekleriz.

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 normalde 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(değişken);

Sonlandırıcı işlevi işlettikten sonra destroy() değişkene türünün .init değerini atar. Sınıf değişkenlerinin ilk değeri null olduğundan nesne o noktadan sonra kullanılamaz. 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.

Uyarı: Yapı göstergesiyle kullanıldığında destroy()'a göstergenin kendisi değil, gösterdiği nesne verilmelidir. Yoksa nesnenin sonlandırıcısı çağrılmaz, göstergenin kendisi null değerini alır:

import std.stdio;

struct S {
    int i;

    this(int i) {
        this.i = i;
        writefln("%s değerli nesne kuruluyor", i);
    }

    ~this() {
        writefln("%s değerli nesne sonlanıyor", i);
    }
}

void main() {
    auto g = new S(42);

    writeln("destroy()'dan önce");
    destroy(g);                        // ← YANLIŞ KULLANIM
    writeln("destroy()'dan sonra");

    writefln("g: %s", g);

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

destroy()'a gösterge verildiğinde sonlandırılan (yani, türünün .init değeri verilen) göstergenin kendisidir:

42 değerli nesne kuruluyor
destroy()'dan önce
destroy()'dan sonra    ← Bu satırdan önce nesne sonlanmamıştır
g: null                ← Onun yerine gösterge null olmuştur
main'den çıkılıyor
42 değerli nesne sonlanıyor

Bu yüzden, yapı göstergesiyle kullanıldığında destroy()'a gösterilen nesne verilmelidir:

    destroy(*g);                       // ← Doğru kullanım

Sonlandırıcı işlevin bu sefer doğru noktada işletildiğini ve göstergenin değerinin null olmadığını görüyoruz:

42 değerli nesne kuruluyor
destroy()'dan önce
42 değerli nesne sonlanıyor    ← Nesne doğru noktada sonlanmıştır
destroy()'dan sonra
g: 7FC5EB4EE200                ← Gösterge null olmamıştır
main'den çıkılıyor
0 değerli nesne sonlanıyor     ← Bir kere de S.init değeriyle

Son satır, artık S.init değerine sahip olan nesnenin sonlandırıcısı bir kez de kapsamdan çıkılırken işletilirken yazdırılmış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) {
        /* "Sözde değişken" __MODULE__, her zaman için içinde
         * bulunulan modülün ismidir ve bir string olarak
         * derleme zamanında kullanılabilir. */
        const tamİsim = __MODULE__ ~ '.' ~ türİsmi;
        writefln("%s kuruluyor", tamİsim);
        hayvanlar ~= cast(Hayvan)Object.factory(tamİsim);
    }

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

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

deneme.Kedi kuruluyor
deneme.Köpek kuruluyor
deneme.Kedi kuruluyor
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 gördüğümüz gibi doğru türe açıkça dönüştürülmesi gerekir.

Özet