Bilgisayar Hafızasını Fethet 3: Kopyala, Yapıştır

Mehmed Fatih Bayraktar
5 min readAug 17, 2023

--

Yerde kapağı açık bir diz üstü bilgisayar, solunda buruşturulmuş üç post-it, sağında yarım dolu bir fincan kahve, iki kurşun kalem, bir boş post-it ve bir de buruşuk post-it
Photo by Lauren Mancke on Unsplash

Önceki yazımızda memset fonksiyonuyla hafızaya bir kapı açmıştık. Bu fonksiyon ile hafızada bir adresten başlayarak istediğimiz bir karakteri istediğimiz sayıda yazıyorduk. Yani memory set işlemi yapıyorduk. Bu yazımızdaki asıl konumuza geçmeden önce yeni sistemlerde kullanılması istenmeyen, eski sitemlerde karşılaşılabilecek bzero fonksiyonundan da bahsetmek istiyorum.

void *bzero(void *dest, size_t len);

Bu fonksiyon strings.h kütüphanesinde tanımlanmış bir fonksiyondur. Fonksiyonun ne yaptığını detaylı anlatmak yerine aşağıya bu fonksiyonun tam olarak yaptığını yapan bir kod bırakacağım. Detaylarını önceki yazımda bulabilirsiniz.

void *ft_memset(void *dest, int c, size_t len) {
for (size_t i = 0; i < len; ++i)
((char *)dest)[i] = c; // char * cast
return dest;
}

void *ft_bzero(void *dest, size_t len) {
return ft_memset(dest, 0, len);
}

Kısaca hafızanın istediğimiz alanını istediğimiz bir karakterle değil de, ASCII tablosundaki ondalık değeri 0 olan NULL karakteriyle dolduruyor. Bu işleme hafızayı reset etmek de diyebiliriz.

Şimdi hafızaya bir şeyler yazabiliyoruz. Peki bunların bir kısmını hafızanın başka bir alanına olduğu gibi kopyalamak istersek bunu nasıl yapabiliriz? Bunun için memcpy diye bir fonksiyonumuz bulunmaktadır.

void *memcpy(void *dst, const void *src, size_t len);

Bu fonksiyon string.h kütüphanesinde tanımlanmış bir fonksiyondur. Fonksiyon, kopyalama işlemini yaptıktan sonra dst adresini döndürür. İlk argümanı olan dst hedef adrestir. İkinci argüman src ise kaynak adrestir. Üçüncü parametre ile de len adet karakter kopyalamak istediğimizi belirtiyoruz. Yine fonksiyonun bir benzerini kendimiz yazarak nasıl çalıştığını anlamaya çalışalım.

#include <unistd.h>
#include <stdio.h>

void *ft_memcpy(void *dst, const void *src, size_t len) {
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (unsigned char *)src;

if (!d && !s)
return NULL;
while (len) {
*(d++) = *(s++);
--len;
}
return dst;
}

int main(void) {
char *tst[7] = {42, 42, 43, 43, 44, 44, 0}; // "**++,,"
char *dst[4] = {42, 42, 42, 0}; // "***"
char *src[4] = {43, 43, 43, 0}; // "+++"

printf("dst: %s\n", ft_memcpy(dst, src, 2)); // dst: ++*
printf("tst: %s\n", ft_memcpy(tst + 2, tst, 4)); // tst: ******
}

Fonksiyon, aşağıdaki algoritmayı gerçekleştirir:

  1. Hafızadaki değerleri birer baytlık karakterler şeklinde okuyup yazabilmek için dst ve src adreslerinin veri tiplerini dönüştürerek adresleri sırasıyla d ve s işaretçilerine ata.
  2. Hem d, hem s değerlerinin ikisi de boşsa, yani dst ve src parametrelerinden bir adres değeri gelmemişse, hiç bir işlem yapmadan geriye NULL döndür.
  3. Bir döngü başlat ve len değeri sıfır oluncaya kadar tekrarla. Sıfır olduğunda yedinci adıma atla.
  4. Hafızanın s adresine gidip içeriğini oku ve d adresine gidip okuduğun değeri oraya yaz.
  5. Adres değerlerini birer arttır.
  6. Döngü koşulu olan len değerini bir azaltarak üçüncü adıma dön.
  7. Sonuç olarak dst adresini döndür.

Algoritmayı test etmek için main fonksiyonunda üç ayrı karakter dizisini gösteren hafıza adresleri tanımladık. Fonksiyonu kullanarak iki ayrı test yaptık. Birincisinde src adresinden 2 karakteri dst adresine kopyaladık. İkincisinde ise tst adresinden başlayarak 4 karakteri tst+2 adresine kopyaladık. Bu kopyalama işlemini daha iyi anlamak için aşağıdaki gibi görsel hale getirerek döngünün her adımında hafıza ne durumda görelim.

Başlangıç durumu:
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * + + , ,
=============================================
1. iterasyon (len->4):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * * + , , -> tst+2 = tst+0
=============================================
2. iterasyon (len->3):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * * * , , -> tst+3 = tst+1
=============================================
3. iterasyon (len->2):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * * * * , -> tst+4 = tst+2
=============================================
4. iterasyon (len->1):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * * * * * -> tst+5 = tst+3

Görüldüğü gibi kaynak ve hedef hafıza bölümlerinin çakıştığı durumda bu algoritma, kaynak verinin bir kısmının kaybolmasına sebep olabiliyor. Peki kaynak veriyi kaybetmeden kopyalamanın bir yolu var mıdır? Evet. Yine string.h kütüphanesindeki memmove fonksiyonu bu amaçla kullanılır.

void *memmove(void *dst, const void *src, size_t len);

Bu fonksiyon memcpy ile aynı argümanlara sahiptir ve aynı şekilde dst adresini geri döndürür. Neyi farklı yaptığını anlamak için fonksiyonu kendimiz gerçekleştirelim.

#include <unistd.h>
#include <stdio.h>

void *ft_memcpy(void *dst, const void *src, size_t len);

void *ft_memmove(void *dst, const void *src, size_t len) {
if (dst < src)
ft_memcpy(dst, src, len);
else {
--len;
while (len) {
((unsigned char *)dst)[len] = ((unsigned char *)src)[len];
--len;
}
((unsigned char *)dst)[0] = ((unsigned char *)src)[0];
}
return dst;
}

int main(void) {
char *tst[7] = {42, 42, 43, 43, 44, 44, 0}; // "**++,,"
char *dst[4] = {42, 42, 42, 0}; // "***"
char *src[4] = {43, 43, 43, 0}; // "+++"

printf("dst: %s\n", ft_memmove(dst, src, 2)); // dst: ++*
printf("tst: %s\n", ft_memmove(tst + 2, tst, 4)); // tst: ****++
}

Eğer dst adresi src adresinden önce geliyorsa, çakışma durumu olsa bile, bir veri kaybı söz konusu olmaz. Bu durumda memcpy ile aynı işlemi yaparız. Aksi durumda verileri soldan sağa değil, sağdan sola kopyalarız. Yine döngü adımlarını görselleştirerek farkı daha iyi anlayalım.

Başlangıç durumu:
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * + + , , -> len = len - 1
=============================================
1. iterasyon (len->3):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * + + , + -> tst+5 = tst+3
=============================================
2. iterasyon (len->2):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * + + + + -> tst+4 = tst+2
=============================================
3. iterasyon (len->1):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * + * + + -> tst+3 = tst+1
=============================================
döngüden çıktıktan sonra (len->0):
tst+0 tst+1 tst+2 tst+3 tst+4 tst+5
----- ----- ----- ----- ----- -----
* * * * + + -> tst+2 = tst+0

Böylece veri kaybı olmadan kopyalama işlemini yapmış olduk.

Önceki yazılarımızda metin (string) verilerin C programlama dilinde char tipinde değerlerden oluşan bir dizi (array) olduğundan bahsetmiştik. Bu yazımızda incelediğimiz iki fonksiyon veri tipinden bağımsız olarak hafızadaki değerleri bayt-bayt kopyalıyor. Yazımızın bu son bölümünde doğrudan metin veriler üzerinde işlem yapan bazı fonksiyonları inceleyeceğiz: strlen, strlcpy, strlcat. Fonksiyonların üçü de string.h kütüphanesinde tanımlıdır.

İlk metin fonksiyonumuz olan strlen, argüman olarak bir metin verisinin hafızadaki başlangıç adresini alır. Sonuç olarak metnin kaç karakterden oluştuğunu yani kaç bayt olduğunu döner. Metin deyince NULL (ASCII ondalık değeri 0 olan) karakterle sonlanmış bir karakter dizisini kastettiğimizi unutmayalım.

size_t strlen(const char *s);

Geri dönüş değerinin tipi unistd.h kütüphanesinde tanımlı olan unsigned long tipinin kısa ismi size_t ile verilmiştir. Metin, hafızayı dolduracak kadar büyük olabilir. Bu da maksimum 4,294,967,295 bayt olabilir. Haydi metnin karakterlerini saymak için kendi fonksiyonumuzu yazalım.

#include <unistd.h>
#include <stdio.h>

size_t ft_strlen(const char *s) {
size_t i;

for (i = 0; s[i]; ++i);
return (i);
}

int main(void) {
printf("%lu\n", ft_strlen("12345"));
return 0;
}

Fonksiyona gönderilen “12345” metni hafızaya yazılır ve başlangıç adresi argüman olarak s parametresine atanır. Sonra fonksiyon, döngü içerisinde o adresten başlayarak sıfırdan farklı bir değer bulduğu müddetçe i sayacını sıfırdan başlayarak arttırır. Beş karakterli bir metin oluşturulduğunda altıncı karakter (i->5) NULL olur. Böylece s[5]->0 olduğu için döngü biter ve metnin büyüklüğü olan 5 değeri geri döndürülür.

Yazımızı çok uzatmadan şimdilik burada duralım. Bir sonraki yazımızda metinler üzerinde daha fazla kontrol sağlayan diğer fonksiyonları incelemeye devam edeceğiz.

--

--

Mehmed Fatih Bayraktar

Master in Computer Engineering, working for Turkcell as Principal Data Analytics Developer and instructor at Turkcell Akademi