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:
- Sınıf değişkenleri
- Dinamik diziler
- Eşleme tabloları
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ö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 nesneninx
ü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;
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 = &c; 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. (Aşağıda göreceğimiz gibi, dizinin son elemanından bir sonrası da gösterilebilir ama kullanılamaz.) 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) } }
- 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
Çı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:
- 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
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:
- 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ı nesnelerinde ve temel türlerde bir göstergedir:
Yapı * yapıGöstergesi = new Yapı; int * intGöstergesi = new int;
- Dizilerde ise bir dinamik dizidir:
int[] dinamikDizi = new int[100];
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ı
Göstergeler D'de oldukça az kullanılırlar. Girişten Bilgi Almak bölümünde de gördüğümüz gibi, readf
bile aslında gösterge gerektirmez.
Kütüphaneler gerektirdiğinde
Göstergeler C ve C++ kütüphanelerinin D ilintilerinde kullanılıyor olabilirler. Ö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(); }
- Değişkenin adresinin bir
ubyte
göstergesine atanması - Göstergenin değerinin, yani değişkenin başlangıç adresinin yazdırılması
- 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:
- 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 öğreniyoruz.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.- Benzer şekilde, sınıf nesnesini oluşturan
i
vej
üyelerinin 4 bayta sığmaları olanaksızdır; ikiint
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
- Kendisine verilen iki
int
'in değerlerini değiş tokuş etmeye çalışan şu işlevi parametrelerinderef
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. - 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. - 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.