8 Eylül 2018 Cumartesi

Offset Kavramı ve Dinamik Dizi Yapımı

Birçok programlama dilinde dinamik dizi kullanabiliyoruz.Dizilerde [] indexer operator yardımıyla dizi elemanlarına erişebiliyoruz.
C++

   int* myArray = new int[2];
   myArray[0] = 10;
   delete[] myArray;
C#

   int[] myArray = new int[2];
   myArray[0] = 10;
Java

   int[] myArray = new int[2];
   myArray[0] = 10;
Delphi

var
  MyArray: array of Integer;
begin
  SetLength(MyArray, 2);
  MyArray[0] := 10;
end.
Yukarıdaki örnekleri incelediğimizde C++, C#, Java gibi dillerde new operatörü, delphi için ise SetLength fonksiyonu yardımıyla dinamik dizileri kullanabiliyoruz.

C dilinde ise; C++, C#, java da bulunan new operatoru ya da delphi'deki gibi bir SetLength fonksiyonu bulunmuyor.Dolayısı ile dinamik dizimizi kendimiz gerçekleştirmemiz gerekiyor.
Aşağıdaki örnekte; 8 byte'lık bir hafıza bloğu tahsis edilip, myArray işaretçisinin [] indexer operatörü yardımıyla değerlere erişiyoruz.

 int* myArray = malloc(sizeof(int) * 2);
 myArray[0] = 10;
 myArray[1] = 20;
 free(myArray);
Sanırım nihayet konuya giriş yapabilmek için gerekli alt yapıyı sağlamış oldum :)

Peki dil C, C++, C#, java ya da delphi ya da bir başka dil farketmeksizin myArray[index] operatorü ile dizi elemanlarına erişmek istediğimde, hafıza bloğunda ilgili index'deki veriye nasıl eriştiğini biliyor muyum ? Bilmiyorum.



Bu yazıda dil seviyesinde index mantığını nasıl gerçekleştirebiliriz bunu anlamaya ve öğrenmeye çalışacağız. Bilindiği üzere C, C++, C#, Delphi gibi dillerde, işaretçileri(pointer), işaretçi aritmetiğini(pointer math) kullanabiliyoruz.Java'da ise yine bildiğim kadarıyla şuan için işaretçi kullanımına dil seviyesinde izin verilmiyor.

İşaretçi(pointer) kullanımına izin veren dillerde index'leme mantığını işaretçiler üzerinden verinin adresini hesaplayarak gerçekleştireceğiz.

Dinamik bellek yöneten fonksiyonlar veriyi hafızada tahsis ettikten sonra geriye, veri bloğunun başlangıç adresini untyped pointer(void* veya PVOID veya LPVOID veya Pointer veya IntPtr) tipinde döndürmektedir. C

void *        _RTLENTRY _EXPFUNC malloc(_SIZE_T __size);
Windows API

WINBASEAPI
_Ret_maybenull_
_Post_writable_byte_size_(dwBytes)
DECLSPEC_ALLOCATOR
LPVOID
WINAPI
HeapAlloc(
    _In_ HANDLE hHeap,
    _In_ DWORD dwFlags,
    _In_ SIZE_T dwBytes
    );
C#

IntPtr p = Marshal.AllocHGlobal(size);
Delphi

procedure GetMem(var P: Pointer; Size: Integer);

function HeapAlloc(hHeap: THandle; dwFlags, dwBytes: DWORD): Pointer; stdcall;
UnTyped Pointer'larda aritmetik işlemlere izin verilmediği için, fonksiyonlardan dönen veri tipini typed pointer'lara çevireceğiz. Örneğin C dilinde aşağıdaki gibi untyped pointer tipinde ki p değişkeni için p += 4; işlemine izin verilmeyecektir.

void* p = malloc(sizeof(int) * 2);
p += 4;
[bcc32 Error] main.c(17): E2453 Size of the type 'void' is unknown or zero
Error C2036 'void *': unknown size

Örneğin Delphi dili için aşağıdaki gibi untyped pointer tipide ki P := P + 4; ya da Inc(P,4); işlemine izin verilmeyecektir.

var
  P: Pointer;
begin
  GetMem(P, SizeOf(Integer) * 2);
  P := P + 4;
end.
[dcc32 Error] Project2.dpr(60): E2015 Operator not applicable to this operand type
ya da

var
  P: Pointer;
begin
  GetMem(P, SizeOf(Integer) * 2);
  Inc(P, 4);
end.
[dcc32 Error] Project2.dpr(70): E2001 Ordinal type required
Dolayısı ile hafıza adreslerini index yoluyla hesaplayabilmemiz için typed pointerları kullanmamız gerekiyor.
C

 int* p = malloc(sizeof(int) * 2);
 *p = 10;
  p += 4;
 *p = 20;
