Bir önceki yazımda C’de yer alan standart başlık dosyalarına değinip matematiksel fonksiyonların ayrıntılarına girmiştim. Bu yazıda da farklı alanlarda kullanabileceğimiz faydalı standart kütüphane fonksiyonlarını görmeye devam edeceğiz. Bu seferki konumuze ise “rastgelelik”. Rastgelelik aslında olayların belli bir şablonunun veya öngörülebilirliğinin olmama olayıdır.[1] Dünyada gerçekten rastgeleliğin var olup olmadığı hala tartışma konusudur. Ancak ben bu derin konulara girmeden doğrudan yazılım dünyasında rastgelelikten bahsedip konunun C ile ilgili bağlantısına gireceğim.
Bilgisayarlar deterministik makinelerdir. Bunun anlamı, bilgisayarların yaptığı her bir işlemin aslında mantıksal bir karara bağlı olmasıdır. Bilgisayarlarda gerçekleşen bazı işlemler (özellikle birden fazla Thread kullanılan sistemlerde) rastgele gerçekleşiyor gibi görünse de aslında bilgisayarlarda bulunan mantıksal devrelerin kararları sonucu bu işlemler gerçekleşmektedir. Yani bir işlemi aynı koşullar altında aynı şekilde gerçekleştirirseniz, yine aynı sonucu alacaksınızdır. Ancak koşullar ve ortam parametreleri bazen öylesine fazla olur ki bunu yapmanız çok zor hale gelir.
Buradan yola çıkarak bu sistemlerde gerçek bir “rastgelelik” oluşturmak aslında mümkün değildir. Ancak günümüzde ortaya çıkan kuantum bilgisayarlar ile bu işin derecesi bir tık arttırılmıştır. Biz burada klasik makinelerdeki durumları inceleyeceğiz. Öncelikle bilgisayarlarda rastgele sayıların nasıl üretildiğinden biraz bahsetmemiz gerekmektedir. Aslında bu konu üniversitede başlı başına bir ders olma niteliği taşır. Ancak ben burada genel hatlarından bahsedeceğim. Bilgisayarlarda rastgele sayılar oluşturmak için önce belli kavramları bilmemiz gerekmektedir. İlk olarak rastgele sayı üreticilerden başlayalım.
Rastgele Sayı Üreticiler (Random Number Generators, RNG) mantıksal olarak tahmin edilemeyen sayı veya sembol dizisi üreten yazılımsal veya donanımsal araçlara verilen isimdir. Bu araçların yaptığı işe ise Rastgele Sayı Üretimi (Random Number Generation) adı verilir. Rastgele sayı üreticiler eğer fiziksel bir formda ise onlara Donanımsal Rastgele Sayı Üreticiler (Hardware Random-Number Generators, HRNGS) adı verlirken, yazılımsal bir formda ise onlara Sözde Rastgele Sayı Üreticiler (Pseudorandom Number Generators, PRNGS) adı verilmektedir. Sözde rastgele sayı üreticilerin ürettiği rastgele sayılar deterministik olduğundan, bu üreticilerin ilk durumları hakkında bilgi sahibi olduğunuzda, genellikle bütün üretilecek seriyi tahmin etmeniz mümkündür.[2]
Sözde rastgele sayılar doğal olarak belli algoritmalar yardımıyla üretilirler. Geçmişten günümüze bu konu ile ilgili pek çok algoritma geliştirilmiştir ve geliştirilmeye devam edilmektedir. Bu metodlardan en eskisi ve belki de en kolay anlaşılanı 1949 yılında John von Neumann tarafından bir konferansta açıklanan Orta-Kare Yöntemi (Middle-Square Method) isimli algoritmadır. Bu algoritmada öncelikle “n” boyutlu bir çekirdek değeri (seed value) belirlenir ve bu değerin karesi alınır. Daha sonra ortaya çıkan sayının ortasında yer alan “n” rakam alınır ve bu hem ilk çıktı hem de bir sonraki çekirdek değeri olur. Bu algoritma siz rastgele sayı üretmeyi bırakıncaya kadar bu şekilde devam eder.
Burada gördüğünüz gibi aslında çoğu rastgele sayı üretme algoritmasında Çekirdek Değeri (Seed Value) adı verilen bir değer kullanılmaktadır. Bu aslında algoritmamızın anahtarıdır. Çünkü eğer baştaki çekirdek değerini tahmin ederseniz ve algoritma ile ilgili çeşitli parametreleri bilirseniz (yukarıdaki “n” sayısı gibi), rastgele sayı serisini tahmin etmeniz çok da zor olmayacaktır. Bu nedenle yazılım dünyasında oluşturduğumuz rastgele sayılarda çekirdek değerini tahmin edilemez hale getirmek oldukça önemlidir. Birazdan C’de rastgele sayıları nasıl oluşturacağımızı anlatırken bu değer ile ilgili bilgileri de vereceğim.
Algoritmalar ile rastgele sayıları nasıl oluşturabileceğimizi ve rastgele sayılarla ilgili temel kavramları anladık. Peki yazılım dünyasında rastgele sayılara neden ihtiyaç duyarız. Programlamaya ilk başlayan kişiler için bazen bu konu çok gereksiz görülebilmektedir. Ancak tecrübe kazandıkça bu konunun aslında ne kadar önemli olduğunu ve ne kadar fazla yerde kullanıldığını anlarsınız. Ben şimdilik size yazılım dünyasında rastgele sayıların kullanıldığı uygulamalardan birkaç örnek vererek, gerisini sizin araştırmanıza bırakmak istiyorum:
- Bilgisayarlardaki kriptografik algoritmaların implementasyonu
- Simülasyon sistemlerinde olağan dışı durumların analizi
- Oyunlarda (özellikle kart oyunlarında) öğelerin her oyunun başında farklı sıralanması
- İstatistiksel algoritmaların implementasyonu
- Yazılımda birim testlerinde rastgele durumların oluşturulması
Rastgelelik ve rastgele sayı üretimi ile ilgili bence bu kadar teorik bilgi şimdilik yeterlidir. Daha önce bahsettiğim gibi bu konu oldukça kapsamlı ve eğlenceli bir konudur. İnternet üzerinde daha fazla araştırma ile bu konunun ayrıntılarına ulaşabilirsiniz. Şimdi C dilinde rastgele sayıları nasıl ele alabileceğimiz konusuna geçelim. Geçen matematiksel fonksiyonların math.h başlık dosyasında bildirildiğini söylemiştik. Rastgele sayılar ile ilgili fonksiyonlar ve sabitler ise stdlib.h başlık dosyasında yer alır. Yani bu fonksiyonları kullanmadan önce başlık dosyasını kaynak koda #include ile dahil etmeniz gerekmektedir. İlk olarak rand fonksiyonu ile başlayalım. Öncelikle bu fonksiyonun prototipi şöyledir:
int rand();
Basit bir şekilde anlatmak gerekirse, bu fonksiyon çağrıldığı anda 0 ile RAND_MAX sabiti arasında olan rastgele bir tamsayı geri döndürmektedir. RAND_MAX yine stdlib.h başlık dosyasında yer alan bir sabittir. Bu sabitin değeri implementasyona göre değişiklik göstermekle birlikte en az 32767 değerine sahip olması garanti altına alınmıştır. Kendi sisteminizde bu sabitin değerini merak ediyorsanız onu yazdırmayı deneyebilirsiniz:
printf("%d\n", RAND_MAX);
Genellikle bu değer sisteminizdeki int türünün boyutu ile doğru orantılı olur. Benim sistemimde int türü 4 byte yer kaplamaktadır ve yukarıdaki çağrının çıktısı 2147483647 olarak çıkmaktadır. Bu sayı sistemimde int türünün alabileceği maksimum değerdir. Ancak implementasyonun her zaman bu kurala uyma gereği yoktur. Şimdi rand fonksiyonunu kullanarak rastgele bir sayı oluşturalım:
int res = rand();
printf("%d\n", res);
Bende bu kodu çıktısı 1804289383 olarak çıkmaktadır. Şimdilik muhtemelen sizde de aynı sonuç çıkacaktır. Bu konuya birazdan geleceğim. Peki birden fazla rastgele sayı elde etmek istediğimizde ne yapacağız? Bunun için bu fonksiyonu sürekli çağırabilirsiniz. Örneğin for döngüsü kullanarak 5 adet rastgele sayıyı ekrana basalım:
int i;
for(i=0; i<5; ++i)
{
printf("%d\n", rand());
}
846930886
1681692777
1714636915
1957747793
Artık bu noktada bir şeyin farkına varmış olmanız gerekiyor. Ben bu kodu ne zaman çalıştırırsam çalıştırayım hep ilk olarak 1804289383 sayısını elde ediyorum. İnsan burada bu fonksiyon gerçekten çalışıyor mu diye endişe etmiyor değil. Yukarıdaki kodda 5 yerine 10 yadığımda ise şu çıktıyı elde ediyorum:
846930886
1681692777
1714636915
1957747793
424238335
719885386
1649760492
596516649
1189641421
Gördüğünüz gibi programı her çalıştırdığımda aynı rastgele sayı serisini elde ediyorum. Bu durumun sebebi başta anlattığım çekirdek değeri ile ilgilidir. Program her seferinde aynı çekirdek değeri ile başladığından, aynı sayı serisini elde ediyoruz. Peki bu durumu değiştirme imkanımız var mı? Elbette C bu konuda bize yardım ediyor, çünkü etmezse rastgele sayı üretmemizin hiçbir anlamı kalmamaktadır. Yine stdlib.h başlık dosyasında yer alan srand isimli fonksiyon ile çekirdek değerini değiştirmemiz mümkünüdür. Bu fonksiyonun prototipi şöyledir:
void srand(unsigned seed);
Buradaki seed değeri C’de kulllanılan rastgele sayı üreticisini besleyen ilk çekirdek değer olacaktır. Yani önce bu fonksiyon ile bir çekirdek değeri ayarlayıp daha sonra rand fonksiyonunu çağırdığımızda farklı bir sayı serisi elde ederiz. Çekirdek değeri herhangi bir unsigned int türünden değer olabilir. Şimdi başta for ile yazdığımız kodu şu şekilde değiştirelim:
srand(3);
int i;
for(i=0; i<5; ++i)
{
printf("%d\n", rand());
}
483147985
844158168
953350440
612121425
Evet, bu sefer farklı bir seri elde etmeyi başardık. Burada çekirdek değeri olarak 3 değerini kullandık. Herhangi bir srand çağrısı yapılmadan kullanılan rand çağrılarında, çekirdek değeri 1 olarak ele alınmaktadır. Bu durumu yukarıdaki kodda 3 yerine 1 yazarak doğrulayabilirsiniz. Şimdi burada başka bir sorunumuz daha var. Biz her ne kadar baştaki sayı serisini değiştirmiş olsak da aynı kodu tekrar tekrar çalıştırdığımızda bu sefer sürekli en son sayı serisini elde etmiş olacağız. Bu durumda ya srand değerini her rand çağrısından önce güncellemeliyiz ya da programı her başlattığımızda farklı bir çekirdek değeri kullanmalıyız. srand çağrısını önce şu şekilde yazarak sonucu inceleyelim:
int i;
for(i=0; i<5; ++i)
{
srand(3);
printf("%d\n", rand());
}
1205554746
1205554746
1205554746
1205554746
Ekranda 5 defa aynı değeri görmemizin sebebi, çekirdek değerinin sürekli olarak aynı değere güncellenmesi ve sayı serisinin ilerleme kaydedemeden yeniden başlamasından kaynaklanmaktadır. Bu elbette istediğimiz sonuç değildir. Peki programı her çalıştırdığımızda farklı bir çekirdek değeri ile nasıl başlarız? Bu sorunun üstesinden gelmenin en bilinen yöntemi zamanı kullanmaktır. time.h içerisinde bildirimi yer alan time fonksiyonu ile 1 Ocak 1970 yılının 00:00:00 anından şu ana kadar geçen saniye sayısını elde edebiliriz. Bu yıldan günümüze kadar geçen zaman Unix Zamanı (Unix Time), Epoch Zamanı (Epoch Time) veya POSIX Zamanı (POSIX Time) gibi isimler alır. time fonksiyonun prototipi şöyledir:
time_t time(time_t *arg);
Buradaki arg parametresi geri dönen zamanı depolayacak olan değişkenin adresini almaktadır. Buraya argüman olarak şimdilik sadece NULL göndermemiz yeterlidir. Biz direk olarak time_t türünden dönen geri dönüş değerini kullanacağız. time_t türü C’de zaman değerlerini tutmak için oluşturulan ve time.h başlık dosyasında yer alan bir türdür. Bu türün eşleniği (yani int veya başka bir tür olması) implementasyona göre değişir. C standartlarında belirtilmese bile bu tür genellikle Unix zamanını tutabilecek genişlikte bir tam sayı türü olmaktadır.
Sistemimizdeki Epoch zamanı programı her çalıştırdığımızda değişiklik gösterdiği için, rastgele sayı üretiminde çekirdek değeri olarak oldukça faydalı bir iş görmektedir. Şimdi bu fonksiyonu srand ile nasıl kullanabileceğimize bakalım:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
time_t epoch = time(NULL);
srand(epoch);
printf("%d\n", rand());
return 0;
}
Şimdi artık programı her çalıştırdığınızda farklı bir rastgele sayı değeri veya rastgele sayı serisi elde etmiş olacaksınız. Böylelikle çekirdek değer sorununu çözmüş olduk. Yukarıdaki srand ve time kullanımını şu şekilde kısa olarak yazmanız da mümkündür:
srand(time(NULL));
Son olarak bir konuya daha değinelim. Artık rastgele sayılarımızı güzel ve “rastgele” bir şekilde oluşturabiliyoruz. Ancak bu sayılar çok değişik büyüklüklerde olabiliyor ki çoğu zaman bu işimizi görmemektedir. Bu sayıları bizim istediğimiz aralıklara nasıl getirebiliriz? Elbette “%” operatörü ile bunu sağlamamız çok kolaydır. rand fonksiyonundan çıkan sonucun “%” operatörü ile modunu alarak, sonucu istediğimiz aralığa getirmemiz mümkündür. Örneğin 0 ile 10 arasında (10 dahil değil) 6 adet rastgele sayı oluşturmak istersek şöyle bir kod yazmamız yeterlidir:
srand(time(NULL));
int i;
for(i=0; i<6; ++i)
{
int current = rand() % 10;
printf("%d ", current);
}
Oluşturacağımız sayıların illaki 0’dan başlamasına gerek yoktur. Diyelim ki 1 ile 6 arasında (yani zar değerlerinde) rastgele sayılar oluşturmak istiyoruz. Bu durumda aşağıdaki yöntem ile bunu yapmamız mümkündür:
int dice = (rand() % 6) + 1;
Yani bunu formülize edecek olursak, C’de “x” ile “y” arasında kalan (bu sayılar da dahil) rastgele tam sayılar oluşturmak istediğimizde, şu formül ile bunu yapabiliriz:
(rand() % (y – x + 1)) + x
Örneğin 7 ile 13 arasında rastgele sayılar oluşturmak istediğimizde:
int x = rand() % (13 – 7 + 1) + 7;
Şimdi örnek olarak küçük bir oyun programlayarak yazımızı tamamlayalım. Bu oyunda kullanıcıdan öncelikle bir uzunluk değeri alıyoruz. Bu uzunluk değerine göre bir minimum adım sayısı hesaplanıyor. Daha sonra bu adım sayısı kadar zar atıp toplamda bu uzunluk değerine veya onun üstüne ulaşmaya çalışıyoruz. Eğer bunu başarırsak oyunu kazanıyor, başaramazsak kaybediyoruz. Oyun oldukça basit bir mantık içerse de tutturmak gerçekten zor olabiliyor. Şimdi kodumuzu ve çıktımızı inceleyelim (ben denediğimde başaramadım):
// Code 5.4.1: Code5_4_1.c
// Minimum sayıda zar atarak hedefe ulaşma oyunu
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int rollDice(void);
int main()
{
srand(time(NULL));
int targetValue, targetSteps;
printf("Please enter target length value: ");
scanf("%d", &targetValue);
targetSteps = (targetValue % 6 == 0) ? targetValue/6 : targetValue/6 + 1;
printf("Minimum target step count: %d\n", targetSteps);
getchar();
while(targetSteps)
{
printf("%d step remains, please press ENTER to roll dice!", targetSteps);
getchar();
int currentValue = rollDice();
printf("Current dice value: %d\n", currentValue);
targetValue -= currentValue;
printf("%d lenght remains!\n", targetValue);
targetSteps--;
}
if(targetValue <= 0)
{
printf("You won game!\n");
}
else
{
printf("You lost game! %d length still remains!\n", targetValue);
}
return 0;
}
int rollDice()
{
return (rand() % 6) + 1;
}
Minimum target step count: 4
4 step remains, please press ENTER to roll dice!
Current dice value: 6
14 lenght remains!
3 step remains, please press ENTER to roll dice!
Current dice value: 2
12 lenght remains!
2 step remains, please press ENTER to roll dice!
Current dice value: 5
7 lenght remains!
1 step remains, please press ENTER to roll dice!
Current dice value: 3
4 lenght remains!
You lost game! 4 length still remains!
Gördüğünüz gibi atılan zarlar sırasıyla 6, 2, 5 ve 3 olarak geldi. Bu sayıların toplamı 16 olduğundan, hedeflediğimiz 20 sayısına ulaşamadığımız için oyunu kaybettik. Kodu incelediğinizde “rollDice” isimli bir fonksiyon görüyoruz. Bu bize rastgele bir zar sayısı döndürmeye yarıyor. 12. satıraki srand fonksiyonu ile de programın her açılışında farklı bir sayı serisi almayı başarıyouz. 17. satırda kullanıcıdan hedef değeri alıyor ve 19. satırda minimum adım sayısını hesaplıyoruz. Burada eğer hedef değeri 6’nın tam katı ise adım sayısı onun 6’ya bölümüne, değilse onun 6’ya bölümünün 1 fazlasına eşit oluyor. Döngüye girmeden önceki “getchar” fonksiyonunun anlamını şimdilik atlayabilirsiniz.
Döngü içinde her adımda “getchar” ile kullanıcıdan bir tuşa basmasını istiyor ve kalan adım sayısını yazdırıyoruz. Döngü en fazla bu adım sayısı kadar dönüyor. Döngünün her adımında “rollDice” fonksiyonundan geri dönen değeri hedef değerden çıkarıyor ve 33. satırda kalan değeri ekrana basıyoruz. Zar atma işlemleri bitip döngüden çıktığımızda ise 38. satırda hedefin sıfırdan büyük olup olmadığına göre oyunun durumunu kullanıcıya bildiriyor ve programı sonlandırıyoruz. İşte bu gibi durumlarda rastgele sayıların kullanımını ve anlamını daha rahat görebilirsiniz.
Bu yazıda rastgelelik kavramını ve yazılım dünyasında rastgele sayıların nasıl üretildiğini inceledik. Daha sonra C’de sözde rastgele sayılar oluşturabileceğiniz çeşitli fonksiyonları gördük ve bunlarla ilgili çeşitli durumları nasıl çözebileceğimize değindik. En son ise bütün konular ile alakalı bir örnek yapmaya çalıştık. Artık ömür ve faaliyet alanı gibi C’nin bir diğer temel konularını incelemeye devam edebiliriz.




güzeeel