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

bağlı liste: [linked list], her elemanı bir sonraki elemanı gösteren veri yapısı
bayt sırası: [endianness], veriyi oluşturan baytların bellekte sıralanma düzeni
büyük soncul: [big endian], değerin üst bitlerini oluşturan baytın bellekte önceki adreste bulunduğu işlemci mimarisi
gösterge: [pointer], bir değişkeni gösteren değişken
küçük soncul: [little endian], değerin alt bitlerini oluşturan baytın bellekte önceki adreste bulunduğu işlemci mimarisi
referans: [reference], asıl nesneye, onun takma ismi gibi erişim sağlayan program yapısı
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
yazmaç: [register], mikro işlemcinin en temel iç depolama ve işlem birimi
... bütün sözlük



İngilizce Kaynaklar


Diğer




Göstergeler

Göstergeler başka değişkenlere erişim sağlamak için kullanılırlar. Değerleri, erişim sağladıkları değişkenlerin adresleridir.

Göstergeler her türden değişkeni, nesneyi, ve hatta başka göstergeleri de gösterebilirler. Ben bu bölümde kısa olsun diye, bunların hepsinin yerine değişken sözünü kullanacağım.

Göstergeler mikro işlemcilerin en temel olanaklarındandır ve sistem programcılığının önemli bir parçasıdır.

D'nin gösterge kavramı ve kullanımı C'den geçmiştir. C öğrenenlerin anlamakta en çok zorlandıkları olanak göstergeler olduğu halde, D'de göstergelerin çok daha kolay öğrenileceğini düşünüyorum. Bunun nedeni, göstergelerin amaçlarından bazılarının D'nin başka olanakları tarafından zaten karşılanıyor olmasıdır. Bu yüzden, hem bir çok durumda gösterge kullanılması gerekmez hem de başka D olanaklarının zaten anlaşılmış olması göstergelerin anlaşılmalarını da kolaylaştırır.

Bu bölümde özellikle basit olarak seçtiğim örnekler göstergelerin kullanım amaçlarını anlatma konusunda yetersiz kalabilirler. Yazımlarını ve kullanımlarını öğrenirken bunu gözardı edebilirsiniz. En sonda vereceğim örneklerin daha anlamlı olacaklarını düşünüyorum.

Ek olarak, örneklerde basitçe gösterge diye seçtiğim isimlerin kullanışsız olduklarını aklınızda bulundurun. Kendi programlarınızda her ismi anlamlı ve açıklayıcı olarak seçmeye özen gösterin.

Referans kavramı

Göstergelere geçmeden önce göstergelerin temel amacı olan referans kavramını şimdiye kadarki bölümlerden tanıdığımız D olanakları ile kısaca hatırlayalım.

foreach'in ref değişkenleri

foreach Döngüsü bölümünde gördüğümüz gibi, döngü değişkenleri normalde elemanların kopyalarıdır:

import std.stdio;

void main() {
    int[] dizi = [ 1, 11, 111 ];

    foreach (sayı; dizi) {
        sayı = 0;     // ← kopya değişir; asıl eleman değişmez
    }

    writeln("Döngüden sonra elemanlar: ", dizi);
}

Yukarıdaki döngü içinde sıfırlanmakta olan sayı değişkeni her seferinde dizi elemanlarından birisinin kopyasıdır. Onun değiştirilmesi dizideki asıl elemanı etkilemez:

Döngüden sonra elemanlar: 1 11 111

Dizideki elemanların kendilerinin değişmeleri istendiğinde foreach değişkeni ref olarak tanımlanır:

    foreach (ref sayı; dizi) {
        sayı = 0;     // ← asıl eleman değişir
    }

sayı bu sefer dizideki asıl elemanın takma ismi gibi işlem görür ve dizideki asıl elemanlar değişir:

Döngüden sonra elemanlar: 0 0 0
ref işlev parametreleri

İşlev Parametreleri bölümünde gördüğümüz gibi, değer türünden olan işlev parametreleri normalde başka değişkenlerin kopyalarıdır:

import std.stdio;

void yarımEkle(double değer) {
    değer += 0.5;        // ← main'deki değer değişmez
}

void main() {
    double değer = 1.5;

    yarımEkle(değer);

    writeln("İşlevden sonraki değer: ", değer);
}

İşlev parametresi ref olarak tanımlanmadığından, işlev içindeki atama yalnızca işlevin yerel değişkeni olan değer'i etkiler. main'deki değer değişmez:

İşlevden sonraki değer: 1.5

İşlev parametresinin, işlevin çağrıldığı yerdeki değişkenin takma ismi olması için ref anahtar sözcüğü kullanılır:

void yarımEkle(ref double değer) {
    değer += 0.5;
}

Bu sefer main içindeki değer etkilenmiş olur:

İşlevden sonraki değer: 2
Referans türleri

D'de bazı türler referans türleridir. Bu türlerden olan değişkenler sahip olmadıkları başka değerlere erişim sağlarlar:

Referans kavramını Değerler ve Referanslar bölümünde görmüştük. Burada o bölüme dahil etmediğim sınıflar üzerinde bir örnek göstermek istiyorum:

import std.stdio;

class TükenmezKalem {
    double mürekkep;

    this() {
        mürekkep = 15;
    }

    void kullan(double miktar) {
        mürekkep -= miktar;
    }
}

void main() {
    auto kalem = new TükenmezKalem;
    auto başkaKalem = kalem;  // ← şimdi ikisi de aynı nesneye
                              //   erişim sağlarlar

    writefln("Önce : %s %s",
             kalem.mürekkep, başkaKalem.mürekkep);

    kalem.kullan(1);          // ← aynı nesne kullanılır
    başkaKalem.kullan(2);     // ← aynı nesne kullanılır

    writefln("Sonra: %s %s",
             kalem.mürekkep, başkaKalem.mürekkep);
}

Sınıflar referans türleri olduklarından, farklı sınıf değişkenleri olan kalem ve başkaKalem tek TükenmezKalem nesnesine erişim sağlamaktadır. Sonuçta, iki değişkenin kullanılması da aynı nesneyi etkiler:

Önce : 15 15
Sonra: 12 12

Bu sınıf nesnesinin ve ona erişim sağlayan iki sınıf değişkeninin bellekte şu şekilde durduklarını düşünebiliriz:

   (TükenmezKalem nesnesi)       kalem      başkaKalem
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │      mürekkep     │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘

Referans kavramı yukarıdaki şekildeki gibidir: Referanslar asıl değişkenleri gösterirler.

Programlama dillerindeki referans ve gösterge kavramları perde arkasında mikro işlemcilerin gösterme amacıyla kullanılan yazmaçları ile gerçekleştirilir.

D'nin yukarıda hatırlattığım üst düzey olanakları da perde arkasında göstergelerle gerçekleştirilmiştir. Bu yüzden hem zaten çok etkin çalışırlar hem de açıkça gösterge kullanmaya gerek bırakmazlar. Buna rağmen, başka sistem programlama dillerinde de olduğu gibi, göstergeler D programcılığında da mutlaka bilinmelidir.

Tanımlanması

D'nin gösterge söz dizimi aynı C'de olduğu gibidir. Bu, C bilen programcılar için bir kolaylık olarak görülse de, özellikle * işlecinin farklı anlamlara sahip olması C'de olduğu gibi D'de de öğrenmeyi güçleştirebilir.

Biraz aşağıda anlatacağım her türü gösterebilen gösterge dışındaki göstergeler ancak belirli türden bir değişkeni gösterebilirler. Örneğin bir int göstergesi yalnızca int türünden olan değişkenleri gösterebilir.

Bir gösterge tanımlanırken, önce hangi türden değer göstereceği sonra da bir * karakteri yazılır:

    göstereceği_tür * göstergenin_ismi;

Bir int'i gösterecek olan bir gösterge şöyle tanımlanabilir:

    int * benimGöstergem;

Böyle bir tanımda * karakterini "göstergesi" diye okuyabilirsiniz. benimGöstergem'in türü bir int*'dır; yani bir "int göstergesidir". * karakterinden önceki ve sonraki boşlukların yazılmaları isteğe bağlıdır ve aşağıdaki gibi kullanımlar da çok yaygındır:

    int* benimGöstergem;
    int *benimGöstergem;

Tek başına tür ismi olarak "int göstergesi" anlamında kullanıldığında, boşluksuz olarak int* olarak yazılması da çok yaygındır.

Göstergenin değeri ve adres alma işleci &

Göstergeler de değişkendir ve her değişkenin olduğu gibi onların da değerleri vardır. Değer atanmayan göstergelerin varsayılan değeri, hiçbir değişkene erişim sağlamama değeri olan null'dır.

Bir göstergenin hangi değişkeni gösterdiği (yani erişim sağladığı), göstergenin değer olarak o değişkenin adresini taşıması ile sağlanır. Başka bir deyişle, gösterge o adresteki değişkeni gösterir.

Şimdiye kadar readf işlevi ile çok kullandığımız & işlecini Değerler ve Referanslar bölümünden de hatırlayacaksınız. Bu işleç, önüne yazıldığı değişkenin adresini alır. Bu adres değeri, gösterge değeri olarak kullanılabilir:

    int beygirGücü = 180;
    int * benimGöstergem = &beygirGücü;

Yukarıdaki ifadede göstergenin beygirGücü'nün adresi ile ilklenmesi, benimGöstergem'in beygirGücü'nü göstermesini sağlar.

Göstergenin değeri beygirGücü'nün adresi ile aynıdır:

    writeln("beygirGücü'nün adresi   : ", &beygirGücü);
    writeln("benimGöstergem'in değeri: ", benimGöstergem);
beygirGücü'nün adresi   : 7FFF2CE73F10
benimGöstergem'in değeri: 7FFF2CE73F10

Not: Adres değeri siz denediğinizde farklı olacaktır. beygirGücü, programın işletim sisteminden aldığı daha büyük bir belleğin bir yerinde bulunur. Bu yer programın her çalıştırılışında büyük olasılıkla farklı bir adreste bulunacaktır.

Bir göstergenin değerinin erişim sağladığı değişkenin adresi olduğunu ve böylece o değişkeni gösterdiğini referanslara benzer biçimde şöyle düşünebiliriz:

  7FFF2CE73F10 adresindeki        başka bir adresteki
         beygirGücü                 benimGöstergem
───┬──────────────────┬───     ───┬────────────────┬───
   │        180       │           │  7FFF2CE73F10  │
───┴──────────────────┴───     ───┴────────│───────┴───
             ▲                             │
             │                             │
             └─────────────────────────────┘

beygirGücü'nün değeri 180, benimGöstergem'in değeri de beygirGücü'nün adresidir.

Göstergeler de değişken olduklarından, onların adreslerini de & işleci ile öğrenebiliriz:

    writeln("benimGöstergem'in adresi: ", &benimGöstergem);
benimGöstergem'in adresi: 7FFF2CE73F18

beygirGücü ile benimGöstergem'in adreslerinin arasındaki farkın bu örnekte 8 olduğuna bakarak ve beygirGücü'nün türü olan int'in büyüklüğünün 4 bayt olduğunu hatırlayarak bu iki değişkenin bellekte 4 bayt ötede bulundukları sonucunu çıkartabiliriz.

Gösterme kavramını belirtmek için kullandığım oku da kaldırırsak, bir şerit gibi soldan sağa doğru uzadığını hayal ettiğimiz belleği şimdi şöyle düşünebiliriz:

    7FFF2CE73F10     7FFF2CE73F14     7FFF2CE73F18
    :                :                :                :
 ───┬────────────────┬────────────────┬────────────────┬───
    │      180       │     (boş)      │  7FFF2CE73F10  │
 ───┴────────────────┴────────────────┴────────────────┴───

Kaynak kodda geçen değişken ismi, işlev ismi, anahtar sözcük, vs. gibi isimler D gibi derlemeli diller ile oluşturulan programların içinde bulunmazlar. Örneğin, programcının isim vererek tanımladığı ve kullandığı değişkenler program içinde mikro işlemcinin anladığı adreslere ve değerlere dönüşürler.

Not: Programda kullanılan isimler hata ayıklayıcıda yararlanılmak üzere programın debug halinde de bulunurlar ama o isimlerin programın işleyişiyle ilgileri yoktur.

Erişim işleci *

Çarpma işleminden tanıdığımız * karakterinin gösterge tanımlarken tür isminden sonra yazıldığını yukarıda gördük. Göstergeleri öğrenirken karşılaşılan bir güçlük, bu karakterin göstergenin gösterdiği değişkene erişmek için de kullanılmasıdır.

Bir göstergenin isminden önce yazıldığında, göstergenin erişim sağladığı değer anlamına gelir:

    writeln("Gösterdiği değer: ", *benimGöstergem);
Gösterdiği değer: 180
Gösterdiğinin üyesine erişim için . (nokta) işleci

Not: Eğer göstergeleri C'den tanıyorsanız, bu işleç C'deki -> işleci ile aynıdır.

* işlecinin gösterilen değişkene erişim için kullanıldığını gördük. Bu, temel türleri gösteren göstergeler için yeterli derecede kullanışlıdır: *benimGöstergem yazılarak gösterilen değere kolayca erişilir.

Gösterilen değişken yapı veya sınıf nesnesi olduğunda ise bu yazım sıkıntılı hale gelir. Örnek olarak x ve y üyeleri ile iki boyutlu düzlemdeki bir noktayı ifade eden bir yapıya bakalım:

struct Konum {
    int x;
    int y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}

O türden bir değişkeni gösteren bir göstergeyi aşağıdaki gibi tanımlayabiliriz ve gösterdiğine erişebiliriz:

    auto merkez = Konum(0, 0);
    Konum * gösterge = &merkez;     // tanım
    writeln(*gösterge);             // erişim

toString işlevi tanımlanmış olduğundan, o kullanım Konum nesnesini yazdırmak için yeterlidir:

(0,0)

Ancak, gösterilen nesnenin bir üyesine erişmek için * işleci kullanıldığında kod karmaşıklaşır:

    // 10 birim sağa ötele
    (*gösterge).x += 10;

O ifade merkez nesnesinin x üyesinin değerini değiştirmektedir. Bunu şu adımlarla açıklayabiliriz:

Gösterilen nesnenin üyesine erişim böyle karışık bir şekilde yazılmak zorunda kalınmasın diye, . (nokta) işleci göstergenin kendisine uygulanır ama gösterdiğinin üyesine erişim sağlar. Yukarıdaki ifadeyi çok daha kısa olarak şöyle yazabiliriz:

    gösterge.x += 10;

Daha basit olan gösterge.x ifadesi yine merkez'in x üyesine eriştirmiştir:

(10,0)

Bunun sınıflardaki kullanımla aynı olduğuna dikkat edin. Bir sınıf değişkenine doğrudan uygulanan . (nokta) işleci aslında sınıf nesnesinin üyesine erişim sağlar:

class SınıfTürü {
    int üye;
}

// ...

    // Solda değişken, sağda nesne
    SınıfTürü değişken = new SınıfTürü;

    // Değişkene uygulanır ama nesnenin üyesine erişir
    değişken.üye = 42;

Sınıflar bölümünden hatırlayacağınız gibi, yukarıdaki koddaki nesne, new ile sağda isimsiz olarak oluşturulur. değişken, o nesneye erişim sağlayan bir sınıf değişkenidir. Değişkene uygulanan . (nokta) işleci aslında asıl nesnenin üyesine erişim sağlar.

Aynı durumun göstergelerde de bulunması sınıf değişkenleri ile göstergelerin temelde benzer biçimde gerçekleştirildiklerini ortaya koyar.

Bu kullanımın hem sınıflarda hem de göstergelerde bir istisnası vardır. . (nokta) işleciyle erişilen .sizeof gibi tür nitelikleri türün kendisine uygulanır, nesneye değil:

    char c;
    char * g;

    writeln(g.sizeof);  // göstergenin uzunluğu, char'ın değil
8
Gösterge değerinin değiştirilmesi

Göstergelerin değerleri arttırılabilir ve azaltılabilir, ve göstergeler toplama ve çıkarma işlemlerinde kullanılabilir:

    ++birGösterge;
    --birGösterge;
    birGösterge += 2;
    birGösterge -= 2;
    writeln(birGösterge + 3);
    writeln(birGösterge - 3);

Aritmetik işlemlerden alıştığımızdan farklı olarak, bu işlemler göstergenin değerini belirtilen miktar kadar değiştirmezler. Göstergenin değeri, belirtilen miktar kadar sonraki (veya önceki) değişkeni gösterecek biçimde değişir.

Örneğin, göstergenin değerinin ++ işleciyle arttırılması o göstergenin bellekte bir sonra bulunan değişkeni göstermesini sağlar:

    ++birGösterge;  // daha önce gösterdiğinden bir sonraki
                    // değişkeni göstermeye başlar

Bunun sağlanabilmesi için göstergenin değerinin türün büyüklüğü kadar arttırılması gerekir. Örneğin, int'in büyüklüğü 4 olduğundan int* türündeki bir göstergenin değeri ++ işlemi sonucunda 4 artar.

Uyarı: Göstergelerin programa ait olmayan adresleri göstermeleri tanımsız davranıştır. Erişmek için kullanılmasa bile, bir göstergenin var olmayan bir değişkeni göstermesi hatalıdır. (Not: Bunun tek istisnası, bir dizinin sonuncu elemanından sonraki hayali elemanın gösterilebilmesidir. Bunu aşağıda açıklıyorum.)

Örneğin, yukarıda tek int olarak tanımlanmış olan beygirGücü değişkenini gösteren göstergenin arttırılması yasal değildir:

    ++benimGöstergem;       // ← tanımsız davranış

Tanımsız davranış, o işlemin sonucunda ne olacağının belirsiz olması anlamına gelir. O işlem sonucunda programın çökeceği sistemler bulunabilir. Modern bilgisayarlardaki mikro işlemcilerde ise göstergenin değeri büyük olasılıkla 4 sonraki bellek adresine sahip olacak ve gösterge yukarıda "(boş)" olarak işaretlenmiş olan alanı gösterecektir.

O yüzden, göstergelerin değerlerinin arttırılması veya azaltılması ancak yan yana bulunduklarından emin olunan değişkenler gösterildiğinde kullanılmalıdır. Diziler (ve dizgiler) bu tanıma uyarlar: Bir dizinin elemanları bellekte yan yanadır (yani art ardadır).

Dizi elemanını gösteren bir göstergenin değerinin ++ işleci ile artırılması onun bir sonraki elemanı göstermesini sağlar:

import std.stdio;
import std.string;
import std.conv;

enum Renk { kırmızı, sarı, mavi }

struct KurşunKalem {
    Renk renk;
    double uzunluk;

    string toString() const {
        return format("%s santimlik %s bir kalem",
                      uzunluk, renk);
    }
}

void main() {
    writeln("KurşunKalem nesnelerinin büyüklüğü: ",
            KurşunKalem.sizeof, " bayt");

    KurşunKalem[] kalemler = [ KurşunKalem(Renk.kırmızı, 11),
                               KurşunKalem(Renk.sarı, 12),
                               KurşunKalem(Renk.mavi, 13) ];

    KurşunKalem * gösterge = &kalemler[0];            // (1)

    for (int i = 0; i != kalemler.length; ++i) {
        writeln("gösterge değeri: ", gösterge);       // (2)

        writeln("kalem: ", *gösterge);                // (3)
        ++gösterge;                                   // (4)
    }
}
  1. Tanımlanması: Dizinin ilk elemanının adresi ile ilklenmektedir
  2. Değerinin kullanılması: Değeri, gösterdiği elemanın adresidir
  3. Gösterdiği nesneye erişim
  4. Bir sonraki nesneyi göstermesi

