D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

bayrak: [flag], bir işlemin veya sonucun geçerli olup olmadığını bildiren bit
bayt: [byte], 8 bitlik tür
bit: [bit], 0 ve 1 değerlerini alabilen en temel bilgi birimi
ifade: [expression], programın değer oluşturan veya yan etki üreten bir bölümü
ikili sayı sistemi: [binary system], iki rakamdan oluşan sayı sistemi
işaretli tür: [signed type], eksi ve artı değer alabilen tür
işaretsiz tür: [unsigned type], yalnızca artı değer alabilen tür
on altılı sayı sistemi: [hexadecimal system], on altı rakamdan oluşan sayı sistemi
yazmaç: [register], mikro işlemcinin en temel iç depolama ve işlem birimi
... bütün sözlük



İngilizce Kaynaklar


Diğer




Bit İşlemleri

Bu bölümde mikro işlemcinin en küçük bilgi birimi olan bitlerle yapılan işlemleri tanıyacağız. Bit işlemleri mikro işlemcinin en temel olanaklarındandır.

Bu işlemler hem alt düzey programcılık açısından bilinmelidir, hem de parametre olarak bayrak alan işlevler için gereklidir. Bayrak alan işlevlere özellikle C kütüphanelerinden uyarlanmış olan D kütüphanelerinde rastlanabilir.

Verinin en alt düzeyde gerçekleştirilmesi

D gibi bir programlama dili aslında bir soyutlamadır. Program içinde tanımladığımız Öğrenci gibi bir kullanıcı türünün bilgisayarın iç yapısı ile doğrudan bir ilgisi yoktur. Programlama dillerinin amaçlarından birisi, donanımın anladığı dil ile insanın anladığı dil arasında aracılık yapmaktır.

Bu yüzden her ne kadar D dilini kullanırken donanımla ilgili kavramlarla ilgilenmek gerekmese de, üst düzey kavramların en alt düzeyde elektronik devre elemanlarına nasıl bağlı olduklarını anlamak önemlidir. Bu konularda başka kaynaklarda çok miktarda bilgi bulabileceğinizi bildiğim için bu başlığı olabildiğince kısa tutacağım.

Transistör

Modern elektronik aletlerin işlem yapma yetenekleri büyük ölçüde transistör denen elektronik devre elemanı ile sağlanır. Transistörün bir özelliği, devrenin başka tarafındaki sinyallerle kontrol edilebilmesidir. Bir anlamda elektronik devrenin kendi durumundan haberinin olmasını ve kendi durumunu değiştirebilmesini sağlar.

Transistörler hem mikro işlemcinin içinde hem de bilgisayarın ana belleğinde çok büyük sayılarda bulunurlar. Programlama dili aracılığıyla ifade ettiğimiz işlemleri ve verileri en alt düzeyde gerçekleştiren elemanlardır.

Bit

Bilgisayarlarda en küçük bilgi birimi bittir. Bit en alt düzeyde bir kaç tane transistörün belirli bir düzende bir araya getirilmesi ile gerçekleştirilir ve veri olarak iki farklı değerden birisini depolayabilir: 0 veya 1. Depoladığı veriyi tekrar değiştirilene kadar veya enerji kaynağı kesilene kadar korur.

Bilgisayarlar veriye bit düzeyinde doğrudan erişim sağlamazlar. Bunun nedeni, her bitin adreslenebilmesinin bilgisayarın karmaşıklığını ve maliyetini çok arttıracak olması ve tek bitlik kavramların desteklenmeye değmeyecek kadar nadir olmalarıdır.

Bayt

Bayt, birbirleriyle ilişkilendirilen 8 bitin bir araya gelmesinden oluşur. Bilgisayarlarda adreslenebilen, yani ayrı ayrı erişilebilen en küçük veri bayttır. Bellekten tek seferde en az bir bayt veri okunabilir ve belleğe en az bir bayt veri yazılabilir.

Bu yüzden, yalnızca false ve true diye iki farklı değer alan ve aslında tek bitlik bilgi taşıyan bool türü bile 1 bayt olarak gerçekleştirilir. Bunu bool.sizeof değerine bakarak kolayca görebiliriz:

    writeln(bool.stringof, ' ', bool.sizeof, " bayttır");
bool 1 bayttır
Yazmaç

Mikro işlemcinin kendi içinde bulunan depolama ve işlem birimleri yazmaçlardır. Yazmaçlar oldukça kısıtlı ama çok hızlı işlemler sunarlar.

Yazmaçlar her işlemcinin bit genişliğine bağlı olan sayıda bayttan oluşurlar. Örneğin, 32 bitlik işlemcilerde yazmaçlar 4 bayttan, 64 bitlik işlemcilerde de 8 bayttan oluşur. Yazmaç büyüklüğü hem mikro işlemcinin etkin olarak işleyebildiği bilgi miktarını hem de en fazla ne kadar bellek adresleyebildiğini belirler.

Programlama dili aracılığıyla gerçekleştirilen her iş eninde sonunda bir veya daha fazla yazmaç tarafından halledilir.

İkili sayı sistemi

Günlük hayatta kullandığımız onlu sayı sisteminde 10 rakam vardır: 0123456789. Bilgisayar donanımlarında kullanılan ikili sayı sisteminde ise iki rakam vardır: 0 ve 1. Bu, bitin iki değer alabilmesinden gelir. Bitler örneğin üç farklı değer alabilseler, bilgisayarlar üçlü sayı sistemini kullanırlardı.

Günlük hayatta kullandığımız sayıların basamakları birler, onlar, yüzler, binler, vs. diye artarak adlandırılır. Örneğin, 1023 gibi bir sayı şöyle ifade edilebilir:

1023 == 1 adet 1000, 0 adet 100, 2 adet 10, ve 3 adet 1

Dikkat ederseniz, sola doğru ilerlendiğinde her basamağın değeri 10 kat artmaktadır: 1, 10, 100, 1000, vs.

Aynı tanımı ikili sayı sistemine taşıyınca, ikili sistemde yazılmış olan sayıların basamaklarının da birler, ikiler, dörtler, sekizler, vs. şeklinde artması gerektiğini görürüz. Yani sola doğru ilerlendiğinde her basamağın değeri 2 kat artmalıdır: 1, 2, 4, 8, vs. Örneğin, 1011 gibi bir ikili sayı şöyle ifade edilebilir:

1011 == 1 adet 8, 0 adet 4, 1 adet 2, ve 1 adet 1

Basamaklar numaralanırken, en sağdaki basamağa (en düşük değerli olan basamağa) 0 numaralı basamak denir. Buna göre, ikili sayı sisteminde yazılmış olan 32 bitlik işaretsiz bir değerin bütün basamaklarını ve basamak değerlerini şöyle gösterebiliriz:

Basamak
Numarası

Değeri
312,147,483,648
301,073,741,824
29536,870,912
28268,435,456
27134,217,728
2667,108,864
2533,554,432
2416,777,216
238,388,608
224,194,304
212,097,152
201,048,576
19524,288
18262,144
17131,072
1665,536
1532,768
1416,384
138,192
124,096
112,048
101,024
9512
8256
7128
664
532
416
38
24
12
01

Yüksek değerli bitlere üst bit, düşük değerli bitlere alt bit denir.

İkili sistemde yazılan hazır değerlerin 0b ile başladıklarını Hazır Değerler bölümünde görmüştük. İkili sistemde değerler yazarak bu tabloya nasıl uyduklarına bakabiliriz. Okumayı kolaylaştırmak için alt çizgi karakterlerinden de yararlanarak:

import std.stdio;

void main() {
    //             1073741824                     4 1
    //             ↓                              ↓ ↓
    int sayı = 0b_01000000_00000000_00000000_00000101;
    writeln(sayı);
}

Çıktısı:

1073741829

Dikkat ederseniz, o hazır değerin içinde rakamı 1 olan yalnızca 3 adet basamak vardır. Yazdırılan değerin bu basamakların yukarıdaki tablodaki değerlerinin toplamı olduğunu görüyoruz: 1073741824 + 4 + 1 == 1073741829.

İşaretli türlerin işaret biti

En üst bit işaretli türlerde sayının artı veya eksi olduğunu bildirmek için kullanılır:

    int sayı = 0b_10000000_00000000_00000000_00000000;
    writeln(sayı);
-2147483648

En üst bitin diğerlerinden bağımsız olduğunu düşünmeyin. Örneğin, yukarıdaki sayı diğer bitlerinin 0 olmalarına bakarak -0 değeri olarak düşünülmemelidir (zaten tamsayılarda -0 diye bir değer yoktur). Bunun ayrıntısına burada girmeyeceğim ve bunun D'nin de kullandığı ikiye tümleyen sayı gösterimi ile ilgili olduğunu söylemekle yetineceğim.

Burada önemli olan, yukarıdaki tabloda gösterilen en yüksek 2,147,483,648 değerinin yalnızca işaretsiz türlerde geçerli olduğunu bilmenizdir. Aynı deneyi uint ile yaptığımızda tablodaki değeri görürüz:

    uint sayı = 0b_10000000_00000000_00000000_00000000;
    writeln(sayı);
2147483648

Bu yüzden, aksine bir neden olmadığı sürece aşağıda gösterilenler gibi bit işlemlerinde her zaman için işaretsiz türler kullanılır: ubyte, ushort, uint, ve ulong.

On altılı sayı sistemi

Yukarıdaki hazır değerlerden de görülebileceği gibi, ikili sayı sistemi okunaklı değildir. Hem çok yer kaplar hem de yalnızca 0 ve 1'lerden oluştuğu için okunması ve anlaşılması zordur. Bu yüzden, daha kullanışlı olan on altılı sayı sistemi yaygınlaşmıştır.

On altılı sayı sisteminde toplam 16 rakam vardır. Alfabelerde 10'dan fazla rakam bulunmadığı için Latin alfabesinden de 6 harf alınmış ve bu sistemin rakamları olarak 0123456789abcdef kabul edilmiştir. O sıralamadan bekleneceği gibi; a, b, c, d, e, ve f harfleri sırasıyla 10, 11, 12, 13, 14, ve 15 değerlerindedir. abcdef harfleri yerine isteğe bağlı olarak ABCDEF harfleri de kullanılabilir.

Yukarıdaki sayı sistemlerine benzer biçimde, bu sistemde sola doğru ilerlendiğinde her basamağın değeri 16 kat artar: 1, 16, 256, 4096, vs. Örneğin, on altılı sistemdeki 8 basamaklı bir sayının basamak değerleri şöyledir:

Basamak
Numarası

Değeri
7268,435,456
616,777,216
51,048,576
465,536
34,096
2256
116
01

On altılı hazır değerlerin 0x ile yazıldıklarını hatırlayarak bir deneme:

    //         1048576 4096 1
    //               ↓  ↓  ↓
    uint sayı = 0x_0030_a00f;
    writeln(sayı);
3186703

Bunun nedenini sayı içindeki sıfır olmayan basamakların katkılarına bakarak anlayabiliriz: 3 adet 1048576, a adet 4096, ve f adet 1. a'nın 10 ve f'nin 15 olduklarını hatırlayarak hesaplarsak: 3145728 + 40960 + 15 == 3186703.

On altılı ve ikili sistemde yazılan sayılar kolayca birbirlerine dönüştürülebilirler. On altılı sistemdeki bir sayıyı ikili sisteme dönüştürmek için, sayının her basamağı ikili sistemde dört basamak olarak yazılır. Birbirlerine karşılık gelen değerler şöyledir:

On altılı İkili Onlu
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
a101010
b101111
c110012
d110113
e111014
f111115

Örneğin, yukarıda kullandığımız 0x0030a00f on altılı değerini ikili olarak şöyle yazabiliriz:

    // on altılı:      0    0    3    0    a    0    0    f
    uint ikili = 0b_0000_0000_0011_0000_1010_0000_0000_1111;

İkili sistemden on altılı sisteme dönüştürmek için de ikili sayının her dört basamağı on altılı sistemde tek basamak olarak yazılır. Yukarıda ilk kullandığımız ikili değer için:

    // ikili:          0100 0000 0000 0000 0000 0000 0000 0101
    uint on_altılı = 0x___4____0____0____0____0____0____0____5;
Bit işlemleri

Değerlerin bitlerle nasıl ifade edildiklerini ve ikili veya on altılı olarak nasıl yazıldıklarını gördük. Şimdi değerleri bit düzeyinde değiştiren işlemlere geçebiliriz.

Her ne kadar bit düzeyindeki işlemlerden bahsediyor olsak da, bitlere doğrudan erişilemediğinden bu işlemler en az 8 biti birden etkilemek zorundadırlar. Örneğin, ubyte türündeki bir ifadenin 8 bitinin hepsi de, ama ayrı ayrı olarak bit işlemine dahil edilir.

Ben üst bitin özel anlamı nedeniyle işaretli türleri gözardı edeceğim ve bu örneklerde uint türünü kullanacağım. Siz buradaki işlemleri ubyte, ushort, ve ulong türleriyle; ve işaret bitinin önemini hatırlamak şartıyla byte, short, int, ve long türleriyle de deneyebilirsiniz.

Önce aşağıdaki işlemleri açıklamada yardımcı olacak bir işlev yazalım. Kendisine verilen sayıyı ikili, on altılı, ve onlu sistemde göstersin:

import std.stdio;

void göster(uint sayı) {
    writefln("  %032b %08x %10s", sayı, sayı, sayı);
}

void main() {
    göster(123456789);
}

Sırasıyla ikili, on altılı, ve onlu:

  00000111010110111100110100010101 075bcd15  123456789
Tersini alma işleci ~

Bu işleç önüne yazıldığı ifadenin bitleri ters olanını üretir. 1 olan bitler 0, 0 olanlar 1 olur:

    uint değer = 123456789;
    göster(değer);
    writeln("~ --------------------------------");
    göster(~değer);

Bu işlecin etkisi ikili gösteriminde çok kolay anlaşılıyor. Her bit tersine dönmüştür:

  00000111010110111100110100010101 075bcd15  123456789
~ --------------------------------
  11111000101001000011001011101010 f8a432ea 4171510506

Bu işlecin bit düzeyindeki etkisini şöyle özetleyebiliriz:

~0 → 1
~1 → 0
Ve işleci &

İki ifadenin arasına yazılır. İki ifadenin aynı numaralı bitlerine sırayla bakılır. Sonuç olarak her iki ifadede de 1 olan bitler için 1 değeri, diğerleri için 0 değeri üretilir.

    uint soldaki = 123456789;
    uint sağdaki = 987654321;

    göster(soldaki);
    göster(sağdaki);
    writeln("& --------------------------------");
    göster(soldaki & sağdaki);

Mikro işlemci bu işlemde her iki ifadenin 31, 30, 29, vs. numaralı bitlerini ayrı ayrı kullanır.

Çıktıda önce soldaki ifadeyi, sonra da sağdaki ifadeyi görüyoruz. Kesikli çizginin altında da bit işleminin sonucu yazdırılıyor:

  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
& --------------------------------
  00000010010110100100100000010001 025a4811   39471121

Dikkat ederseniz, kesikli çizginin altına yazdırdığım sonuç değerde 1 olan bitler çizginin üstündeki her iki ifadede de 1 değerine sahip olan bitlerdir.

Bu işleç bu yüzden ve işleci olarak isimlendirilmiştir: soldaki ve sağdaki bit 1 olduğunda 1 değerini üretir. Bunu bir tablo ile gösterebiliriz. İki bitin 0 ve 1 oldukları dört farklı durumda ancak iki bitin de 1 oldukları durum 1 sonucunu verir:

0 & 0 → 0
0 & 1 → 0
1 & 0 → 0
1 & 1 → 1

Gözlemler:

Veya işleci |

İki ifadenin arasına yazılır. İki ifadenin aynı numaralı bitlerine sırayla bakılır. Her iki ifadede de 0 olan bitlere karşılık 0 değeri üretilir; diğerlerinin sonucu 1 olur:

    uint soldaki = 123456789;
    uint sağdaki = 987654321;

    göster(soldaki);
    göster(sağdaki);
    writeln("| --------------------------------");
    göster(soldaki | sağdaki);
  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
| --------------------------------
  00111111110111111110110110110101 3fdfedb5 1071639989

Dikkat ederseniz, sonuçta 0 olan bitler her iki ifadede de 0 olan bitlerdir. Bitin soldaki veya sağdaki ifadede 1 olması, sonucun da 1 olması için yeterlidir:

0 | 0 → 0
0 | 1 → 1
1 | 0 → 1
1 | 1 → 1

Gözlemler:

Ya da işleci ^

İki ifadenin arasına yazılır. İki ifadenin aynı numaralı bitlerine sırayla bakılır. İki ifadede farklı olan bitlere karşılık 1 değeri üretilir; diğerlerinin sonucu 0 olur:

    uint soldaki = 123456789;
    uint sağdaki = 987654321;

    göster(soldaki);
    göster(sağdaki);
    writeln("^ --------------------------------");
    göster(soldaki ^ sağdaki);
  00000111010110111100110100010101 075bcd15  123456789
  00111010110111100110100010110001 3ade68b1  987654321
^ --------------------------------
  00111101100001011010010110100100 3d85a5a4 1032168868

Dikkat ederseniz, sonuçta 1 olan bitler soldaki ve sağdaki ifadelerde farklı olan bitlerdir. İkisinde de 0 veya ikisinde de 1 olan bitlere karşılık 0 üretilir.

0 ^ 0 → 0
0 ^ 1 → 1
1 ^ 0 → 1
1 ^ 1 → 0

Gözlem:

Değeri ne olursa olsun, aynı değişkenin kendisiyle "ya da"lanması 0 sonucunu üretir:

    uint değer = 123456789;

    göster(değer ^ değer);
  00000000000000000000000000000000 00000000          0
Sağa kaydırma işleci >>

İfadenin değerini oluşturan bitleri belirtilen sayıda basamak kadar sağa kaydırır. Kaydırılacak yerleri olmayan en sağdaki bitler düşerler ve değerleri kaybedilir. Sol taraftan yeni gelen bitler işaretsiz türlerde 0 olur.

Bu örnek bitleri 2 basamak kaydırıyor:

    uint değer = 123456789;
    göster(değer);
    göster(değer >> 2);

Hem sağdan kaybedilecek olan bitleri hem de soldan yeni gelecek olan bitleri işaretli olarak gösteriyorum:

  00000111010110111100110100010101 075bcd15  123456789
  00000001110101101111001101000101 01d6f345   30864197

Dikkat ederseniz, alt satırdaki bitler üst satırdaki bitlerin iki bit sağa kaydırılması ile elde edilmiştir.

Bitler sağa kaydırılırken sol tarafa yeni gelenlerin 0 olduklarını gördünüz. Bu, işaretsiz türlerde böyledir. İşaretli türlerde ise işaret genişletilmesi (sign extension) denen bir yöntem uygulanır ve sayının en soldaki biti ne ise soldan hep o bitin değerinde bitler gelir.

Bu etkiyi göstermek için int türünde ve özellikle üst biti 1 olan bir değer seçelim:

    int değer = 0x80010300;
    göster(değer);
    göster(değer >> 3);

Asıl sayıda üst bit 1 olduğu için yeni gelen bitler de 1 olur:

  10000000000000010000001100000000 80010300 2147549952
  11110000000000000010000001100000 f0002060 4026540128

Üst bitin 0 olduğu bir değerde yeni gelen bitler de 0 olur:

    int değer = 0x40010300;
    göster(değer);
    göster(değer >> 3);
  01000000000000010000001100000000 40010300 1073808128
  00001000000000000010000001100000 08002060  134226016
İşaretsiz sağa kaydırma işleci >>>

Bu işleç sağa kaydırma işlecine benzer biçimde çalışır. Tek farkı işaret genişletilmesinin uygulanmamasıdır. Türden ve en soldaki bitten bağımsız olarak soldan her zaman için 0 gelir:

    int değer = 0x80010300;
    göster(değer);
    göster(değer >>> 3);
  10000000000000010000001100000000 80010300 2147549952
  00010000000000000010000001100000 10002060  268443744
Sola kaydırma işleci <<

Sağa kaydırma işlecinin tersi olarak bitleri belirtilen basamak kadar sola kaydırır:

    uint değer = 123456789;
    göster(değer);
    göster(değer << 4);

En soldaki bit değerleri kaybedilir ve sağ taraftan 0 değerli bitler gelir:

  00000111010110111100110100010101 075bcd15  123456789
  01110101101111001101000101010000 75bcd150 1975308624
Atamalı bit işleçleri

Yukarıdaki işleçlerin ikili olanlarının atamalı karşılıkları da vardır: &=, |=, ^=, >>=, >>>=, ve <<=. Tamsayılar ve Aritmetik İşlemler bölümünde gördüğümüz atamalı aritmetik işleçlerine benzer biçimde, bunlar işlemi gerçekleştirdikten sonra sonucu soldaki ifadeye atarlar.

Örnek olarak &= işlecini kullanalım:

    değer = değer & 123;
    değer &= 123;         // üsttekiyle aynı şey
Anlamları

Bu işleçlerin bit düzeyinde nasıl işledikleri işlemlerin hangi anlamlarda görülmeleri gerektiği konusunda yeterli olmayabilir. Burada bu anlamlara dikkat çekmek istiyorum.

| işleci birleşim kümesidir

İki ifadenin 1 olan bitlerinin birleşimini verir. Uç bir örnek olarak, bitleri birer basamak atlayarak 1 olan ve birbirlerini tutmayan iki ifadenin birleşimi, sonucun bütün bitlerinin 1 olmasını sağlar:

    uint soldaki = 0xaaaaaaaa;
    uint sağdaki = 0x55555555;

    göster(soldaki);
    göster(sağdaki);
    writeln("| --------------------------------");
    göster(soldaki | sağdaki);
  10101010101010101010101010101010 aaaaaaaa 2863311530
  01010101010101010101010101010101 55555555 1431655765
| --------------------------------
  11111111111111111111111111111111 ffffffff 4294967295
& işleci kesişim kümesidir

İki ifadenin 1 olan bitlerinin kesişimini verir. Uç bir örnek olarak, yukarıdaki iki ifadenin 1 olan hiçbir biti diğerini tutmadığı için, kesişimlerinin bütün bitleri 0'dır:

    uint soldaki = 0xaaaaaaaa;
    uint sağdaki = 0x55555555;

    göster(soldaki);
    göster(sağdaki);
    writeln("& --------------------------------");
    göster(soldaki & sağdaki);
  10101010101010101010101010101010 aaaaaaaa 2863311530
  01010101010101010101010101010101 55555555 1431655765
& --------------------------------
  00000000000000000000000000000000 00000000          0
|= işleci belirli bitleri 1 yapar

İfadelerden bir taraftakini asıl değişken olarak düşünürsek, diğer ifadeyi de 1 yapılacak olan bitleri seçen ifade olarak görebiliriz:

    uint ifade = 0x00ff00ff;
    uint birYapılacakBitler = 0x10001000;

    write("önce       :"); göster(ifade);
    write("1 olacaklar:"); göster(birYapılacakBitler);

    ifade |= birYapılacakBitler;
    write("sonra      :"); göster(ifade);

Etkilenen bitlerin önceki ve sonraki durumlarını işaretli olarak gösterdim:

önce       :  00000000111111110000000011111111 00ff00ff   16711935
1 olacaklar:  00010000000000000001000000000000 10001000  268439552
sonra      :  00010000111111110001000011111111 10ff10ff  285151487

birYapılacakBitler değeri, bir anlamda hangi bitlerin 1 yapılacakları bilgisini taşımış ve asıl ifadenin o bitlerini 1 yapmış ve diğerlerine dokunmamıştır.

&= işleci belirli bitleri siler

İfadelerden bir taraftakini asıl değişken olarak düşünürsek, diğer ifadeyi de silinecek olan bitleri seçen ifade olarak görebiliriz:

    uint ifade = 0x00ff00ff;
    uint sıfırYapılacakBitler = 0xffefffef;

    write("önce        :"); göster(ifade);
    write("silinecekler:"); göster(sıfırYapılacakBitler);

    ifade &= sıfırYapılacakBitler;
    write("sonra       :"); göster(ifade);

Etkilenen bitlerin önceki ve sonraki durumlarını yine işaretli olarak gösteriyorum:

önce        :  00000000111111110000000011111111 00ff00ff   16711935
silinecekler:  11111111111011111111111111101111 ffefffef 4293918703
sonra       :  00000000111011110000000011101111 00ef00ef   15663343

sıfırYapılacakBitler değeri, hangi bitlerin sıfırlanacakları bilgisini taşımış ve asıl ifadenin o bitlerini sıfırlamıştır.

& işleci belirli bir bitin 1 olup olmadığını sorgular

Eğer ifadelerden birisinin tek bir biti 1 ise diğer ifadede o bitin 1 olup olmadığı sorgulanabilir:

    uint ifade = 123456789;
    uint sorgulananBit = 0x00010000;

    göster(ifade);
    göster(sorgulananBit);
    writeln(ifade & sorgulananBit ? "evet, 1" : "1 değil");

Asıl ifadenin hangi bitinin sorgulandığını işaretli olarak gösteriyorum:

  00000111010110111100110100010101 075bcd15  123456789
  00000000000000010000000000000000 00010000      65536
evet, 1

Başka bir bitini sorgulayalım:

    uint sorgulananBit = 0x00001000;
00000111010110111100110100010101 075bcd15  123456789
00000000000000000001000000000000 00001000       4096
1 değil

Sorgulama ifadesinde birden fazla 1 kullanarak o bitlerin hepsinin birden asıl ifadede 1 olup olmadıkları da sorgulanabilir.

Sağa kaydırmak ikiye bölmektir