free(p);
Delphi

var
  P: ^Integer; // ya da PInteger
begin
  GetMem(P, SizeOf(Integer) * 2);
  P^ := 10;
  Inc(P, 4);
  P^ := 20;
  FreeMem(P, SizeOf(Integer) * 2);
end;
C#

public static IntPtr Add(IntPtr pointer, int offset);
C#'da ise IntPtr(Pointer ya da Handle veri tipi için) yapısında bulunan Add methodu yardımıyla hesaplama yapabiliyoruz.

static void Main(string[] args)
{
  IntPtr p = Marshal.AllocHGlobal(sizeof(int) * 2);
  Marshal.WriteInt32(p, 10);
  IntPtr newP = IntPtr.Add(p, 4);
  Marshal.WriteInt32(newP, 20);
  Marshal.FreeHGlobal(p);
}
Yukarıdaki gibi hafıza bloğunun başlangıç adresini gösteren bir işaretçi üzerinde "doğrudan" değişiklik yapan p += 4; ya da P := P + 4; gibi aritmetik işlemler yazacağımız index'leme mantığı için çok doğru olmayabilir.O nedenle dinamik bellekte veriye işaret eden işaretçi üzerinde doğrudan bir değişiklik yapmadan yeni adresi hesaplayabiliriz.

Basit bir hesap ile formülümüz aşağıdaki gibi olacaktır.
Kaç Bayt = index X Veri tipinin kapladığı alan(Byte)
Yeni Adres = Başlangıç adresi + Kaç Byte

Kabaca başlangıç adresine kaç byte ekleyeceğimizi index yoluyla hesaplayarak verinin offset'ini hesaplamış olduk. Formülü kodlayacak olursak;
C

int index = 0;
int* p = malloc(sizeof(int) * 2);
int* newp = p + (index * sizeof(int));
*newp = 10;
free(p);
Delphi

var
  P, NewP: PInteger;
  Index: Integer;
begin
  Index := 0;
  GetMem(P, SizeOf(Integer) * 2);
  NewP := PInteger(PByte(P) + (Index * SizeOf(Integer)));
  NewP^ := 10;
  FreeMem(P, SizeOf(Integer) * 2);
end;
index değişkeninin değerini değiştirerek dizinin elemanlarına erişim sağlayabiliriz. Yazdığımız kodları eleştirecek olursak; Kodumuz çok da yakışıklı olmadı.
Örneğin; index * sizeof(int) yerine bir offset fonksiyonu yazabiliriz. C

int Offset(int index){
  return index * sizeof(int);
}
int* newp = p + Offset(index);
Delphi

function Offset(Index: Integer): Integer;
begin
  Result := Index * SizeOf(Integer);
end;
NewP := PInteger(PByte(P) + Offset(Index));
Dizinin eleman sayısından, hafızada tahsis edilecek veri bloğununun kaplayacağı alanı hesaplayan bir fonksiyon yazılabilir.
C

int* p = malloc(sizeof(int) * 2);
yerine

int MemSize(int count) {
   return sizeof(int) * count;
}
int* p = malloc(MemSize(2));
Delphi

GetMem(P, SizeOf(Integer) * 2); 
yerine

function MemSize(Count: Integer): Integer;
begin
  Result := SizeOf(Integer) * Count;
end;
GetMem(P, MemSize(2));
Eleştirmeye devam edecek olursak;
Şuana kadar yazdığımız kodların tamamı int veri tipi üzerine idi. Programlama sadece int veri tipinden ibaret değil ki ?
Başka veri tipleri için ne yapacağız ?
Aynı mantığı her veri tipi için tekrar tekrar mı yazacağız ?

İşte tam bu noktada, algoritmayı veri tipinden soyutlamak ya da bağımsız kılmak için, nesne yönelimli programlamanın sağladığı faydalardan yararlanabiliriz. Bunun için C++'da templates, C# ve Delphi'de generics'ler ile daha esnek kodlamalar yapabiliriz.Generics ve templates ile her ne kadar veri tipini esnek hale getirmiş olsak da, algoritmamız her veri tipi için uygun olmayabilir.Bu nokta da generics constraint'ler ile belli tipler için kurallar getirebiliriz. Tabi ki yine C'de untyped pointerlar ile daha generic fonksiyonlar mümkün.

Bahsettiğim generic haline Dynamic Array Implementation adresinden erişebilir, katkı sağlayabilir (mümkünse), gördüğünüz eksik ya da yanlışlar konusunda bilgilendirirseniz sevinirim.

Sağlıcakla.

Hiç yorum yok:

Yorum Gönder