Çıktısı:

KurşunKalem nesnelerinin büyüklüğü: 12 bayt
gösterge değeri: 114FC0
kalem: 11 santimlik kırmızı bir kalem
gösterge değeri: 114FCC
kalem: 12 santimlik sarı bir kalem
gösterge değeri: 114FD8
kalem: 13 santimlik mavi bir kalem

Dikkat ederseniz, yukarıdaki döngü kalemler.length kere tekrarlanmakta ve o yüzden gösterge hep var olan bir elemanı göstermektedir.

Göstergeler risklidir

Göstergelerin doğru olarak kullanılıp kullanılmadıkları konusunda denetim sağlanamaz. Ne derleyici, ne de çalışma zamanındaki denetimler bunu garantileyebilirler. Bir göstergenin değerinin her zaman için geçerli olması programcının sorumluluğundadır.

O yüzden, göstergeleri kullanmayı düşünmeden önce D'nin üst düzey ve güvenli olanaklarının yeterli olup olmadıklarına bakmanızı öneririm.

Dizinin son elemanından bir sonrası

Dizinin sonuncu elemanından hemen sonraki hayali elemanın gösterilmesi yasaldır.

Bu, dilimlerden alışık olduğumuz aralık kavramına benzeyen yöntemlerde kullanışlıdır. Hatırlarsanız, dilim aralıklarının ikinci indeksi işlem yapılacak olan elemanlardan bir sonrasını gösterir:

    int[] sayılar = [ 0, 1, 2, 3 ];
    writeln(sayılar[1 .. 3]);   // 1 ve 2 dahil, 3 hariç

Bu yöntem göstergelerle de kullanılabilir. Başlangıç göstergesinin ilk elemanı göstermesi ve bitiş göstergesinin son elemandan sonraki elemanı göstermesi yaygın bir işlev tasarımıdır.

Bunu bir işlevin parametrelerinde görelim:

import std.stdio;

// Kendisine verilen aralıktaki değerleri 10 katına çıkartır
void onKatı(int * baş, int * son) {
    while (baş != son) {
        *baş *= 10;
        ++baş;
    }
}

void main() {
    int[] sayılar = [ 0, 1, 2, 3 ];
    int * baş = &sayılar[1];  // ikinci elemanın adresi
    onKatı(baş, baş + 2);     // ondan iki sonrakinin adresi
    writeln(sayılar);
}

baş + 2 değeri, baş'ın gösterdiğinden 2 sonraki elemanın, yani indeksi 3 olan elemanın adresi anlamına gelir.

Yukarıdaki onKatı işlevi, iki gösterge almaktadır; bunlardan ilkinin gösterdiği int'i kullanmakta ama ikincisinin gösterdiği int'e hiçbir zaman erişmemektedir. İkinci göstergeyi, işlem yapacağı int'lerin dışını belirten bir değer olarak kullanmaktadır. son'un gösterdiği elemanı kullanmadığı için de dizinin yalnızca 1 ve 2 numaralı indeksli elemanları değişmiştir:

0 10 20 3

Yukarıdaki gibi işlevler for döngüleri ile de gerçekleştirilebilir:

    for ( ; baş != son; ++baş) {
        *baş *= 10;
    }

Dikkat ederseniz, for döngüsünün hazırlık bölümü boş bırakılmıştır. Bu işlev yeni bir gösterge kullanmak yerine doğrudan baş parametresini arttırmaktadır.

Aralık bildiren çift göstergeler foreach deyimi ile de uyumlu olarak kullanılabilir:

    foreach (gösterge; baş .. son) {
        *gösterge *= 10;
    }

Bu gibi bir yöntemde bir dizinin elemanlarının hepsinin birden kullanılabilmesi için ikinci göstergenin dizinin sonuncu elemanından bir sonrayı göstermesi gerekir:

    // ikinci gösterge dizinin sonuncu elemanından sonraki
    // hayali bir elemanı gösteriyor:
    onKatı(baş, baş + sayılar.length);

Dizilerin son elemanlarından sonraki aslında var olmayan bir elemanın gösterilmesi işte bu yüzden yasaldır.

Dizi erişim işleci [] ile kullanımı

D'de hiç gerekmese de göstergeler bir dizinin elemanlarına erişir gibi de kullanılabilirler:

    double[] kesirliler = [ 0.0, 1.1, 2.2, 3.3, 4.4 ];

    double * gösterge = &kesirliler[2];

    *gösterge = -100;          // gösterdiğine erişim
    gösterge[1] = -200;        // dizi gibi erişim

    writeln(kesirliler);

Çıktısı:

0 1.1 -100 -200 4.4

Böyle bir kullanımda göstergenin göstermekte olduğu değişken sanki bir dilimin ilk elemanıymış gibi düşünülür ve [] işleci o hayali dilimin belirtilen elemanına erişim sağlar. Yukarıdaki programdaki gösterge, kesirliler dizisinin 2 indeksli elemanını göstermektedir. gösterge[1] kullanımı, sanki hayali bir dilim varmış gibi o dilimin 1 indeksli elemanına, yani asıl dizinin 3 indeksli elemanına erişim sağlar.

Karışık görünse de bu kullanımın temelinde çok basit bir dönüşüm yatar. Derleyici gösterge[indeks] gibi bir yazımı perde arkasında *(gösterge + indeks) ifadesine dönüştürür:

    gösterge[1] = -200;      // dizi gibi erişim
    *(gösterge + 1) = -200;  // üsttekiyle aynı elemana erişim

Yukarıda da belirttiğim gibi, bu kullanımın geçerli bir değişkeni gösterip göstermediği denetlenemez. Güvenli olabilmesi için bunun yerine dilim kullanılmalıdır:

    double[] dilim = kesirliler[2 .. 4];
    dilim[0] = -100;
    dilim[1] = -200;

O dilimin yalnızca iki elemanı bulunduğuna dikkat edin. Dilim, asıl dizinin 2 ve 3 indeksli elemanlarına erişim sağlamaktadır. İndeksi 4 olan eleman dilimin dışındadır.

Dilimler güvenlidir; eleman erişimi hataları çalışma zamanında yakalanır:

    dilim[2] = -300;   // HATA: dilimin dışına erişim

Dilimin 2 indeksli elemanı bulunmadığından bir hata atılır ve böylece programın yanlış sonuçlarla devam etmesi önlenmiş olur:

core.exception.RangeError@deneme(8391): Range violation
Göstergeden dilim elde etmek

Dizi erişim işleciyle sorunsuz olarak kullanılabiliyor olmaları göstergelerin dilimlerle eşdeğer oldukları düşüncesini doğurabilir ancak bu doğru değildir. Göstergeler hem dilimlerin aksine eleman adedini bilmezler hem de aslında tek değişken gösterebildiklerinden dilimler kadar kullanışlı ve güvenli değillerdir.

Buna rağmen, art arda kaç eleman bulunduğunun bilindiği durumlarda göstergelerden dilim oluşturulabilir. Böylece riskli göstergeler yerine kullanışlı ve güvenli dilimlerden yararlanılmış olur.

Aşağıdaki koddaki nesnelerOluştur'un bir C kütüphanesinin bir işlevi olduğunu varsayalım. Bu işlev Yapı türünden belirtilen adet nesne oluşturuyor olsun ve bu nesnelerden ilkinin adresini döndürüyor olsun:

    Yapı * gösterge = nesnelerOluştur(10);

Belirli bir göstergenin göstermekte olduğu elemanlara erişim sağlayacak olan dilim oluşturan söz dizimi aşağıdaki gibidir:

    /* ... */ dilim = gösterge[0 .. adet];

Buna göre, nesnelerOluştur'un oluşturduğu ve ilkinin adresini döndürdüğü 10 elemana erişim sağlayan bir dilim aşağıdaki gibi oluşturulur:

    Yapı[] dilim = gösterge[0 .. 10];

Artık dilim programda normal bir D dilimi gibi kullanılmaya hazırdır:

    writeln(dilim[1]);    // İkinci elemanı yazdırır
Her türü gösterebilen void*

D'de hemen hemen hiç gerekmese de, yine C'den gelen bir olanak, herhangi türden değişkenleri gösterebilen göstergelerdir. Bunlar void göstergesi olarak tanımlanırlar:

    int tamsayı = 42;
    double kesirli = 1.25;
    void * herTürüGösterebilen;

    herTürüGösterebilen = &tamsayı;
    herTürüGösterebilen = &kesirli;

Yukarıdaki koddaki void* türünden olan gösterge hem bir int'i hem de bir double'ı gösterebilmektedir. O satırların ikisi de yasaldır ve hatasız olarak derlenir.

void* türünden olan göstergeler kısıtlıdır. Getirdikleri esnekliğin bir sonucu olarak, gösterdikleri değişkenlere kendileri erişim sağlayamazlar çünkü gösterilen asıl tür bilinmediğinden gösterilen elemanın kaç baytlık olduğu da bilinemez:

    *herTürüGösterebilen = 43;     // ← derleme HATASI

Böyle işlemlerde kullanılabilmesi için, void*'nin değerinin önce doğru türü gösteren bir göstergeye aktarılması gerekir:

    int tamsayı = 42;                         // (1)
    void * herTürüGösterebilen = &tamsayı;    // (2)

    // ...

    int * tamsayıGöstergesi = cast(int*)herTürüGösterebilen; // (3)
    *tamsayıGöstergesi = 43;                  // (4)

Yukarıdaki örnek kodu şu adımlarla açıklayabiliriz:

  1. Asıl değişken
  2. Değişkenin değerinin bir void* içinde saklanması
  3. Daha sonra o değerin doğru türü gösteren bir göstergeye aktarılması
  4. Değişkenin değerinin doğru türü gösteren gösterge ile erişilerek değiştirilmesi

void* türündeki bir göstergenin değeri arttırılabilir veya azaltılabilir. void* aritmetik işlemlerde ubyte gibi tek baytlık bir türün göstergesiymiş gibi işlem görür:

    ++herTürüGösterebilen;    // değeri 1 artar

D'de void* çoğunlukla C kütüphaneleri kullanılırken gerekir. interface, sınıf, şablon, vs. gibi üst düzey olanakları bulunmayan C kütüphaneleri void* türünden yararlanmış olabilirler.

Mantıksal ifadelerde kullanılmaları

Göstergeler otomatik olarak bool türüne dönüşebilirler. Bu onların değerlerinin mantıksal ifadelerde kullanılabilmesini sağlar. null değere sahip olan göstergeler mantıksal ifadelerde false değerini alırlar, diğerleri de true değerini. Yani hiçbir değişkeni göstermeyen göstergeler false'tur.

Çıkışa nesne yazdıran bir işlev düşünelim. Bu işlev, kaç bayt yazdığını da bir çıkış parametresi ile bildiriyor olsun. Ancak, o bilgiyi yalnızca özellikle istendiğinde veriyor olsun. Bunun isteğe bağlı olması işleve gönderilen göstergenin null olup olmaması ile sağlanabilir:

void bilgiVer(KurşunKalem kalem, size_t * baytAdedi) {
    immutable bilgi = format("Kalem: %s", kalem);
    writeln(bilgi);

    if (baytAdedi) {
        *baytAdedi = bilgi.length;
    }
}

Kaç bayt yazıldığı bilgisinin gerekmediği durumlarda gösterge olarak null değeri gönderilebilir:

    bilgiVer(KurşunKalem(Renk.sarı, 7), null);

Bayt adedinin önemli olduğu durumlarda ise null olmayan bir değer:

    size_t baytAdedi;
    bilgiVer(KurşunKalem(Renk.mavi, 8), &baytAdedi);
    writeln("Çıkışa ", baytAdedi, " bayt yazılmış");

Bunu yalnızca bir örnek olarak kabul edin. Bayt adedinin işlevden her durumda döndürülmesi daha uygun bir tasarım olarak kabul edilebilir:

size_t bilgiVer(KurşunKalem kalem) {
    immutable bilgi = format("Kalem: %s", kalem);
    writeln(bilgi);

    return bilgi.length;
}
new bazı türler için adres döndürür

Şimdiye kadar sınıf nesneleri oluştururken karşılaştığımız new'ü yapı nesneleri, diziler, ve temel tür değişkenleri oluşturmak için de kullanabiliriz. new ile oluşturulan değişkenlere dinamik değişken denir.

new önce bellekten değişken için gereken büyüklükte bir yer ayırır. Ondan sonra bu yerde bir değişken kurar. Bu değişkenlerin kendi isimleri bulunmadığından onlara ancak new'ün döndürmüş olduğu referans ile erişilir.

Bu referans değişkenin türüne bağlı olarak farklı çeşittendir:

auto ve typeof bölümünden hatırlayacağınız gibi, sol taraftaki tür isimleri yerine normalde auto anahtar sözcüğü kullanıldığından çoğunlukla bu ayrıma dikkat etmek gerekmez:

    auto sınıfDeğişkeni = new Sınıf;
    auto yapıGöstergesi = new Yapı;
    auto intGöstergesi = new int;
    auto dinamikDizi = new int[100];

Herhangi bir ifadenin tür isminin typeof(Tür).stringof yöntemiyle yazdırılabildiğini hatırlarsanız, new'ün değişik türler için ne döndürdüğü küçük bir programla şöyle görülebilir:

import std.stdio;

struct Yapı {
}

class Sınıf {
}

void main() {
    writeln(typeof(new int   ).stringof);
    writeln(typeof(new int[5]).stringof);
    writeln(typeof(new Yapı  ).stringof);
    writeln(typeof(new Sınıf ).stringof);
}

Çıktıdan anlaşıldığı gibi, new temel tür ve yapılar için gösterge türünde bir değer döndürmektedir:

int*
int[]
Yapı*
Sınıf

Eğer new ile oluşturulan dinamik değişkenin türü bir değer türü ise, o değişkenin yaşam süreci, programda ona eriştiren en az bir referans (örneğin, bir gösterge) bulunduğu sürece uzar. (Bu, referans türleri için varsayılan durumdur.)

Dizilerin .ptr niteliği

Dizilerin (ve dilimlerin) .ptr niteliği ilk elemanın adresini döndürür. Bu değerin türü eleman türünü gösteren bir göstergedir:

    int[] sayılar = [ 7, 12 ];

    int * ilkElemanınAdresi = sayılar.ptr;
    writeln("İlk eleman: ", *ilkElemanınAdresi);

Bu değer de C kütüphanelerini kullanırken yararlı olabilir. Bazı C işlevleri bellekte art arda bulunan elemanların ilkinin adresini alırlar.

Dizgilerin de dizi olduklarını hatırlarsanız, onların .ptr niteliği de ilk karakterlerinin adresini verir. Burada dikkat edilmesi gereken bir konu, dizgi elemanlarının harf değil, o harflerin Unicode kodlamasındaki karşılıkları olduklarıdır. Örneğin, ş harfi bir char[] veya string içinde iki tane char olarak bulunur.

.ptr niteliğinin döndürdüğü adres ile erişildiğinde, Unicode kodlamasında kullanılan karakterler ayrı ayrı gözlemlenebilirler. Bunu örnekler bölümünde göreceğiz.

Eşleme tablolarının in işleci

Aslında göstergeleri Eşleme Tabloları bölümünde gördüğümüz in işleci ile de kullanmıştık. Orada henüz göstergeleri anlatmamış olduğumdan in işlecinin dönüş türünü geçiştirmiş ve o değeri üstü kapalı olarak bir mantıksal ifadede kullanmıştım:

    if ("mor" in renkKodları) {
        // evet, renkKodları'nda "mor" indeksli eleman varmış

    } else {
        // hayır, yokmuş...
    }

Aslında in işleci tabloda bulunuyorsa elemanın adresini, bulunmuyorsa null değerini döndürür. Yukarıdaki koşul da bu değerin false'a veya true'ya otomatik olarak dönüşmesi temeline dayanır.

in'in dönüş değerini bir göstergeye atarsak, tabloda bulunduğu durumlarda o elemana etkin biçimde erişebiliriz:

import std.stdio;

void main() {
    // Tamsayıdan string'e dönüşüm tablosu
    string[int] sayılar =
        [ 0 : "sıfır", 1 : "bir", 2 : "iki", 3 : "üç" ];

    int sayı = 2;
    auto eleman = sayı in sayılar;               // (1)

    if (eleman) {                                // (2)
        writeln("Biliyorum: ", *eleman);         // (3)

    } else {
        writeln(sayı, " sayısının yazılışını bilmiyorum");
    }
}

Yukarıdaki koddaki eleman göstergesi in işleci ile ilklenmekte (1) ve değeri bir mantıksal ifadede kullanılmaktadır (2). Değeri null olmadığında da gösterdiği değişkene erişilmektedir (3). Hatırlarsanız, null değerinin gösterdiği geçerli bir nesne olmadığı için, değeri null olan bir göstergenin gösterdiğine erişilemez.

Orada eleman'ın türü, eşleme tablosunun değer türünde bir göstergedir. Bu tablodaki değerler string olduklarından in'in dönüş türü string*'dir. Dolayısıyla, auto yerine tür açık olarak aşağıdaki gibi de yazılabilir:

    string * eleman = sayı in sayılar;
Ne zaman kullanmalı
Kütüphaneler gerektirdiğinde

readf işlevinde de gördüğümüz gibi, kullandığımız bir kütüphane bizden bir gösterge bekliyor olabilir. Her ne kadar D kütüphanelerinde az sayıda olacaklarını düşünsek de, bu tür işlevlerle karşılaştığımızda onlara istedikleri türde gösterge göndermemiz gerekir.

Örneğin, bir C kütüphanesi olan gtk'den uyarlanmış olan gtkD'nin bazı işlevlerinin bazı parametreleri göstergedir:

    GdkGeometry boyutlar;
    // ... boyutlar nesnesinin üyelerinin kurulması ...

    pencere.setGeometryHints(/* ... */, &boyutlar, /* ... */);
Değer türünden değişkenleri göstermek için

Yine kesinlikle gerekmese de, değer türünden olan bir değişkenin hangisiyle işlem yapılacağını bir gösterge ile belirleyebiliriz. Örnek olarak yazı-tura deneyi yapan bir programa bakalım:

import std.stdio;
import std.random;

void main() {
    size_t yazıAdedi = 0;
    size_t turaAdedi = 0;

    foreach (i; 0 .. 100) {
        size_t * hangisi = (uniform(0, 2) == 1)
                           ? &yazıAdedi
                           : &turaAdedi;
        ++(*hangisi);
    }

    writefln("yazı: %s  tura: %s", yazıAdedi, turaAdedi);
}

Tabii aynı işlemi gösterge kullanmadan da gerçekleştirebiliriz:

        uniform(0, 2) ? ++yazıAdedi : ++turaAdedi;

Veya bir if koşuluyla:

        if (uniform(0, 2)) {
            ++yazıAdedi;

        } else {
            ++turaAdedi;
        }
Veri yapılarının üyelerinde

Bazı veri yapılarının temeli göstergelere dayanır.

Dizilerin elemanlarının yan yana bulunmalarının aksine, bazı veri yapılarının elemanları bellekte birbirlerinden ayrı olarak dururlar. Bunun bir nedeni, elemanların veri yapısına farklı zamanlarda eklenmeleri olabilir. Böyle veri yapıları elemanların birbirlerini göstermeleri temeli üzerine kuruludur.

Örneğin, bağlı liste veri yapısının her düğümü kendisinden bir sonraki düğümü gösterir. İkili ağaç veri yapısının düğümleri de sol ve sağ dallardaki düğümleri gösterirler. Başka veri yapılarında da gösterge kullanımına çok rastlanır.

D'de veri yapıları referans türleri kullanarak da gerçekleştirilebilseler de göstergeler bazı durumlarda daha doğal olabilirler.

Gösterge üye örneklerini biraz aşağıda göreceğiz.

Belleğe doğrudan erişmek gerektiğinde

Göstergeler belleğe doğrudan ve bayt düzeyinde erişim sağlarlar. Hataya açık olduklarını akılda tutmak gerekir. Ek olarak, programa ait olmayan belleğe erişmek tanımsız davranıştır.

Örnekler
Basit bir bağlı liste

Bağlı liste veri yapısının elemanları düğümler halinde tutulurlar. Liste, her düğümün kendisinden bir sonraki düğümü göstermesi düşüncesi üzerine kuruludur. Sonuncu düğüm hiçbir düğümü göstermez (değeri null'dır):

   ilk düğüm            düğüm                  son düğüm
 ┌────────┬───┐     ┌────────┬───┐          ┌────────┬────┐
 │ eleman │ o────▶  │ eleman │ o────▶  ...  │ eleman │null│
 └────────┴───┘     └────────┴───┘          └────────┴────┘

Yukarıdaki şekil yanıltıcı olabilir: Düğümlerin bellekte art arda bulundukları sanılmamalıdır; düğümler normalde belleğin herhangi bir yerinde bulunabilirler. Önemli olan, her düğümün kendisinden bir sonraki düğümü gösteriyor olmasıdır.

Bu şekle uygun olarak, bir int listesinin düğümünü şöyle tanımlayabiliriz:

struct Düğüm {
    int eleman;
    Düğüm * sonraki;

    // ...
}

Not: Kendi türünden nesneleri gösterdiği için bunun özyinelemeli bir yapı olduğunu söyleyebiliriz.

Bütün düğümlerin bir liste olarak düşünülmesi de yalnızca başlangıç düğümünü gösteren bir gösterge ile sağlanabilir:

struct Liste {
    Düğüm * baş;

    // ...
}

Bu bölümün amacından fazla uzaklaşmamak için burada yalnızca listenin başına eleman ekleyen işlevi göstermek istiyorum:

struct Liste {
    Düğüm * baş;

    void başınaEkle(int eleman) {
        baş = new Düğüm(eleman, baş);
    }

    // ...
}

Bu kodun en önemli noktası başınaEkle işlevini oluşturan satırdır. O satır yeni elemanı listenin başına ekler ve böylece bu yapının bir bağlı liste olmasını sağlar. (Not: Aslında sonuna ekleme işlemi daha doğal ve kullanışlıdır. Bunu problemler bölümünde göreceğiz.)

Yukarıdaki satırda sağ tarafta dinamik bir Düğüm nesnesi oluşturuluyor. Bu yeni nesne kurulurken, sonraki üyesi olarak listenin şu andaki başı kullanılıyor. Listenin yeni başı olarak da bu yeni düğümün adresi kullanılınca, listenin başına eleman eklenmiş oluyor.

Bu küçük veri yapısını deneyen küçük bir program:

import std.stdio;
import std.conv;
import std.string;

struct Düğüm {
    int eleman;
    Düğüm * sonraki;

    string toString() const {
        string sonuç = to!string(eleman);

        if (sonraki) {
            sonuç ~= " -> " ~ to!string(*sonraki);
        }

        return sonuç;
    }
}

struct Liste {
    Düğüm * baş;

    void başınaEkle(int eleman) {
        baş = new Düğüm(eleman, baş);
    }

    string toString() const {
        return format("(%s)", baş ? to!string(*baş) : "");
    }
}

void main() {
    Liste sayılar;

    writeln("önce : ", sayılar);

    foreach (sayı; 0 .. 10) {
        sayılar.başınaEkle(sayı);
    }

    writeln("sonra: ", sayılar);
}

Çıktısı:

önce : ()
sonra: (9 -> 8 -> 7 -> 6 -> 5 -> 4 -> 3 -> 2 -> 1 -> 0)
ubyte göstergesi ile belleğin incelenmesi

Belleğin adresleme birimi bayttır. Her adreste tek baytlık bilgi bulunur. Her değişken, kendi türü için gereken sayıda bayt üzerinde kurulur. Göstergeler belleğe bayt bayt erişme olanağı sunarlar.

Belleğe bayt olarak erişmek için en uygun tür ubyte*'dir. Bir değişkenin adresi bir ubyte göstergesine atanır ve bu gösterge ilerletilerek o değişkeni oluşturan baytların tümü gözlemlenebilir.

Burada açıklayıcı olsun diye değeri on altılı düzende yazılmış olan bir tamsayı olsun:

    int birSayı = 0x01_02_03_04;

Bu değişkeni gösteren bir göstergenin şu şekilde tanımlandığını gördük:

    int * adresi = &birSayı;

O göstergenin değeri, birSayı'nın bellekte bulunduğu yerin adresidir. Göstergenin değerini tür dönüşümü ile bir ubyte göstergesine de atayabiliriz:

    ubyte * baytGöstergesi = cast(ubyte*)adresi;

Bu adresteki int'i oluşturan 4 baytı şöyle yazdırabiliriz:

    writeln(baytGöstergesi[0]);
    writeln(baytGöstergesi[1]);
    writeln(baytGöstergesi[2]);
    writeln(baytGöstergesi[3]);

Eğer sizin mikro işlemciniz de benimki gibi küçük soncul ise, int'i oluşturan baytların bellekte ters sırada durduklarını görebilirsiniz:

4
3
2
1

Değişkenleri oluşturan baytları gözlemleme işini kolaylaştırmak için bir işlev şablonu yazabiliriz:

import std.stdio;

void baytlarınıGöster(T)(ref T değişken) {
    const ubyte * baş = cast(ubyte*)&değişken;    // (1)

    writefln("tür    : %s", T.stringof);
    writefln("değer  : %s", değişken);
    writefln("adres  : %s", baş);                 // (2)
    writef  ("baytlar: ");

    writefln("%(%02x %)", baş[0 .. T.sizeof]);    // (3)

    writeln();
}
  1. Değişkenin adresinin bir ubyte göstergesine atanması
  2. Göstergenin değerinin, yani değişkenin başlangıç adresinin yazdırılması
  3. Türün büyüklüğünün .sizeof niteliği ile edinilmesi ve göstergenin gösterdiği baytların yazdırılması (baş göstergesinden dilim elde edildiğine ve o dilimin yazdırıldığına dikkat edin.)

Baytlar * işleci ile erişerek şöyle de yazılabilirdi:

    foreach (bayt; baş .. baş + T.sizeof) {
        writef("%02x ", *bayt);
    }

bayt göstergesinin değeri o döngüde baş .. baş + T.sizeof aralığında değişir. baş + T.sizeof değerinin aralık dışında kaldığına ve ona hiçbir zaman erişilmediğine dikkat edin.

O işlev şablonunu değişik türlerle çağırabiliriz:

struct Yapı {
    int birinci;
    int ikinci;
}

class Sınıf {
    int i;
    int j;

    this(int i, int j) {
        this.i = i;
        this.j = j;
    }
}

void main() {
    int tamsayı = 0x11223344;
    baytlarınıGöster(tamsayı);

    double kesirli = double.nan;
    baytlarınıGöster(kesirli);

    string dizgi = "merhaba dünya";
    baytlarınıGöster(dizgi);

    int[3] dizi = [ 1, 2, 3 ];
    baytlarınıGöster(dizi);

    auto yapıNesnesi = Yapı(0xaa, 0xbb);
    baytlarınıGöster(yapıNesnesi);

    auto sınıfDeğişkeni = new Sınıf(1, 2);
    baytlarınıGöster(sınıfDeğişkeni);
}

Çıktısı aydınlatıcı olabilir:

tür    : int
değer  : 287454020
adres  : BFFD6D0C
baytlar: 44 33 22 11                             ← (1)

tür    : double
değer  : nan
adres  : BFFD6D14
baytlar: 00 00 00 00 00 00 f8 7f                 ← (2)

tür    : string
değer  : merhaba dünya
adres  : BFFD6D1C
baytlar: 0e 00 00 00 e8 c0 06 08                 ← (3)

tür    : int[3u]
değer  : 1 2 3
adres  : BFFD6D24
baytlar: 01 00 00 00 02 00 00 00 03 00 00 00     ← (1)

tür    : Yapı
değer  : Yapı(170, 187)
adres  : BFFD6D34
baytlar: aa 00 00 00 bb 00 00 00                 ← (1)

tür    : Sınıf
değer  : deneme.Sınıf
adres  : BFFD6D3C
baytlar: c0 ec be 00                             ← (4)

Gözlemler:

  1. Bazı türlerin baytları beklediğimiz gibidir: int'in, sabit uzunluklu dizinin (int[3u]), ve yapı nesnesinin değerlerinin baytları bellekte ters sırada bulunmaktadır.
  2. double.nan özel değerini oluşturan baytları ters sırada düşününce bu değerin 0x7ff8000000000000 özel bit dizisi ile ifade edildiğini öğreniyoruz.
  3. string 8 bayttan oluşmaktadır; onun değeri olan "merhaba dünya"nın o kadar küçük bir alana sığması olanaksızdır. Bu, string türünün perde arkasında bir yapı gibi tanımlanmış olmasından gelir. Derleyicinin bir iç türü olduğunu vurgulamak için ismini __ ile başlatarak, örneğin şöyle bir yapı olduğunu düşünebiliriz:
    struct __string {
        size_t uzunluk;
        char * ptr;    // asıl karakterler
    }
    

    Bu tahmini destekleyen bulguyu string'i oluşturan baytlarda görüyoruz: Dikkat ederseniz, "merhaba dünya" dizgisindeki toplam 13 harf, içlerindeki ü'nün UTF-8 kodlamasında iki baytla ifade edilmesi nedeniyle 14 bayttan oluşur. string'in yukarıda görülen ilk 4 baytı olan 0x0000000e'nin değerinin onlu sistemde 14 olması bu gözlemi doğruluyor.

  4. Benzer şekilde, sınıf nesnesini oluşturan i ve j üyelerinin 4 bayta sığmaları olanaksızdır; iki int için 8 bayt gerektiğini biliyoruz. O çıktı, sınıf değişkenlerinin sınıf nesnesini gösterecek şekilde tek bir göstergeden oluştuğu şüphesini uyandırır:
    struct __Sınıf_DeğişkenTürü {
        __Sınıf_AsılNesneTürü * nesne;
    }
    

Şimdi biraz daha esnek bir işlev düşünelim. Belirli bir değişkenin baytları yerine, belirli bir adresteki belirli sayıdaki baytı gösteren bir işlev yazalım:

import std.stdio;
import std.ascii;

void belleğiGöster(T)(T * bellek, size_t uzunluk) {
    const ubyte * baş = cast(ubyte*)bellek;

    foreach (adres; baş .. baş + uzunluk) {
        char karakter = (isPrintable(*adres) ? *adres : '.');

        writefln("%s:  %02x  %s", adres, *adres, karakter);
    }
}

std.ascii modülünde tanımlı olan isPrintable, kendisine verilen bayt değerinin ASCII tablosunun görüntülenebilen bir karakteri olup olmadığını bildirir. Bazı bayt değerlerinin tesadüfen uç birimin kontrol karakterlerine karşılık gelerek uç birimin çalışmasını bozmalarını önlemek için "isPrintable olmayan" karakterler yerine '.' karakterini yazdırıyoruz.

Bu işlevi string'in .ptr niteliğinin gösterdiği karakterlere erişmek için kullanabiliriz:

import std.stdio;

void main() {
    string dizgi = "merhaba dünya";
    belleğiGöster(dizgi.ptr, dizgi.length);
}

Çıktıdan anlaşıldığına göre ü harfi için iki bayt kullanılmaktadır:

8067F18:  6d  m
8067F19:  65  e
8067F1A:  72  r
8067F1B:  68  h
8067F1C:  61  a
8067F1D:  62  b
8067F1E:  61  a
8067F1F:  20   
8067F20:  64  d
8067F21:  c3  .
8067F22:  bc  .
8067F23:  6e  n
8067F24:  79  y
8067F25:  61  a
Problemler
  1. Kendisine verilen iki int'in değerlerini değiş tokuş etmeye çalışan şu işlevi parametrelerinde ref kullanmadan düzeltin:
    void değişTokuş(int birinci, int ikinci) {
        int geçici = birinci;
        birinci = ikinci;
        ikinci = geçici;
    }
    
    void main() {
        int i = 1;
        int j = 2;
    
        değişTokuş(i, j);
    
        // Değerleri değişsin
        assert(i == 2);
        assert(j == 1);
    }
    

    O programı çalıştırdığınızda assert denetimlerinin başarısız olduklarını göreceksiniz.

  2. Bu bölümde gösterilen liste yapısını şablona dönüştürün ve böylece int'ten başka türlerle de kullanılabilmesini sağlayın.
  3. Bağlı listede yeni elemanların sona eklenmeleri daha doğal bir işlemdir. Ben daha kısa olduğu için bu bölümde başına eklemeyi seçtim. Yeni elemanların listenin başına değil, sonuna eklenmelerini sağlayın.

    Bunun için listenin sonuncu elemanını gösteren bir gösterge yararlı olabilir.