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 derste 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 derslerden tanıdığımız D olanakları ile kısaca hatırlayalım.
foreach'in ref değişkenleri
foreach Döngüsü dersinde 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 dersinde 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ığı için, 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; }
Şimdi 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, kendileri sahip olmadıkları başka değerlere erişim sağlarlar:
- sınıflar
- dinamik diziler
- erişim tabloları
Referans kavramını Değerler ve Referanslar dersinde görmüştük. Burada, o derse 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ı için; farklı sınıf değişkenleri olan kalem ve başkaKalem, new ile oluşturulmuş olan 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ı her zaman için bu şekilde düşünülebilir. 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östergelerin D programcılığında da mutlaka bilinmeleri gerekir.
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;
Buna göre, bir int'i gösteren bir gösterge şöyle tanımlanabilir:
int * benim_göstergem;
Böyle bir tanımda * karakterini "göstergesi" diye okuyabilirsiniz. benim_gö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* benim_göstergem; int *benim_gö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 (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, değer olarak bir adres taşıması, göstergenin o adresteki değişkeni göstermesi anlamına gelir.
Şimdiye kadar readf işlevi ile çok kullandığımız & işlecini Değerler ve Referanslar dersinden 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 * benim_göstergem = &beygirGücü;
Yukarıdaki ifadede göstergenin beygirGücü'nün adresi ile ilklenmesi, benim_göstergem'in beygirGücü'nü göstermesini sağlar.
Göstergenin değeri çıkışa yazdırıldığında beygirGücü'nün adresi ile aynı olur:
writeln("beygirGücü'nün adresi : ", &beygirGücü); writeln("benim_göstergem'in değeri: ", benim_göstergem);
beygirGücü'nün adresi : BFDB9830 benim_göstergem'in değeri: BFDB9830
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 küçük 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 olmasını, ve böylece o değişkeni göstermesini, referanslara benzer şekilde şöyle düşünebiliriz:
BFDB9830 adresindeki başka bir adresteki
beygirGücü benim_göstergem
---+-------------------+--- ---+---------------+---
| 180 | | BFDB9830 |
---+-------------------+--- ---+-------|-------+---
▲ |
| |
+-----------------------------+
beygirGücü'nün değeri 180, benim_gö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("benim_göstergem'in adresi: ", &benim_göstergem);
benim_göstergem'in adresi: BFDB9834
beygirGücü ile benim_göstergem'in adreslerinin arasındaki farkın bu örnekte 4 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 yan yana 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:
BFDB9830 BFDB9834 BFDB9838
: : :
---+----------------+----------------+---
| 180 | BFDB9830 |
---+----------------+----------------+---
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: ", *benim_gö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 biliyorsanı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: *benim_gö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
O kullanım, toString işlevi tanımlanmış olduğu için Konum nesnesini yazdırmak için yeterlidir:
(0,0)
Ancak, gösterilen nesnenin bir üyesine erişmek için * işleci kullanıldığında yazım 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österge:merkez'i gösteren gösterge*gösterge: nesneye erişim; yanimerkez'in kendisi(*gösterge): nokta karakteri gösterge'ye değil, onun gösterdiğine uygulansın diye gereken parantezler(*gösterge).x: gösterdiği nesnenin x üyesi
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;
writeln(merkez);
Daha basit olan gösterge.x ifadesi, merkez'in x üyesini değiştirmiştir:
(10,0)
Bunun sınıflarla aynı olduğuna dikkat edin. Sınıflarda da sınıf değişkenlerine 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; nesnenin üyesine erişir değişken.üye = 42;
Sınıflar dersinden 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ı, yapı değişkenleri ile göstergelerin temelde benzer şekilde gerçekleştirildiklerini ortaya koyar.
Gösterge değerinin değiştirilmesi
Göstergelerin değerleri arttırılabilir, azaltılabilir, ve 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 arttırmazlar. Göstergenin değeri, belirtilen miktar kadar sonraki (veya önceki) değişkeni gösterecek şekilde 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
Derleyici, bunu sağlayabilmek için göstergenin değerini türün büyüklüğü kadar arttırır. Örneğin int* türündeki bir göstergenin değeri; int'in büyüklüğü 4 olduğu için, ++ 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 bir int olarak tanımlanmış olan beygirGücü değişkenini gösteren göstergeyi arttırmak yasal değildir:
++benim_göstergem; // ← tanımsız davranış
Öyle bir işlemde programın nasıl davranacağının tanımlanmamış olması, o işlemin sonucunda ne olacağının belirsiz olması anlamına gelir. O işlem sonucunda programın çökeceği sistemler bulunabilir. Günlük kullanımdaki bilgisayarlardaki mikro işlemcilerde ise, göstergenin değeri 4 sonraki bellek adresine sahip olacaktır. (Not: İlginç bir gözlem olarak, yukarıdaki programdaki adreslerden anlaşıldığı gibi, beygirGücü'nden bir sonraki adreste benim_göstergem'in kendisi bulunduğu için, gösterge kendisini sanki bir int'miş gibi gösterecektir.)
O yüzden, göstergelerin değerlerinin arttırılması veya azaltılması ancak yan yana bulundukları bilinen değişkenler gösterildiğinde kullanılmalıdır.
Değişkenlerin yan yana bulunmaları; dizilerin, ve zaten karakter dizileri olan dizgilerin tanımları gereğidir. Dizi içindeki bir elemanı gösteren 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, to!string(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) } }
- tanımlanması; dizinin ilk elemanının adresi ile ilklenmektedir
- değerinin kullanılması; değeri, gösterdiği elemanın adresidir
- gösterdiği nesneye erişim
- bir sonraki nesneyi göstermesi
Bu programın çı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 tekrarlandığı için, gösterge hiçbir zaman var olmayan bir nesneye erişmemektedir.
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ı
Göstergelerin, bir dizinin sonuncu elemanından hemen sonraki hayali elemanı göstermeleri 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 oldukça 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 almakta ve 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şlevlerin for döngüleriyle gerçekleştirilenlerine de rastlayabilirsiniz:
void onKatı(int * baş, int * son) { 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);
İşte bunun için, dizilerin son elemanlarından sonraki aslında var olmayan bir elemanın gösterilmesi 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 dizinin ilk elemanıymış gibi düşünülür ve [] işleci o hayali dizinin 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 orada hayali bir dizi varmış gibi, o dizinin 1 indeksli elemanına, yani asıl dizinin 3 indeksli elemanına erişim sağlar.
Karışık gibi görülse 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) olarak 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. 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ığı için bir hata atılır ve böylece programın yanlış sonuçlarla devam etmesi önlenmiş olur:
core.exception.RangeError@deneme(8391): Range violation
Her türü gösterebilen void*
D'de 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 çok kısıtlıdırlar: ne onların değerlerini sonraki (veya önceki) değişkeni gösterecek şekilde değiştirebiliriz, ne de gösterdikleri değişkenlere erişebiliriz. Bu, getirdiği esnekliğin bir sonucudur: gösterilen türün ne olduğu bilinmediği için, gösterdiği 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:
- asıl değişken
- değişkenin değerinin bir
void*içinde saklanması - daha sonra o değerin doğru türü gösteren bir göstergeye aktarılması
- değişkenin değerinin doğru türü gösteren gösterge ile erişilerek değiştirilmesi
interface, sınıf, şablon, vs. gibi üst düzey olanakları bulunmayan C kütüphanelerinde void* türleri çok kullanılır. Böyle kütüphaneler kullandığımızda bizim de void* türünde göstergeler kullanmamız gerekebilir.
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. Ama o işi ancak özellikle istendiğinde yapıyor olsun. Bunun isteğe bağlı olması, işleve gönderilen gösterge değerinin null olup olmaması ile gerçekleştirilebilir:
void bilgiVer(const ref KurşunKalem kalem, int * baytAdedi) { const string 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:
int baytAdedi; bilgiVer(KurşunKalem(Renk.mavi, 8), &baytAdedi); writeln("Çıkışa ", baytAdedi, " bayt yazılmış");
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 için de kullanabiliriz. new ile oluşturulan değişkenlere dinamik değişken denir.
new, bellekten değişken için gereken sayıda baytlık bir yer ayırır. Ondan sonra bu yerde bir değişken kurar. Bu değişkenlerin kendi isimleri bulunmadığı için; onlara ancak new'ün döndürmüş olduğu referans ile erişilir.
Bu referans,
- sınıf nesnelerinde, şimdiye kadar çok gördüğümüz gibi bir sınıf değişkenidir:
Sınıf sınıfDeğişkeni = new Sınıf;
Yapı * yapıGöstergesi = new Yapı; int * intGöstergesi = new int;
int[] dinamikDizi = new int[100];
auto ve typeof dersinden hatırlayacağınız gibi, sol taraftaki tür isimleri yerine normalde auto anahtar sözcüğü kullanıldığı için çoğunlukla bu ayrıma dikkat etmemiz 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
Dizilerin .ptr niteliği
Dizilerin .ptr niteliği, dizideki ilk elemanın adresini döndürür. Bu değerin türü, dizinin 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 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östereceğim.
Eşleme tablolarının in işleci
Göstergeleri aslında Eşleme Tabloları dersinde gördüğümüz in işleci ile kullandık. Orada henüz göstergeleri anlatmamış olduğum için, 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ş... }
in işleci, aslında 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 dönüşmesine göre işler.
in'in dönüş değerini bir göstergeye atarsak, elemanın tabloda bulunduğu durumlarda ona etkin bir şekilde 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ı için, in'in dönüş türü string*'dir. Dolayısıyla, auto yerine türü açık olarak yazmak istesek:
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() { int yazıAdedi; int turaAdedi; foreach (i; 0 .. 100) { int * hangisi = (uniform(0, 2) == 1 ? &yazıAdedi : &turaAdedi); *hangisi += 1; } 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ında
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 hızlı veya daha doğal olabilirler.
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 son
düğüm düğüm düğüm düğüm
+--------+---+ +--------+---+ +--------+---+ +--------+------+
| eleman | o----▶ | eleman | o----▶ | eleman | o----▶ ... | eleman | null |
+--------+---+ +--------+---+ +--------+---+ +--------+------+
Yukarıdaki şekil yanıltıcı olabilir: burada düğümlerin bellekte art arda bulundukları sanılmasın. 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ş; // ... }
Dersin 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 soruyorum.)
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; this(int eleman, Düğüm * sonraki) { this.eleman = eleman; this.sonraki = 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, bize belleğe bayt bayt erişme olanağı sunarlar.
Belleğe bayt olarak erişmek için en uygun tür ubyte*'dir. Örneğin 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.
Örnek olarak, değerini açıklayıcı olsun diye onaltılı düzende yazdığım bir tamsayı olsun:
int birSayı = 0x01020304;
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 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:
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: "); foreach (i; 0 .. T.sizeof) { // (3) writef("%02x ", baş[i]); // (4) } writeln(); writeln(); }
- Değişkenin adresinin bir
ubytegöstergesine atanması - Göstergenin değerinin, yani değişkenin başlangıç adresinin yazdırılması
- Türün büyüklüğünün
.sizeofniteliği ile edinilmesi - Göstergenin gösterdiği bayta
[]işleci ile erişilmesi
Yukarıdaki döngüyü * işleci ile erişecek şekilde şöyle de yazabilirdik:
foreach (bayt; baş .. baş + T.sizeof) { writef("%02x ", *bayt); }
O döngüde bayt göstergesininin değeri, 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ıfNesnesi = new Sınıf(1, 2); baytlarınıGöster(sınıfNesnesi); }
Çı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:
- 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 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 öğreniyoruzstring8 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,stringtü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:- Benzer şekilde, sınıf nesnesini oluşturan
ivejüyelerinin 4 bayta sığmaları olanaksızdır; ikiintiç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 __string { int 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 tahmini güçlendiriyor.
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.ctype; // ... void belleğiGöster(T)(T * bellek, int uzunluk) { const ubyte * baş = cast(ubyte*)bellek; foreach (i; 0 .. uzunluk) { const ubyte * adres = baş + i; const char karakter = isprint(*adres) ? *adres : '.'; writefln("%s: %02x %s", adres, *adres, karakter); } }
std.ctype modülünde tanımlı olan isprint işlevi, 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, "isprint olmayan" karakterler yerine '.' karakterini yazdırıyoruz.
Bu işlevi, string'in .ptr niteliğinin gösterdiği karakterlere erişmek için kullanabiliriz:
string dizgi = "merhaba dünya";
belleğiGöster(dizgi.ptr, dizgi.length);
Çıktıdan anlaşıldığına göre ü harfi için gerçekten de 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
- Kendisine verilen iki
int'in değerlerini değiş tokuş etmeye çalışan şu işlevi parametrelerinderefkullanmadan düzeltin: - Bu derste 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. - Bağlı listede yeni elemanların sona eklenmeleri daha doğal bir işlemdir. Ben daha kısa olduğu için bu derste başına eklemeyi seçtim. Yeni elemanların listenin başına değil, sonuna eklenmelerini sağlayın.
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. Bu çözümde ref parametre kullanmak yasak! :o)
Bunun için listenin sonuncu elemanını gösteren bir gösterge yararlı olabilir.
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları