github icontwitter icon

Pointer'landıramadıklarımızdan Mısınız?

Author imageEnes Başpınar /15 Haz 2020
17 min read •––– views

C, C++ gibi dillerde daha yoğun olsa da programlama genelinde bu kavramdan kaçışınız yoktur. Bu sebeple 'Pointer' kavramını herkesin anlayabileceği seviyeye çekelim.

Değişkenler Nereye ve Nasıl Kaydedilir?

Programlamanın bir aşamasında "Tanımladığımız değişkenler nereye gidiyor?" diye hepimiz düşünmüşüzdür. Cevap basit: RAM belleğe.

RAM bellek, alt alta kutular şeklinde tasvir edilir denk gelmişseniz. Her biri 8 bit yani 1 bayt yer kaplayan kutular halinde. 1 GB kapasitesi olan RAM'de 1.073.741.824 adet kutu vardır. Bu da 4 bayt yer tutan tamsayılar ile dolduracak olursak maksimum 268.435.456 adet tam sayı saklayabileceğimiz anlamına gelir. Her bir kutunun kendine özel, bellekte bulundukları konumu tarif eden adresi vardır. Bu sayede CPU kutulara veri yerleştirebilir ve yerleştirdiği verilere sonradan erişebilir.

Adresler sayıdır ancak bildiğimiz sayılardan değil. 16'lık (hexadecimal) tabanda ifade edilirler.

Örneklendirmeye bir tam sayı değişkeni tanımlayarak başlayalım:


_1
int dogum_tarihi = 1999;

Şunu anlamakta yarar var: bellekteki her değere adresi ile erişim sağlarız. Ancak programlama dillerinde bununla uğraşmamıza gerek bırakmayan değişken kavramı vardır. Biz bu ismi verdiğimizde CPU hangi adrese bakacağını anlar. Örnekteki dogum_tarihi dediğimizde CPU "He tamam, bu 1999 değerini istiyor." der.

Günlük yaşantımızda sayıları 10'luk tabanda (decimal) kullanıyoruz. Ancak bilgisayarlar bunları anlayabilmek için 2'lik tabana (binary) çevirmek zorunda. Bellek temsillerinde ise 16 tabanını (hexadecimal) kullanılıyor. Aşağıda 1999 değerinin bu tabanlardaki halini görebilirsiniz:

Değişkenin bellekteki şekline bakalım. 16'lık tabandaki karşılığını kutulara ikişerli olarak yerleştiririz:

İkişerli yerleştirme sebebimiz, 2'lik tabandaki her 8 bitin 16'lık tabanda 2 karaktere karşılık gelmesidir. Ve bu kısma özel bir isim vererek dogum_tarihi dedik:

Artık biz bu değişkeni çağırdığımızda CPU belleğe gider ve değişkenin başlangıcı adresi olan 90000008 adresine uğrar. Değişken int ise 4 bayt olacaktır. Dolayısıyla 4 kutu okur ve birleştirir. Yani 00 00 07 CF değerini elde eder ve 10 tabanına çevirerek 1999 sayısını verir.

Bellek Adresini Bilmek Ne İşimize Yarar?

Verinin kendisi yerine adresini kullanmak, onu kopyalamadan birden fazla yerden erişim sağlayabilmemizi ve değişiklik yapabilmemizi sağlar. Bellekte gereksiz yer işgalini önlemiş oluruz.

Bir otonom araba hayal edin. Kameradan gelen görüntüler belleğe kaydediliyor ve birden fazla programda kullanılıyor. Bir program bu görüntüler ile yolu takip ediyor, bir diğeri yayaları takip ediyor, başka bir tanesi ise trafik ışıklarını takip ediyor. Şimdi her program için görüntüleri kopyaladığımızı düşünün. Görüntülerin kaplayacağı alan birden 3 katına çıktı. Şimdi bir de bu programlara yalnızca resimlerin adreslerini verdiğinizi hayal edin. Her biri ordan alıp işini görebilir. Ne bellek doldurulmuş olur ne de programlar veriden mahrum kalmış olur.

Değişkenlerin Adreslerini Nasıl Elde Ederiz?

Burada devreye, bellek adresi tutan değişkenler girer. Değerleri nasıl değişkenlerde depolayabiliyorsak bellek adreslerini de aynı şekilde depolayabiliriz. Bunu gerçekleştiren yapılara pointer denir. Aslında kendisi de bir değişkendir.

Önceki kısımda görsel olarak anlattığımız örneği kodlama ortama dökelim. İlk olarak değişken tanımımızı yapalım:


_1
int dogum_tarihi = 1999;

Dediğimiz gibi dogum_tarihi 1999 tamsayı değerini depoluyor. Şimdi bu değerin bellekteki adresini elde edelim:


_6
// 1. tanımlama biçimi
_6
int* dogum_tarihi_ptr = &dogum_tarihi;
_6
_6
// 2. tanımlama biçimi (atamayı daha sonra yap)
_6
int* dogum_tarihi_ptr;
_6
dogum_tarihi_ptr = &dogum_tarihi; // yıldız yok dikkat edin.

Pointer'ları belli etmek için genellikle ismin başına veya sonuna "p", "ptr" gibi eklemeler yapılır.

Şimdi bu sözdizimini inceleyelim.

dogum_tarihi_ptr değişkenimizin türü, göreceğimiz üzere int*. Bu da bize tamsayı değişkenin adresini tutan pointer olduğunu gösterir. Başına koyduğumuz '&'' işareti ise değişkenimizin adresini elde etmemize yarayan address-of operatörüdür. Yani &dogum_tarihi, dogum_tarihi değişkeninin bellekteki adresidir.

Şu iki çıktıya bakarsak '&' işaretinin ne işe yaradığı daha net anlaşılabilir:


_5
printf("%d\n", dogum_tarihi);
_5
// bellek adreslerini yazdırmak için
_5
// özel %p belirteci kullanılır.
_5
printf("%p\n", &dogum_tarihi);
_5
printf("%p\n", dogum_tarihi_ptr);

=== Output:

1999 0x7ffd83c692bc 0x7ffd83c692bc

Hemen toparlayalım. dogum_tarihi ifadesi, bildiğimiz int değişkenidir. Ve 1999 sayısını depolar. &dogum_tarihi ifadesi, değişkenimizin adresidir. dogum_tarihi_ptr ifadesi ise elde ettiğimiz adresi depoladığımız değişkendir. Tipi ise int*'dir. Yani değişken adresi depolayan değişken diyebiliriz :)

Bu kısmı tamamlarken bir noktaya daha değinmekte fayda görüyorum. Pointer tanımlarken int *dogum_tarihi_ptr, int* dogum_tarihi_ptr sözdimlerinden herhangi birini kullanabiliriz. Yıldızın sabit konumu yoktur, sadece tip ile değişken ismi arasında olmalıdır. Ancak C/C++ programcıları geleneksel olarak int *dogum_tarihi_ptr yazımını kullanılır. Bunun sebebi aşağıdaki kullanımda yarattığı kafa karışıklığıdır:


_1
int* ptr_a, ptr_b;

Buradaki iki değişkeninde pointer olacağını düşünüyorsunuz değil mi? Ancak ptr_a pointer iken ptr_b int değerdir. Şu şekilde yazılmış gibi kabul edilir:


_2
int* ptr_a;
_2
int ptr_b;

Bu yüzden aşağıdaki şekilde kullanırsak, herhangi bir sorun oluşmaz:


_1
int *ptr_a, ptr_b;

Değişken ismine bitişik olduğu için değişkene ait olduğundan emin oluruz.

Adresler ile Değişken Değerlerine Erişmek

Değişkenin adresini elde edebileceğimiz gibi adres yardımıyla değişken değerine de erişebiliriz. Şu görseli birlikte inceleyelim:

Örneğin elimizdeki doğum tarihi bilgisini kullanıcının yanlış girmiş. Bunu değiştirmek istiyor. Elimizde hem değişken hem de adresini tutan pointer var:


_2
int dogum_tarihi = 1999;
_2
int *dogum_tarihi_ptr = &dogum_tarihi;

Değişkenlerin değerini nasıl değiştirebileceğimizi zaten biliyorduk:


_1
dogum_tarihi = 1990;

"Peki neden hala o yöntemle değiştirmeye devam etmiyoruz?" diyorsanız çoğunlukla öyle yapacak olsakta bazı zamanlar gelecek ki pointer kullanmak zorunda kalacağız. İleride göreceğiz.

Artık pointer vasıtasıyla da değiştirebiliriz:


_4
*dogum_tarihi_ptr = 1990;
_4
_4
printf("%d", dogum_tarihi_ptr);
_4
printf("%d", *dogum_tarihi_ptr);

=== Output:

0x7ffc0206d83c // başındaki "0x" hexadecimal olduğunu belirtir

Burada kafalar çorba olabiliyor. Pointer tanımlanırken kullanılan * işareti, onun pointer olduğunu belirtmek için konur ve başka amacı yoktur. Ancak tanımlandıktan sonra kullanacağımız her * işareti, pointer'ın işaret ettiği adresteki değeri temsil eder. Buna dereference operator denir.

Adresler ile Dizi Elemanlarına Erişmek

Bu kısma geçmeden önce küçük bir ipucu vermek istiyorum. String değişkenler tanımlarken char[] isim = "Ahmet" şeklindeki bir kullanıma denk gelmişsinizdir. C'de aslında string diye bir tip yoktur. Son elemanının '\0' karakteri olan karakter dizileri ile ifade edilirler. Yani bu anlatacaklarım onlar için de geçerli olacak.

Dizi isimleri farklı kullanımlarda farklı anlam ifade edebilirler. Pointer kavramı, dizileri anlamak için önemlidir. Bu sebeple bir dizi oluşturup pointer ile ilişkisini görelim:


_1
int sayi_dizisi[] = {10, 20, 30};

Dizinin ismi, ilk elemanın adresini tutar. Dolayısıyla pointer gibi davranır. Aşağıdaki çıktılara bakacak olursak bunu net bir şekilde görebiliriz:


_3
printf("%p\n", sayi_dizisi); //dizinin ismi
_3
printf("%p\n", &sayi_dizisi); //dizinin adresi
_3
printf("%p\n", &sayi_dizisi[0]); //ilk elemanın adresi

=== Output:

0x7ffcd4fb610c 0x7ffcd4fb610c 0x7ffcd4fb610c

Üçünün de aynı olduğunu ve diziye erişeceğimiz adresi verdiklerini görebiliriz. Özetleyecek olursak; diziler, temel veri tipleri gibi değer tutmak yerine dizinin ilk elemanının adresini tutar. Bunu sayi_dizisi ifadesini yazdırarak görebiliriz. &sayi_dizisi dediğimizde dizinin adresini elde ederiz. Ve dizinin ilk elemanı ile aynıdır. Son olarak &sayi_dizisi[0] ifadesiyle dizimizin ilk elemanının adresine bakıyoruz ki ki bu da aynıdır.

printf() ile kullandığımızda dizinin adresini ifade eder ancak bazı durumlarda da dizinin değerini temsil eder. Örneğin sizeof() fonksiyonuna gönderirsek, pointer boyutunu değil dizi değerinin boyutunu verecektir:


_1
printf("%d", sizeof(sayi_dizisi)); // 4 bayt * 3 = 12 bayt

=== Output:

5

Yani adresi yerine gerçek diziyi kullandığımız anlamına gelir.

Pointer Aritmetiği

Kullandığımız diziyi tekrardan hatırlayalım ve adresini tutan ekstra bir pointer oluşturalım:


_3
int sayi_dizisi[] = {10, 20, 30};
_3
int *sayi_dizisi_ptr = sayi_dizisi;
_3
// &sayi_dizisi de yazılabilir

sayi_dizisi zaten adres tutuyordu değil mi? İkinci bir yerde daha tutmak için sayi_dizisi_ptr pointer'ını oluşturduk.

Dizimizin bellekteki durumunu görselleştirelim:

Dizilerin elemanlarına erişmek için indeksleri kullanıyorduk (bir sonraki kısımda karşılaştıracağız). Ancak arkada neler dönüyor merak ediyoruz. İşte bunu pointer yardımıyla anlayabiliriz.

Her bir elemana dizi adresine uyguladığımız aritmetik işlemler ile erişiyoruz. Şu örneğe bakalım:


_12
printf("ilk adres: %p\n", sayi_dizisi_ptr);
_12
printf("ilk eleman: %i\n", *sayi_dizisi_ptr);
_12
_12
sayi_dizisi_ptr += 1;
_12
_12
printf("ikinci adres: %p\n", sayi_dizisi_ptr);
_12
printf("ikinci eleman: %i\n", *sayi_dizisi_ptr);
_12
_12
sayi_dizisi_ptr += 1;
_12
_12
printf("son adres: %p\n", sayi_dizisi_ptr);
_12
printf("son eleman: %i\n", *sayi_dizisi_ptr);

=== Output:

ilk adres: 0x7ffcd4fb610c ilk eleman: 10 ikinci adres: 0x7ffcd4fb6110 ikinci eleman: 20 son adres: 0x7ffcd4fb6114 son eleman: 30

İsmin ilk öğeye işaretçi olduğunu söylemiştik. Dolayısıyla *array_ptr dediğimizde ilk öğeyi verdiğini biliyoruz. Bu kısım tamam. Peki array_ptr += 1; ne anlama geliyor?

Pointer'a 1 eklediğimizde bir sonraki kutunun adresini tutmasını istiyoruz.

Biraz kafalar karışmış olabilir. Bu tarz şeylere görsellik katmadan anlamak zordur bilirim. İkinci elemana ulaşmak için adrese 1 eklediğimizde olacak şey böyledir:

Göreceğimiz gibi 1 eklediğimizde esasen 1*sizeof(int) demek isteriz. int olmasının sebebi dizinin elemanlarının tam sayı olmasıdır. Tam sayı boyutunun 4 bayt olduğundan bahsetmiştik. Yani 4 kutu. 1 eklemek istediğimizde 4 kutu sonraya git demiş oluruz. Yani sayi_dizisi_ptr pointer değişkeni artık 0x7ffcd4fb6110 adresini depolayacak. Bunu yapma sebebimiz ise diğer elemanın adresine erişmek. Tekrardan 1 eklediğimizde de aynı işlemler uygulanır ve adres 0x7ffcd4fb6114 olur.

Direk olarak 3. elemana erişmek isteseydik 2*sizeof(int), yani 2 eleman sonraya git diyebilirdik:

Bu sefer sonraki 8. kutuya giderek 30 değerinin adresine sahip olurdu.

Açıklaması biraz uzun sürmüş olabilir. Ama kafanızda netleşeceğini düşünüyorum. Anlayıp anlamadığınızı bir sorgulayın. Eğer anladıysanız devam, anlamadıysanız editörü açın ve bir takım C/C++ deneyleri yapın.

Söylememe gerek var mı bilmiyorum ancak pointer'a tekrar atama yaptığımız için adres değişir diyoruz. Yani sayi_dizisi_ptr += 1; ifadesi adresi kalıcı olarak değiştirir. Sonraki adresi yazdırmak amacıyla sayi_dizisi_ptr + 1; ifadesini kullansaydık adresin değeri değişmezdi. Oradaki = kritik öneme sahiptir.

Son işlemin ardından artık sayi_dizisi_ptr sonuncu elemanın adresini tutacaktır:


_2
printf("adres: %p\n", sayi_dizisi_ptr);
_2
printf("eleman: %i\n", *sayi_dizisi_ptr);

=== Output:

adres: 0x7ffcd4fb6114 eleman: 30

Peki tekrardan 2. elemana ulaşmak istersek? Çocuk oyuncağı. Toplama yerine çıkarma yaparak önceki kutulara gideceğiz:


_3
sayi_dizisi_ptr -= 1;
_3
printf("adres: %p\n", sayi_dizisi_ptr);
_3
printf("eleman: %i\n", *sayi_dizisi_ptr);

=== Output:

adres: 0x7ffcd4fb6110 eleman: 20

Artık pointer ile toplama ve çıkarma yapmanın ne anlama geldiğini benimsediğimizi düşünüyorum. Bunu anlamak önemli.

Bu kısımdan bir çıkarım yapacak olursak, dizi tanımlamak için ilk elemanın adresini ve eleman sayısını bilmemizin yeterli olacağını anlayabiliriz. Bunu anladığınızda dizi tanımlamak çok daha kolay hale gelir.

İndekslerin Pointer Aritmetiğindeki Karşılıkları

Az önceki kısımda anlattığımız dizi elemanlarını elde etmek işlemini pointerlara girmeden önce indeksler ile kolayca yapabiliyorduk:


_3
printf("ilk eleman: %i\n", sayi_dizisi[0]);
_3
printf("ikinci eleman: %i\n", sayi_dizisi[1]);
_3
printf("son eleman: %i\n", sayi_dizisi[2]);

=== Output:

ilk eleman: 10 ikinci eleman: 20 son eleman: 30

Ancak bazen pointerlara ihtiyacımız olur. O yüzden indekslere karşılık gelen pointer ifadelerine göz gezdireceğiz. Yukarıdaki kodu indeksler yerine pointer aritmetiği kullanılarak yeniden yazalım


_5
// İlk ifade, *(sayi_dizisi) ve
_5
// *(sayi_dizisi + 0) ile tamamen aynıdır.
_5
printf("ilk eleman: %d\n", *sayi_dizisi);
_5
printf("ikinci eleman: %d\n", *(sayi_dizisi + 1));
_5
printf("son eleman: %d\n", *(sayi_dizisi + 2));

=== Output:

ilk eleman: 10 ikinci eleman: 20 son eleman: 30

Ancak bu kısımda dikkat etmemiz gereken bir şey var. Bir dizinin 2. elemanının adresini depolayan bir pointer tanımlayalım. Ve indeks 1'deki değere bakalım:


_2
int *yeni_sayi_dizisi = &sayi_dizisi[1]; // ya da (sayi_dizisi + 2)
_2
printf("%d", yeni_sayi_dizisi[1])

=== Output:

30

30 sonucunu alırız. Çünkü dizimiz artık şu hale gelecektir:

Dolayısıyla ilk eleman artık 30 olacaktır (başlangıçtan 4 kutu sonrası olarak hayal ediyorduk hatırlayın).

Fonksiyonlar ile Pointer İlişkisi

Bir fonksiyon içerisinde değişkenlerin değerini değiştirmek istediğimizi düşünelim. Bunun için bir örnek oluşturalım:


_14
#include <stdio.h>
_14
_14
int kare_al(int n) {
_14
n *= n;
_14
printf("Fonksiyon içinde sayı: %d\n", n);
_14
return n;
_14
}
_14
_14
int main() {
_14
int sayi = 12;
_14
printf("Fonksiyon öncesinde sayı: %d\n", sayi);
_14
printf("Fonksiyondan return edilen sayı: %d\n", kare_al(sayi));
_14
printf("Fonksiyon sonrasında sayı: %d\n", sayi);
_14
}

=== Output:

Fonksiyon öncesinde sayı: 12 Fonksiyon içinde sayı: 144 Fonksiyondan return edilen sayı: 144 Fonksiyon sonrasında sayı: 12

Burada fonksiyona gönderilen sayi değişkeninin n isminde kopyası oluşturulur ve bellekte yer işgal eder. Yapılan işlemlerin orjinal değişken üzerinde etkisi olmaz. Yeni değer yalnızca return ile çağrıldığı yere döndürülebilir.

Peki ya argümanı alırken pointer kullanırsak ne olurdu?


_13
#include <stdio.h>
_13
_13
void kare_al(int *n) {
_13
*n *= *n; // adresteki sayıyı kendisiyle çarpıyoruz
_13
printf("Fonksiyon içinde sayı: %d\n", *n)
_13
}
_13
_13
int main() {
_13
int sayi = 12;
_13
printf("Fonksiyon öncesinde sayı: %d\n", sayi);
_13
kare_al(&sayi);
_13
printf("Fonksiyon sonrasında sayı: %d\n", sayi);
_13
}

=== Output:

Fonksiyon öncesinde sayı: 12 Fonksiyon içinde sayı: 144 Fonksiyon sonrasında sayı: 144

Farkı net bir şekilde görebilirsiniz. Fonksiyona değişkenin adresini gönderdiğimizde yapılan işlemler orjinal değişkeni değiştirir. Adresteki içeriğin karesi alır ve adresin içeriği değiştirilir. Fonksiyon sonrasında o adrese bakacak olursak değiştiğini görürüz.

Bu özelliğin yararı çok fazladır. Örneğin çok sayıda veri üzerinde çalışıyorsunuz, mesela 100.000 adet resmimiz var. Ve bu resimleri nesne tespitini yapacak fonksiyona göndermek istiyoruz. Ancak pointer olmasaydı ve hepsinin kopyasını göndermiş olsaydık, bellekte 200.000 resim depolanmış olurdu. Muazzam bir yer işgali olacaktır ve performans düşecektir. Bu sebeple, sadece adresin içeriğinde değişiklik yapmak faydalıdır. Her yerden orjinal içeriğe erişebiliriz.