Sağa bir bit kaydırmak değerin yarıya inmesine neden olur. Bunu yukarıdaki basamak değerleri tablosunda görebilirsiniz: bir sağdaki bit her zaman için soldakinin yarısı değerdedir.

Sağa birden fazla sayıda kaydırmak o kadar sayıda yarıya bölmek anlamına gelir. Örneğin 3 bit kaydırmak, 3 kere 2'ye bölmek, yani sonuçta 8'e bölmek anlamına gelir:

    uint değer = 8000;

    writeln(değer >> 3);
1000

Ayrıntısına girmediğim ikiye tümleyen sisteminde sağa kaydırmak işaretli türlerde de ikiye bölmektir:

    int değer = -8000;

    writeln(değer >> 3);
-1000
Sola kaydırmak iki katını almaktır

Basamaklar tablosundaki her bitin, bir sağındakinin iki katı olması nedeniyle, bir bit sola kaydırmak 2 ile çarpmak anlamına gelir:

    uint değer = 10;

    writeln(değer << 5);

Beş kere 2 ile çarpmak 32 ile çarpmanın eşdeğeridir:

320
Bazı kullanımları
Bayraklar

Bayraklar birbirlerinden bağımsız olarak bir arada tutulan tek bitlik verilerdir. Tek bitlik oldukları için var/yok, olsun/olmasın, geçerli/geçersiz gibi iki değerli kavramları ifade ederler.

Her ne kadar böyle tek bitlik bilgilerin yaygın olmadıklarını söylemiş olsam da bazen bir arada kullanılırlar. Bayraklar özellikle C kütüphanelerinde yaygındır. C'den uyarlanan D kütüphanelerinde bulunmaları da beklenebilir.

Bayraklar bir enum türünün birbirleriyle örtüşmeyen tek bitlik değerleri olarak tanımlanırlar.

Bir örnek olarak araba yarışıyla ilgili bir oyun programı düşünelim. Bu programın gerçekçiliği kullanıcı seçimlerine göre belirlensin:

Oyun sırasında bunlardan hangilerinin etkin olacakları bayrak değerleriyle belirtilebilir:

enum Gerçekçilik {
    benzinBiter        = 1 << 0,
    hasarOluşur        = 1 << 1,
    lastiklerEskir     = 1 << 2,
    lastikİzleriOluşur = 1 << 3
}

Dikkat ederseniz, o enum değerlerinin hepsi de birbirleriyle çakışmayan tek bitten oluşmaktadırlar. Her değer 1'in farklı sayıda sola ötelenmesi ile elde edilmiştir. Bit değerlerinin şöyle olduklarını görebiliriz:

benzinBiter        : ...0001
hasarOluşur        : ...0010
lastiklerEskir     : ...0100
lastikİzleriOluşur : ...1000

Hiçbir bit diğerlerininkiyle çakışmadığı için bu değerler | ile birleştirilebilir ve hep birden tek bir değişkende bulundurulabilir. Örneğin, yalnızca lastiklerle ilgili ayarların etkin olmaları istendiğinde değer şöyle kurulabilir:

    Gerçekçilik ayarlar = Gerçekçilik.lastiklerEskir
                          |
                          Gerçekçilik.lastikİzleriOluşur;
    writefln("%b", ayarlar);

Bu iki bayrağın bitleri aynı değer içinde yan yana bulunurlar:

1100

Daha sonradan, programın asıl işleyişi sırasında bu bayrakların etkin olup olmadıkları & işleci ile denetlenir:

    if (ayarlar & Gerçekçilik.benzinBiter) {
        // ... benzinin azalmasıyla ilgili kodlar ...
    }

    if (ayarlar & Gerçekçilik.lastiklerEskir) {
        // ... lastiklerin eskimesiyle ilgili kodlar ...
    }

& işlecinin sonucu, ancak belirtilen bayrak ayarlar içinde de 1 ise 1 sonucunu verir.

if koşuluyla kullanılabilmesinin bir nedeni de Tür Dönüşümleri bölümünden hatırlayacağınız gibi, sıfır olmayan değerlerin otomatik olarak true'ya dönüşmesidir. & işlecinin sonucu 0 olduğunda false, farklı bir değer olduğunda da true değerine dönüşür ve bayrağın etkin olup olmadığı böylece anlaşılmış olur.

Maskeleme

Bazı kütüphanelerde ve sistemlerde belirli bir tamsayı değer içine birden fazla bilgi yerleştirilmiş olabilir. Örneğin, 32 bitlik bir değerin üst 3 bitinin belirli bir anlamı ve alt 29 bitinin başka bir anlamı bulunabilir. Bu veriler maskeleme yöntemiyle birbirlerinden ayrılabilirler.

Bunun bir örneğini IPv4 adreslerinde görebiliriz. IPv4 adresleri ağ paketleri içinde 32 bitlik tek bir değer olarak bulunurlar. Bu 32 bitin 8'er bitlik 4 parçası günlük kullanımdan alışık olduğumuz noktalı adres gösterimi değerleridir. Örneğin, 192.168.1.2 gibi bir adres, 32 bit olarak 0xc0a80102 değeridir:

c0 == 12 * 16 + 0 = 192
a8 == 10 * 16 + 8 = 168
01 ==  0 * 16 + 1 =   1
02 ==  0 * 16 + 2 =   2

Maske, ilgilenilen veri ile örtüşen sayıda 1'lerden oluşur. Asıl değişken bu maske ile "ve"lendiğinde, yani & işleci ile kullanıldığında verinin değerleri elde edilir. Örneğin, 0x000000ff gibi bir maske değeri ifadenin alt 8 bitini olduğu gibi korur, diğer bitlerini sıfırlar:

    uint değer = 123456789;
    uint maske = 0x000000ff;

    write("değer:"); göster(değer);
    write("maske:"); göster(maske);
    write("sonuç:"); göster(değer & maske);

Maskenin seçerek koruduğu bitleri işaretli olarak gösteriyorum. Diğer bütün bitler sıfırlanmıştır:

değer:  00000111010110111100110100010101 075bcd15  123456789
maske:  00000000000000000000000011111111 000000ff        255
sonuç:  00000000000000000000000000010101 00000015         21

Bu yöntemi 0xc0a80102 IPv4 adresine ve en üst 8 biti seçecek bir maskeyle uyguladığımızda noktalı gösterimdeki ilk adres değerini elde ederiz:

    uint değer = 0xc0a80102;
    uint maske = 0xff000000;

    write("değer:"); göster(değer);
    write("maske:"); göster(maske);
    write("sonuç:"); göster(değer & maske);

Maskenin üst bitleri 1 olduğundan, değerin de üst bitleri seçilmiş olur:

değer:  11000000101010000000000100000010 c0a80102 3232235778
maske:  11111111000000000000000000000000 ff000000 4278190080
sonuç:  11000000000000000000000000000000 c0000000 3221225472

Ancak, sonucun onlu gösterimi beklediğimiz gibi 192 değil, 3221225472 olmuştur. Bunun nedeni, maskelenen 8 bitin değerin en sağ tarafına kaydırılmalarının da gerekmesidir. O 8 biti 24 bit sağa kaydırırsak birlikte ifade ettikleri değeri elde ederiz:

    uint değer = 0xc0a80102;
    uint maske = 0xff000000;

    write("değer:"); göster(değer);
    write("maske:"); göster(maske);
    write("sonuç:"); göster((değer & maske) >> 24);
değer:  11000000101010000000000100000010 c0a80102 3232235778
maske:  11111111000000000000000000000000 ff000000 4278190080
sonuç:  00000000000000000000000011000000 000000c0        192
Problemler
  1. Verilen IPv4 adresinin noktalı gösterimini döndüren bir işlev yazın:
    string noktalıOlarak(uint ipAdresi) {
        // ...
    }
    
    unittest {
        assert(noktalıOlarak(0xc0a80102) == "192.168.1.2");
    }
    
  2. Verilen 4 değeri 32 bitlik IPv4 adresine dönüştüren bir işlev yazın:
    uint ipAdresi(ubyte bayt3,    // en yüksek değerli bayt
                  ubyte bayt2,
                  ubyte bayt1,
                  ubyte bayt0) {  // en düşük değerli bayt
        // ...
    }
    
    unittest {
        assert(ipAdresi(192, 168, 1, 2) == 0xc0a80102);
    }
    
  3. Maske oluşturan bir işlev yazın. Belirtilen bit ile başlayan ve belirtilen uzunlukta olan maske oluştursun:
    uint maskeYap(int düşükBit, int uzunluk) {
        // ...
    }
    
    unittest {
        assert(maskeYap(2, 5) ==
               0b_0000_0000_0000_0000_0000_0000_0111_1100);
        //                                            ↑
        //                             başlangıç biti 2
        //                             ve 5 bitten oluşuyor
    }