Peki dizi göndermek istesek ne olurdu? Dizi isimlerinin pointer olduğundan bahsetmiştik. Diziyi fonksiyona kabul ederken parametre olarak tanımlayabileceğimiz ve temelde aynı olan 2 seçeneğimiz var.

Birincisi dizi olarak almak:


_1
int sayilara_5_ekle(int dizi[])

İkincisi ise pointer olarak almak:


_1
int sayilara_5_ekle(int *dizi) // önerilen budur

Bunların ikisi de adresler ile işlem yapacağından aynıdır ve orjinal diziyi değiştirirler. Örneğimize bakalım:


_15
#include <stdio.h>
_15
_15
void sayilara_5_ekle(int *dizi, int dizi_uzunlugu) {
_15
for (int i = 0; i < dizi_uzunlugu; ++i) {
_15
dizi[i] += 5;
_15
}
_15
}
_15
_15
int main() {
_15
int sayilar[] = {1,8,3,7,5};
_15
sayilara_5_ekle(sayilar, 5);
_15
_15
for(int i = 0; i < 5; i++)
_15
printf("%d ", sayilar[i]);
_15
}

=== Output:

6 13 8 12 10

Ancak pointer olarak atamak içeriğini değiştirmek istemediğimiz durumlarda (mesela örnekteki gibi en büyük elemanı bulmak istersek) risk oluşturur. Ya yanlışlıkla değiştirirsek? İstenmeyen veri kayıpları olabilir. Bundan kaçınmak için içeriğin değiştirilemeyeceğini belirten const anahtar kelimesini kullanabiliriz:


_15
#include <stdio.h>
_15
_15
int en_buyuk_deger_bul(const int *dizi, int dizi_uzunlugu) {
_15
int en_buyuk_deger = dizi[0];
_15
for (int i = 1; i < dizi_uzunlugu; ++i) {
_15
if (en_buyuk_deger < dizi[i])
_15
en_buyuk_deger = dizi[i];
_15
}
_15
return en_buyuk_deger;
_15
}
_15
_15
int main() {
_15
int sayilar[] = {1,8,3,7,5};
_15
printf("En büyük sayi: %d", en_buyuk_deger_bul(sayilar, 5));
_15
}

=== Output:

En büyük sayi: 8

Matruşka Pointerlar

Bunu sona bırakmak istedim. Pointer'ın en çok kafa karıştıran kısmıdır. Basitçe anlatmaya çalışacağım.

pointer değişkenin adres depoladığını artık biliyoruz. Elbette pointer değişkenlerde bellekte depolanır. Bu sebeple pointer adresi tutan pointer oluşturmak mümkündür. Bir değişken ve pointer oluşturalım:


_2
int yas = 21;
_2
int *yas_ptr = &yas;

Şimdi bu pointer'ın adresini depolayan bir pointer daha oluşturalım:


_1
int **yas_ptr_ptr = &yas_ptr;

Bu da ne, hani tek yıldız ile oluşturuluyordu? Sakin. Hala tek yıldız ile oluşturuluyor. Yukarıda int *yas_ptr_ptr ismini int* yas_ptr_ptr olarak ifade ederek analize başlayalım. int tipinde yas değişkenin adresini bir pointer'a atarsak bunun tipinin int* olduğunu gördük. Şimdi ise int* tipini tutan pointer oluşturmuş oluruz. Yeni değişkenimizin tipi ise int* olur. Biraz daha netleşti değil mi?

Yeni pointer ile ilgili birkaç çıktıya bakalım:


_3
printf("%p\n", yas_ptr_ptr);
_3
printf("%p\n", *yas_ptr_ptr);
_3
printf("%d\n", **yas_ptr_ptr);

=== Output:

0x7fffd4e80798 0x7fffd4e80794 50

İlk satırı biliyoruz yas_ptr_ptr'in adresini yazdırdı. İkinci satırda ise bu pointer'ın değerini, yani yas_ptr değişkeninin adresini yazdırdık. Son satırda ise yas_ptr_ptr pointer değişkeninin adresini depoladığı yas_ptr pointer değişkeninin adresinin içerisinde bulunan değeri yazdırmış olduk. Görsel olarak da eklemiş olayım:

Yeni yazılarda görüşmek üzere, esen kalın.

Kaynaklar

2022 © No rights are reserved.Inspired by Lee Robinson's blog.