|
C++ Nedir??
Programlarımızın kolay yazılır, doğru, bakımı kolay, ve
belli sınırlar içerisinde verimli olmalarını isteriz.
Bundan, doğal olarak C++'ı (ve başka dilleri de) bu
amaca olabildiğince yakın olarak kullanmamız gerekliliği
ortaya çıkar. C++ kullananların hâlâ Standart C++'ın
getirdiği olanakları benimsemeyerek, C++ kullanım
biçemlerini değiştirmediklerine; ve bu nedenle,
değindiğim amaca yönelik büyük adımların atılamadığına
inanıyorum. Bu yazı, Standart C++'ın getirdiği
olanakların değil, bu olanakların desteklediği
programlama biçemlerinin üzerinde durmaktadır.
Büyük gelişmelerin anahtarı, kod büyüklüğünü ve
karmaşıklığını kitaplıklar kullanarak azaltmaktır.
Bunları aşağıda, C++'a giriş kurslarında
karşılaşılabilecek birkaç örnek kullanarak gösteriyor ve
nicelendiriyorum.
Kod büyüklüğünü ve karmaşıklığını azaltarak hem
geliştirme zamanını azaltmış oluruz, hem program
bakımını kolaylaştırırız, hem de program sınamanın
bedelini düşürürüz. Daha da önemlisi, C++'ın
öğrenilmesini de kolaylaştırmış oluruz. Önemsiz veya bir
dersten yalnızca geçer not almak için yazılan
programlarda bu basitleştirme yeterlidir. Ancak,
verimlilik, profesyonel programcılar için çok önemli bir
konudur. Programlama biçemimizi, ancak günümüz bilişim
hizmetlerinde ve işyerlerinde karşılaşılan boyutlardaki
verilerle ve gerçek zaman programlarıyla uğraşırken
verimlilik kaybına neden olmayacaksa değiştirebiliriz.
Onun için, karmaşıklığın azaltılmasının verimliliği
düşürmeden elde edilebileceğini gösteren ölçümler de
sunuyorum.
Son olarak, bu görüşün C++'ın öğrenilmesi ve
öğretilmesine olan etkilerini tartışıyo
KARMAŞIKLIK
Bir
programlama dilini öğrenirken görülen ilk çalışma
programlarından birisi olabilecek şu örneği ele alalım:
"Lütfen adınızı girin" yazın
adı okuyun
"Merhaba " yazın
Standart C++ çözümü şöyledir:
#include
// standart giris/cikis
#include // standart dizgi
int
main()
{
using namespace std; // standart kitapliga erisim
cout <<
"Lütfen adinizi girin:\n";
string ad;
cin >> ad;
cout << "Merhaba " << ad << '\n';
}
Programcılığa yeni başlayan birisine bazı temelleri
anlatmamız gerekir: 'main()' nedir? '#include' ne
demektir? 'using' ne işe yarar? Ek olarak, '\n'in ne
yaptığı ve noktalı virgülün nerelerde kullanıldığı gibi
ayrıntıları da anlamamız gerekir. Yine de bu programın
temeli kavramsal olarak kolay, ve soru metninden ancak
gösterim açısından farklı. Tabii dilin gösterimini de
öğrenmemiz gerekir. Ama bu da kolay: 'string' bir dizgi,
'cout' çıkış, '<<' çıkışa yazı göndermekte kullanılan
bir işleç. Karşılaştırmak için, geleneksel C biçemiyle
yazılmış çözüme bakalım. (Hoş göründükleri için değişmez
bildirilerini ve yorumları C++ türünde yazdım. ISO
standardına uygun C yazmak için '#define' ve '/* */'
yorumları kullanılmalıdır.)
#include // standart giris/cikis
int
main()
{
const int encok = 20;
char ad[encok];
printf("Lutfen adinizi girin:\n");
scanf("%s", ad); // ady oku
printf("Merhaba %s\n", ad);
return
0;
}
Dizileri
ve '%s'i de açıklamak gerektiği için bu programın
içeriği, C++ eşdeğerinden az da olsa daha karmaşık. Asıl
sorun, bu basit C çözümünün düşük nitelikli olması. Eğer
birisi sihirli sayı 19'dan (belirtilen 20 sayısından C
dizgilerinin sonlandırma karakterini çıkartarak) daha
fazla harfli bir ad girerse program bozulur.
Daha sonradan uygun bir çözüm gösterildiği sürece bu
niteliksizliğin zararsız olduğu öne sürülebilir. Ancak
bu ifade "iyi" olmak yerine, olsa olsa "kabul
edilebilir" olabilir. Yeni bir programcıya bu kadar
kırılgan bir program göstermemek çok daha iyidir.
Peki davranış olarak C++ eşdeğerine yakın bir C programı
nasıl olurdu? İlk deneme olarak dizi taşmasını
'scanf()'i daha doğru kullanarak engelleyebilirdik:
#include // standart giris/cikis
int
main()
{
const int encok = 20;
char ad[encok];
printf("Lutfen adinizi girin:\n");
scanf("%19s", ad); // adi en fazla 19 harf olarak oku
printf("Merhaba %s\n", ad);
return
0;
}
'scanf()'in biçim dizgisinde ad dizisinin boyutunu
gösteren 'encok'un simgesel şeklini kullanmanın standart
bir yolu olmadığı için, tamsayı '19'u yazıyla yazmak
zorunda kaldım. Bu hem kötü bir programlama biçemi, hem
de program bakımı için bir kabustur. Bunu önlemenin
oldukça ileri düzey sayılacak bir yolu var; ama bunu
programlamaya yeni başlayan birisine açıklamaya
yeltenmem bile:
char
bicim[10];
sprintf(bicim, "%%%ds", encok-1); // bicim dizgisini
hazirla; %s tasabilecegi icin
scanf(bicim, ad);
Dahası bu
program, fazladan yazılan harfleri de gözardı eder. Asıl
istediğimiz, dizginin girdiyle orantılı olarak
büyümesidir. Bunu sağlayabilmek için daha alt düzey bir
soyutlamaya inip karakterlerle tek tek ilgilenmek
gerekir:
#include
#include
#include
void
cik() // hatayi ilet ve programdan cik
{
fprintf(stderr, "Bellekte yer kalmadi\n");
exit(1);
}
int
main()
{
int encok = 20;
char * ad = (char *)malloc(encok); // arabellek ayir
if (ad == 0) cik();
printf("Lütfen adýnýzý girin:\n");
while
(true) { // bastaki boþluklari atla
int c = getchar();
if (c == EOF) break; // kutuk sonu
if (!isspace(c)) {
ungetc(c,stdin);
break;
}
}
int i =
0;
while (true) {
int c = getchar();
if (c == '\n' || c == EOF) { // sonlandirma karakterini
ekle
ad[i] = 0;
break;
}
ad[i] = c;
if (i==encok-1) { // arabellek doldu
encok = encok+encok;
ad = (char*)realloc(ad,encok); // daha buyuk yeni bir
arabellek ayir
if (ad == 0) cik();
}
i++;
}
printf("Merhaba %s\n", ad);
free(ad); // arabellegi birak
return 0;
}
Bir
öncekiyle karşılaştırıldığında bu program çok daha
karmaşık. Çalışma programında istenmediği halde baştaki
boşlukları atlayan kodu yazdığım için kendimi biraz kötü
hissediyorum. Ne var ki, olağan olan, boşlukları
atlamaktır; zaten programın eşdeğerleri de bunu
yapıyorlar.
Bu örneğin o kadar da kötü olmadığı öne sürülebilir.
Zaten birçok deneyimli C ve C++ programcısı gerçek bir
programda herhalde (umarız?) buna benzer birşey
yazmıştır. Daha da ileri giderek, böyle bir programı
yazamayacak birisinin profesyonel bir programcı olmaması
gerektiğini bile ileri sürebiliriz. Bu programın yeni
başlayan birisini ne kadar zorlayacağını düşünün.
Program bu şekliyle dokuz değişik standart kitaplık
işlevi kullanmakta, oldukça ayrıntılı karakter düzeyinde
giriş işlemleriyle uğraşmakta, işaretçiler kullanmakta,
ve bellek ayırmayla ilgilenmektedir. Hem 'realloc()'u
kullanıp hem de uyumlu kalabilmek için 'malloc()'u
kullanmak zorunda kaldım ('new'ü kullanmak yerine).
Bunun sonucu olarak da işin içine bir de arabellek
boyutları ve tür dönüşümleri girmiş oldu. (C'nin bunun
için tür dönüşümünü açıkça yazmayı gerektirmediğini
biliyorum. Ama onun karşılığında ödenen bedel, 'void
*'dan yapılan güvensiz bir örtülü tür dönüşümüne izin
vermektir. Onun için C++, böyle bir durumda tür
dönüşümünün açıkça yapılmasını gerektirir.) Bellek
tükendiğinde tutulacak en iyi yolun ne olduğu bu kadar
küçük bir programda o kadar açık değil. Konuyu fazla
dallandırmamak için kolay anlaşılır bir yol tuttum. C
biçemini kullanan bir öğretmen, bu konuda ilerisi için
temel oluşturacak ve kullanımda da yararlı olacak uygun
bir yol seçmelidir.
Özetlersek, başta verdiğimiz basit örneği çözmek için,
çözümün özüne ek olarak, döngüleri, koşulları, bellek
boyutlarını, işaretçileri, tür dönüşümlerini, ve bellek
yönetimini de tanıtmak zorunda kaldım. Bu biçemde ayrıca
hataya elverişli birçok olanak da var. Uzun deneyimimin
yardımıyla bir eksik, bir fazla, veya bellek ayırma
hataları yapmadım. Ama bir süredir çoğunlukla C++'ın
akım giriş/çıkışını kullanan birisi olarak, yeni
başlayanların çokça yaptıkları hatalardan ikisini
yaptım: 'int' yerine 'char'a okudum ve EOF'la
karşılaştırmayı unuttum. C++ standart kitaplığının
bulunmadığı bir ortamda çoğu öğretmenin neden düşük
nitelikli çözümü yeğleyip bu konuları sonraya bıraktığı
anlaşılıyor. Ne yazık ki çoğu öğrenci düşük nitelikli
biçemin "yeterince iyi" olduğunu ve ötekilerden (C++
olmayan biçemler içinde) daha çabuk yazıldığını
hatırlıyor. Sonuçta da vazgeçilmesi güç bir alışkanlık
edinip arkalarında yanlışlarla dolu programlar
bırakıyorlar.
İşlevsel eşdeğeri olan C++ programı 10 satırken, son C
programı tam 41 satır. Programların temel öğelerini
saymazsak fark, 30 satıra karşın 4 satır. Üstelik C++
programındaki satırlar hem daha kısa, hem de daha kolay
anlaşılır. C++ ve C programlarını anlatmak için gereken
toplam kavram sayısını ve bu kavramların
karmaşıklıklarını nesnel olarak ölçmek zor. Ben C++
biçeminin 10'a 1 daha kazançlı olduğunu düşünüyorum.
VERİMLİLİK
Yukarıdaki gibi basit bir programın verimliliği o kadar
önemli değildir. Böyle programlarda önemli olan,
basitlik ve tür güvenliğidir. Verimliliğin çok önemli
olduğu parçalardan oluşabildikleri için, gerçek
sistemler için "üst düzey soyutlamayı kabul edebilir
miyiz" sorusu doğaldır.
Verimliliğin önemli olduğu sistemlerde bulunabilecek
türden basit bir örneği ele alalım:
belirsiz sayıda öğe oku
öğelerin her birisine bir şey yap
öğelerin hepsiyle bir şey yap
Aklıma gelen en basit örnek, girişten okunacak bir dizi
çift duyarlıkla kayan noktaya sanyınyan ortalama ve orta
değerlerini bulmak. Bunun geleneksel C gibi yapılan bir
çözümü böyle olurdu:
// C biciminde cozum
#include
#include
int karsilastir(const void * p, const void * q) //
qsort()'un kullandigi karsilastirma islevi
{
register double p0 = *(double*)p; // sayilari
karsilastir
register double q0 = *(double*)q;
if(p0>q0) return 1;
if (p0 return 0;
}
void cik() // hatayi ilet ve programdan cik
{
fprintf(stderr, "Bellekte yer kalmadi\n");
exit(1);
}
int main(int argc, char* argv[])
{
int boyut = 1000; // ayrimin baslangic boyutu
char* kutuk = argv[2];
double* arabellek =
(double*)malloc(sizeof(double)*boyut);
if (arabellek==0) cik();
double orta = 0;
double ortalama = 0;
int adet = 0; // toplam öge sayisi
FILE* giris=fopen(kutuk, "r"); // kutugu ac
double sayi;
while(fscanf(giris, "%lg", &sayi) == 1) { // sayiyi oku,
ortalamayi deðistir
if (adet==boyut) {
boyut += boyut;
arabellek = (double*)realloc(arabellek,
sizeof(double)*boyut);
if (arabellek==0) cik();
}
arabellek[adet++] = sayi;
// olasi yuvarlatma hatasi:
ortalama = (adet==1) ? sayi : ortalama + (sayi -
ortalama) / adet;
}
qsort(arabellek, adet, sizeof(double), karsilastir);
if (adet) {
int ortadaki = adet / 2;
orta = (adet % 2) ? arabellek[ortadaki] :
(arabellek[ortadaki - 1] + arabellek[ortadaki]) / 2;
}
printf("toplam öge = %d, orta deger = %g, ortalama =
%g\n", adet, orta, ortalama);
free(arabellek);
}
Karşılaştırmasını yapabilmek için C++ biçimini de
veriyorum:
// C++ standart kitapligini kullanan cozum
#include
#include
#include
#include
using namespace std;
int main(int argc, char * argv[])
{
char * kutuk = argv[2];
vector arabellek;
double orta = 0;
double ortalama = 0;
fstream giris(kutuk, ios::in); // kutugu ac
double sayi;
while (giris >> sayi) {
arabellek.push_back(sayi);
// olasý yuvarlatma hatasý:
ortalama = (arabellek.size() == 1) ? sayi : ortalama +
(sayi - ortalama) / arabellek.size();
}
sort(arabellek.begin(), arabellek.end());
if (arabellek.size()) {
int ortadaki = arabellek.size() / 2;
orta = (arabellek.size() % 2) ? arabellek[ortadaki] :
(arabellek[ortadaki-1]+arabellek[ortadaki]) / 2;
}
cout << "toplam öge = " << arabellek.size()
<< ", orta deger = " << orta << ", ortalama = " <<
ortalama << '\n';
}
Program büyüklüklerindeki fark bir önceki örnekte
olduğundan daha az: boş satırları saymayınca, 43'e
karşılık 25. Satır sayıları, 'main()'in bildirilmesi ve
orta değerin hesaplanması gibi ortak satırları (13
satır) çıkartınca 20'ye karşılık 12 oluyor. Okuma ve
depolama döngüsü ve sıralama gibi önemli bölümler C++
çözümünde çok daha kısa: okuma ve depolama için 9'a
karşılık 4, sıralama için 9'a karşılık 1. Daha da
önemlisi, düşünce şekli çok daha basit olduğu için, C++
programının öğrenilmesi de çok daha kolay.
Tekrar belirtirsem, bellek yönetimi C++ programında
örtülü olarak yapılıyor; yeni öğeler 'push_back'le
eklendikçe 'vector' gerektiğinde kendiliğinden büyüyor.
C gibi yazılan programda bu işin 'realloc()'
kullanılarak açıkça yapılması gerekir. Aslında, C++
programında kullanılan 'vector'ün kurucu ve 'push_back'
işlevleri, C gibi yazılan programdaki 'malloc()',
'realloc()', ve ayrılan belleğin büyüklüğüyle uğraşan
kod satırlarının yaptıkları işleri örtülü olarak
yapmaktadırlar. C++ gibi yazılan programda belleğin
tükenme olasılığını C++'ın kural dışı durum işleme
düzeneğine bıraktım. Belleğin bozulmasını önlemek için C
gibi yazılan programda bunu açıkça yazdığım sınamalarla
yaptım.
C++ programına doğru olarak oluşturmak da daha kolaydı.
İşe bazı satırları C gibi yazılan programdan
kopyalayarak başladım. kütüğünü içermeyi unuttum, iki
yerde 'adet'i 'arabellek.size()'la değiştirmeyi unuttum,
derleyicim yerel 'using' yönergelerini desteklemediği
için 'using namespace std;' satırına 'main()'in dışına
taşıdım. Program bu dört hatayı düzeltmemin ardından
hatasız olarak çalıştı.
Programlamaya yeni başlayanlar 'qsort()'u biraz "garip"
bulurlar. Öğe sayısının belirtilmesi neden gereklidir?
(Çünkü C dizileri bunu bilmezler.) 'double'ın
büyüklüğünün belirtilmesi neden gereklidir? (Çünkü
'qsort()' 'double'ları sıralamakta olduğunu bilmez.) O
hoş gözükmeyen 'double' karşılaştırma işlevini neden
yazmak zorundayız? (Çünkü 'double' sıraladığını bilmeyen
'qsort()'a karşılaştırmayı yaptırması için bir işlev
gerekir.) 'qsort()'un kullandığı karşılaştırma işlevinin
bağımsız değişkenleri neden 'char*' türünde değil de
'const void*' türündedir? (Çünkü 'qsort()'un sıralaması
dizgi olmayan türden değişkenler üzerinedir.) 'void*'
nedir ve 'const' olmasa ne anlama gelir? (E, şey, buna
daha sonra değineceğiz.) Bunu yeni başlayanın boş
bakışlarıyla karşılaşmadan anlatmak oldukça zordur.
Bununla karşılaştırıldığında, sort(v.begin(),
v.end())'in ne yaptığını anlatmak çok kolay: "Bu durumda
'sort(v)' kullanmak daha kolay olurdu ama bazen bir
kabın yalnızca bir aralığındaki öğeleri sıralamak
istediğimiz için, daha genel olarak, sıralanacak
aralığın başını ve sonunu belirtiriz."
Programların verimliliklerini karşılaştırmadan önce,
bunu anlamlı kılmak için kaç tane öğe kullanılması
gerektiğini belirledim. Öğe sayısı 50.000 olduğunda
programların ikisi de işlerini yarım saniyenin altında
bitirdiler. Onun için programları 500.000 ve 5.000.000
öğeyle çalıştırdım.
|
Kayan noktalı
sayıları okumak, sıralamak, ve yazmak |
|
|
Eniyileştirmeden |
Eniyileştirerek |
|
|
C++ |
C |
C/C++ oranı |
C++ |
C |
C/C++ oranı |
|
500.000 öğe |
3.5 |
6.1 |
1.74 |
2.5 |
5.1 |
2.04 |
|
5.000.000 öğe |
38.4 |
172.6 |
4.49 |
27.4 |
126.6 |
4.62 |
Burada önemli olan değerler, oranlardır: birden büyük
değerler C++ programının daha hızlı olduğu anlamına
geliyor. Dil, kitaplık, ve programlama biçemi
karşılaştırmalarının ne kadar güç olduğu bilinen bir
gerçektir. Onun için, bu basit denemeden kesin sonuçlar
çıkartmayın. Değerler, üzerinde iş yapılmayan bir
bilgisayarda birçok değerin ortalaması alınarak bulundu.
Değişik değerler arasındaki sapma %1'den daha azdı.
Ayrıca C gibi yazılan programların ISO C'ye sadık olan
uyarlamalarını kullandım. Bekleneceği gibi, bu
programların hızları C gibi yazılan C++ programlarının
hızlarından farklı çıkmadı.
C++ gibi yazılan programların çok az farkla daha hızlı
çıkacaklarını bekliyordum. Ama başka gerçeklemeler
kullandığım zaman sonuçlarda büyük oynamalar gördüm.
Hatta bazı durumlarda, küçük sayıda öğeler
kullanıldığında, C gibi yazılan program C++ gibi yazılan
programdan daha hızlı çıktı. Ancak, bu örneği
kullanarak, üst düzey soyutlamaların ve hatalara kaşı
daha iyi korunmanın günümüz teknolojisiyle kabul edilir
hızlarda elde edilebileceğini göstermeye çalıştım. Salt
araştırma konusu olmayan, yaygın, ve ucuz olarak elde
edilebilen bir gerçekleme kullandım. Daha da yüksek
hızlara ulaştığını söyleyen gerçeklemeler de var.
Kolaylık elde etmek ve hatalara karşı daha iyi korunmak
için; 3, 10, hatta 50 kat fazla ödemeyi kabul eden
insanlar bulmak güç değildir. Bunlara ek olarak iki kat,
dört kat gibi bir hız kazancı da olağanüstü. Bence bu
değerler, bir C++ kitaplık firması için kabul edilir en
düşük değerler olmalıdır.
Programların kullandıkları sürenin nerelerde
geçirildiğini anlamak için birkaç deneme daha yaptım:
|
500.000 öğe |
|
|
Eniyileştirmeden |
Eniyileştirerek |
|
|
C++ |
C |
C/C++ oranı |
C++ |
C |
C/C++ oranı |
|
okuma |
2.1 |
2.8 |
1.33 |
2.0 |
2.8 |
1.40 |
|
üretme |
.6 |
.3 |
.5 |
.4 |
.3 |
.75 |
|
okuma-sıralama |
3.5 |
6.1 |
1.75 |
2.5 |
5.1 |
2.04 |
|
üretme-sıralama |
2.0 |
3.5 |
1.75 |
.9 |
2.6 |
2.89 |
Doğal olarak, "okuma" yalnızca okumayı, "okuma ve
sıralama" hem okumayı hem de okumanın sıraya dizilmesini
gösteriyor. Ayryca, veri girişine ödenen bedeli daha iyi
görebilmek için "üretme," okumak yerine sayıları rasgele
üretiyor.
|
5.000.000 öğe |
|
|
Eniyileştirmeden |
Eniyileştirerek |
|
|
C++ |
C |
C/C++ oranı |
C++ |
C |
C/C++ oranı |
|
okuma |
21.5 |
29.1 |
1.35 |
21.3 |
28.6 |
1.34 |
|
üretme |
7.2 |
4.1 |
.57 |
5.2 |
3.6 |
.69 |
|
okuma-sıralama |
38.4 |
172.6 |
4.49 |
27.4 |
126.6 |
4.62 |
|
üretme-sıralama |
24.4 |
147.1 |
6.03 |
11.3 |
100.6 |
8.90 |
Başka örneklerden ve gerçeklemelerden gördüklerime
dayanarak C++'yn akım giriş/çıkışının C'nin standart
giriş/çıkışından daha yavaş olacağını bekliyordum. Bu
programın 'fstream' yerine standart giriş 'cin'i
kullanan daha önceki bir uyarlamasında gerçekten de
böyle olmuştu. Bunun bir nedeni, standart giriş 'cin'le,
standart çıkış 'cout' arasındaki bağın kullandığım
gerçeklemede kötü işlemesiydi. Yine de bu değerler, C++
giriş/çıkışının C giriş/çıkışı kadar hızlı olabileceğini
gösteriyor.
Programları kayan noktalı sayılar yerine tamsayılar
kullanacak şekilde değiştirmek hız oranlarında bir
değişiklik yapmadı. Yine de, bu değişikliğin C++ gibi
yazılan programda C gibi yazılandan çok daha kolay
olduğunu görmek güzeldi: 2 değişikliğe karşılık 12
değişiklik. Bu, program bakımı açısından çok çok iyi.
"üretme" denemelerinde görülen fark, bellek ayırma
bedellerindeki farkları yansıtıyor. 'vector' ve
'push_back()'in hızının, bir dizi ile birlikte
kullanılan 'malloc()' ve 'free()'nin hızıyla aynı olması
beklenirdi; ama değil. Bu, boş işlevlere yapılan
çağrılar, eniyileştirme sırasında atlanmadıkları için
olmalı. Neyse ki bellek ayırmanın bedeli, bu bedeli
doğuran girişin bedelinin yanında yok sayılacak kadar
küçük.
Beklendiği gibi, 'sort()' 'qsort()'tan oldukça hızlı
çıktı. Bunun ana nedeni, 'qsort()'un sıralamayı yaparken
bir işlev çağırmasına karşın, 'sort()'un
karşılaşıtırmayı kendi içinde yapmasıdır.
Verimlilik konularını gösterecek örnekler seçmek zor.
Bir çalışma arkadaşım, sayı okuma ve sıralamanın doğal
olmadığı yönünde bir yorum yaptı. Ona göre, okumayı ve
sıralamayı dizgilerle yapmalıydım.Bunun üzerine
aşağıdaki programı denedim:
#include
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
char* gkutuk = argv[2]; // giris kutugu adi
char* ckutuk = argv[3]; // çikis kutugu adi
vector arabellek;
fstream giris(gkutuk, ios::in);
string dizgi;
while(getline(giris, dizgi)) arabellek.push_back(dizgi);
// giristen arabellege ekle
sort(arabellek.begin(), arabellek.end());
fstream cikis(ckutuk, ios::out);
copy(arabellek.begin(), arabellek.end(),
ostream_iterator(cikis, "\n")); // cikisa kopyala
}
Bunu C'ye çevirdim ve karakter girişini deneme
yanılmayla hızlandırdım. C++ gibi yazılan program,
dizgilerin kopyalanmasını engellemek için elle
eniyileştirilmiş C programyıla karşılaştırıldığında yine
de fena değildi. Az sayıda veri için aralarında belirgin
bir fark yok; çok sayıda veri için ise 'sort()', yine
karşılaştırmaları kendi içinde yaptığı için burada da
'qsort()'tan daha hızlı.
|
Dizgi okuma,
sıralama, ve yazma |
|
|
C++ |
C |
C/C++ oranı |
C (dizgi
kopyalamadan) |
Eniyileştirilmiş C/C++ oranı |
|
500.000 öğe |
8.4 |
9.5 |
1.13 |
8.3 |
.99 |
|
2.000.000 öğe |
37.4 |
81.3 |
2.17 |
76.1 |
2.03 |
Bilgisayarımda beş milyon öğeyi sayfalamaya geçmeden
barındıracak kadar bellek olmadığı için iki milyon öğe
kullandım.
Nerede ne kadar süre geçtiğini anlamak için programı bir
de 'sort()'u çıkartarak çalıştırdım.
|
Dizgi okuma ve
yazma |
|
|
C++ |
C |
C/C++ oranı |
C (dizgi
kopyalamadan) |
Eniyileştirilmiş C/C++ oranı |
|
500.000 öğe |
2.5 |
3.0 |
1.20 |
2.0 |
.80 |
|
2.000.000 öğe |
9.8 |
12.6 |
1.29 |
8.9 |
.91 |
Kullandığım dizgiler kısa sayılırdı: ortalama yedi
karakter.
'string'in standart kitaplıkta bulunmasına rağmen
aslında ek bir tür olduğuna dikkat edin. 'string'
kullanarak yapabildiğimiz bu verimli ve güzel şeyleri
başka ek türlerle de yapabiliriz.
Verimliliği neden programlama biçemi ve öğretimi
bağlamında tartışıyorum? Öğrettiğimiz biçemler ve
teknikler gerçek programlarda da uygulanabilmelidir.
Büyük ölçekli ve belirli verimlilik ölçütleri olan
sistemler de C++'ın hedeflediği sistemlerin arasındadır.
Bundan dolayı, C++'ın insanları yalnızca basit
programlarda kullanılabilecek biçemler ve teknikler
kullanmaya yöneltecek şekilde öğretilmesini kabul
edemiyorum. Bu, onları başarısızlığa ve öğrendiklerinden
vazgeçmeye götürür. Yukarıdaki ölçümler, genel
programlamaya ve somut türlere dayanarak basit ve tür
güvenliği içeren C++ programları üretme biçeminin,
geleneksel C biçemlerine göre daha verimli olduğunu
gösteriyor. Nesneye dayalı programlama biçemleriyle de
benzer sonuçlar elde edilmiştir.
Değişik standart kitaplık gerçeklemeleri arasında büyük
hız farklarının olması oldukça önemli bir sorun.
Standart kitaplığa veya yaygın olarak kullanılan başka
kitaplıklara dayalı olarak program yapan bir programcı
için, bir sistemde yüksek hızlar getiren programlama
biçemlerinin, başka sistemlerde de hiç olmazsa kabul
edilir düzeyde hızlar getirmesi önem taşır. C++
biçeminde yazılan programlarımın, C biçeminde yazılan
eşdeğerlerinden bazı sistemlerde iki kere daha hızlı
çalışmalarına rağmen, başka sistemlerde onların yarısı
hızında çalıştıklarını görmek beni çok şaşırttı.
Programcılar, sistemler arasında dört gibi yüksek bir
hız katsayısını kabul etmek zorunda kalmamalıdırlar. Bu
farklılığı getirecek görebildiğim hiçbir temel neden
olmadığı için, kitaplık gerçekleyenlerin fazla uğraşa
girmeden bu tutarlılığı sağlayabileceklerine inanıyorum.
Standart C++'ın hem algılanan hem de gerçek hızını
geliştirmenin en kolay yolu, belki de eniyileştirilmiş
kitaplıklar kullanmaktır. Derleyici gerçekleyenler,
başka derleyicilere karşı küçük hız kazançları sağlamak
için yoğun bir çaba içindeler. Ben, standart kitaplık
gerçeklemelerindeki gelişme kapsamının daha büyük
olduğuna inanıyorum.
Yukarıdaki C++ çözümünün C çözümünden daha kolay
oluşunun nedeni, standart C++ kitaplığını kullanmasıdır.
Peki bu, karşılaştırmayı geçersiz veya haksız yapar mı?
Sanmıyorum. C++'ın en önemli özelliklerinden birisi,
düzenli ve verimli kitaplıkları desteklemesidir.
Buradaki basit örneklerle gösterilen üstünlükler,
düzenli ve verimli kitaplıkların olduğu veya
yazılabileceği her uygulama alanında da geçerlidir. C++
topluluğunun karşısındaki güçlük, bu yararları sıradan
programcıların kullanabilecekleri başka alanlara
yaymaktır. Yani, başka birçok uygulama alanına yönelik
düzenli ve verimli kitaplıklar tasarlamalı,
gerçekleştirmeli, ve yaygınlaştırmalıyız.
C++ 'I
ÖĞRENMEK
Bir
programlama dilinin tümünü birden öğrenip sonra da
kullanmaya çalışmak profesyonel programcılar için bile
çok zordur. Programlama dilleri, getirdikleri olanaklar
küçük örneklerle denenerek parça parça öğrenilir. Onun
için, bir dili her zaman için bir dizi altkümesinde
ustalaşarak öğreniriz. Doğru soru, "Önce bir altkümesini
mi öğrenmeliyim?"den çok, "Önce hangi altkümesini
öğrenmeliyim?"dir.
"C++'ın önce hangi altkümesini öğrenmeliyim?" sorusunun
geleneksel bir yanıtı, "C++'ın C altkümesini"dir. Benim
kanımca, bu iyi bir seçim değil. Önce C'ye yönelmek,
beraberinde alt düzey ayrıntılara fazla erkenden
odaklanmayı getirir. Ayrıca programlama biçem ve tasarım
konularını da öğrenciyi bir sürü teknik güçlükle yüz
yüze bıraktığı için bulandırır. İkinci ve üçüncü
bölümlerdeki örnekler bu noktayı açıklıyor. C++'ın
kitaplık desteğinin, gösteriminin, ve tür denetiminin
daha iyi olması, önce C'ye yönelmememiz gerektiği
sonucunu doğurur. Ancak, benim önerimin "önce saf
nesneye dayalı programlama" olmadığına da dikkat edin.
Bence bu da başka bir uç nokta olur.
Bir dili öğrenme şekli, programlamaya yeni başlayanlara
etkin programlama tekniklerini de öğretecek şekilde
olmalıdır. C++'a yeni başlayan deneyimli programcılar
için ise, etkin programlama tekniklerinin C++'ta nasıl
kullanıldıklarına ve programcının ilk defa gördüğü
tekniklerin anlatılmalarına odaklanmalıdır. Deneyimli
programcıların karşılaştıkları en büyük engel, başka bir
dilde etkin olarak kullandıklarını C++'ta dile getirmeye
çalışmalarıdır. Hem yeni başlayanlar hem de deneyimliler
için üzerinde durulacaklar, kavramlar ve teknikler
olmalıdır. C++'ın desteklediği programlama tasarım ve
tekniklerini anlamada, C++'ın sözdizimi ve anlamsal
ayrıntıları ikinci derecede önemlidir.
Öğretmenin en iyi yolu, iyi seçilmiş somut örneklerden
başlayıp daha genel ve daha soyut örneklere geçmektir.
Bu hem çocukların öğrenme şekli, hem de bizim yeni
düşünceleri kavrama şeklimizdir. Dilin olanakları her
zaman için kullanıldıkları kapsamda sunulmalıdır. Yoksa
programcının ilgisi, sistem üretmek yerine anlaşılması
güç teknik ayrıntılara yönelir. Dilin teknik
ayrıntılarıyla ilgilenmek eğlencelidir ama etkin bir
öğretim biçimi değildir.
Öte yandan, programlamayı salt çözümleme ve tasarıma
yardımcı olarak görmek de işe yaramaz. Kod üzerine
yapılacak görüşmeleri üst düzey konuların sunulmasından
sonraya bırakma hatasının bedeli, defalarca çok pahalıya
ödenmiştir. Bu yaklaşım, insanları programlamadan
uzaklaştırmaya ve üretim düzeyi niteliklerinde kod
yazmanın getirdiği güçlükleri küçümsemeye
yöneltmektedir.
"Önce tasarım" yaklaşımının tam karşıtı da, bir C++
gerçeklemesini alıp hemen kodlamaya geçmektir. Bir
sorunla karşılaşıldığında tıklayarak yardım ekranlarında
neler bulunacağına bakılır. Buradaki yaklaşımdaki sorun,
özelliklerin ve olanakların, birbirlerinden ayrı olarak
anlaşılmalarına dayalı olmasıdır. Genel kavramlar ve
teknikler bu şekilde öğrenilemezler. Bu yaklaşımın
getirdiği ek bir sorun, C++ sözdizimi ve kitaplıkları
kullansalar bile, deneyimli programcıları daha önceden
bildikleri bir dilde düşünmeye yönlendirmesidir. Sonuçta
yeni başlayanların kodu, program örneklerinden
kopyalanmış satırların bir sürü 'if-else' arasına
serpiştirilmesinden oluşmaktadır. Yeni başlayanlar
kopyalanan satırlardaki kodun amacını ve nasıl işe
yaradığını çoğu zaman anlayamazlar. Kişi ne kadar akıllı
olursa olsun durum değişmez. Bu "kurcalama yöntemi"
aslında iyi bir öğretim ve iyi bir kitapla birlikte
olduğunda çok yararlıdır ama tek başına kullanıldığında
felakete davettir.
Ben özetle şöyle bir yöntem öneriyorum;
somuttan soyuta yönelmeli
dilin özelliklerini, destekledikleri programlama ve
tasarım teknikleri kapsamında sunmalı
kodu, kuruldukları alt düzey ayrıntılara girmeden üst
düzey kitaplıklara dayalı olarak sunmalı
gerçek programlara taşınamayacak tekniklerden kaçınmalı
ayrıntılara girmeden önce, benimsenmiş ve kullanışlı
teknikler sunmalı; ve
dilin özelliklerinden çok, kavramlara ve tekniklere
odaklanmalı.
Hayır, bu yöntemin yeni veya değişik olduğunu
düşünmüyorum. Herkesin akla yakın bulacağını
düşünüyorum. Ne yazık ki bu akla yakınlık; C'nin C++'tan
önce öğrenilmesinin doğru olup olmadığı, nesneye dayalı
programlamanın tam olarak anlaşılması için Smalltalk'un
gerekip gerekmediği, programlamanın saf nesneye dayalı
olarak mı (her ne demekse) öğretilmesinin iyi olduğu, ve
kod yazmaya geçmeden önce yazılım geliştirme sürecinin
iyice anlaşılmasının ne kadar önemli olduğu gibi
tartışmalar arasında yok olup gitmektedir.
Neyse ki benim koyduğum ölçütler doğrultusunda biraz
deneyimimiz var. Benim en sevdiğim yöntem; dilin
değişkenler, bildiriler, döngüler gibi temel
kavramlarını iyi bir kitaplık şeklinde öğretmektir.
Öğrencilerin ilgilerini C dizgileri gibi karmaşıklıklar
yerine programlamaya yönlendirmek için kitaplıklar
gerekir. Ben standart C++ kitaplıklarını veya bir
altkümelerini öneririm. Bu yöntem, Amerikan liselerinde
'bilgisayar bölümlerine hazırlama' derslerinde de
kullanılmaktadır [Horwitz, 1999]. O yöntemin deneyimli
programcılara yönelen daha geliştirilmiş bir şekli de
başarıyla uygulanmıştır [Koenig, 1998].
Bu yöntemlerin bir zayıflığı, görsel programlamaya hemen
girmemeleridir. Bunu karşılamanın bir yolu, arabirimi
kolay olan görsel bir kitaplığı tanıtmaktır. Bu
arabirim, öğrencilere C++ dersinin ikinci gününde
verilebilecek kadar kolay olmalıdır. Ne yazık ki bu
şartı sağlayan yaygın bir C++ görsel kitaplığı yok.
Baştaki bu kitaplıklara dayalı öğretimden sonra,
öğrencilerin ilgileri doğrultusunda çok değişik konulara
geçilebilir. Bir noktada, C++'ın düzensiz ve alt düzey
bazı özelliklerine de değinmek gerekecektir. İşaretçi,
tür dönüşümü, ve bellek ayırma gibi özellikleri
anlatmanın bir yolu, temelleri öğretirken kullanılan
sınıfların nasıl gerçekleştirildiklerini incelemektir.
Örneğin 'string', 'vector', 'list' gibi sınıflar, C++'ın
ilk derslerde gözardı edilen C altkümesini anlatmak için
çok uygundur.
'vector' ve 'string' gibi değişken sayıda öğe barındıran
sınıfları gerçeklerken, bellek yönetimi ve işaretçiler
kullanılması gerekir. Sınıf gerçekleme tanıtılırken,
önce gerçeklenmelerinde bu kavramların kullanılmalarına
gerek olmayan 'Tarih', 'Nokta', ve 'SanalSayi' gibi
sınıflar tanıtılabilir.
Ben soyut sınyfları ve sınıf hiyerarşilerini tanıtmayı
genelde kapların ve kap gerçeklemenin anlatılmasından
sonraya bırakıyorum ama bu konuda başka seçenekler de
var. Konuların verildiği sıra, kullanılan kitaplıklara
göre değişir. Örneğin sınıf hiyerarşilerine dayalı
görsel kitaplıklar kullanan bir kurs, çok şekilliliği ve
sınıf türetmeyi daha önce işlemelidir.
Son olarak, lütfen C++ dilini ve onun tasarım ve
programlama tekniklerini anlatmanın birden fazla yolu
olduğunu unutmayın. Öğrencilerin olduğu kadar
öğretmenlerin ve ders kitapları yazarlarının da
hedefleri ve çıkış noktaları farklıdır.
ÖZET
Programlarımızın kolay yazılır, doğru, bakımı kolay, ve
belli sınırlar içerisinde verimli olmalarını isteriz.
Bunu başarabilmek için, programlarımızı C'de ve eski
C++'ta kullanılanlardan daha üst düzey soyutlamalar
kullanarak tasarlamalyız. Bu amaca, alt düzey biçemlerle
karşılaştırıldığında verimlilik kaybı olmadan,
kitaplıklar kullanarak ulaşabiliriz. Yani, standart C++
kitaplığı gibi kitaplıklarla tutarlı yeni kitaplıklar
geliştirmenin ve bunları yaygınlaştırmanın C++
kullanıcıları için yararı büyüktür.
Eğitim, daha düzgün ve üst düzey programlama biçemlerine
geçişte büyük rol oynar. Yersiz verimlilik kaygılarıyla
alt düzey kitaplık olanaklarını kullanan yeni bir
programcı kuşağının C++ kullanıcıları arasına girmesine
gerek yoktur. Yeni başlayanlar kadar deneyimli olan
programcılar da Standart C++'ı yeni ve üst düzey bir dil
olarak öğrenmeliler ve alt düzey soyutlamalara ancak
gerçekten gerek olduğunda inmelidirler. Standart C++'ı
daha üstün ve sınıflar eklenmiş bir C gibi kullanmak,
onun sunduğu olanakları harcamak anlamına gelir.
DEĞİŞKEN TANIMLAMA VE
KULLANMA
Programlarımızda
işlemlerimizi yaparken verileri kullanırız. Mesela
herhangi iki sayıyı toplarız veya iki tane karakter
dizisini (string) karşılaştırırız. Bu işlemler için
kullandığımız verilerimizi değişkenler içinde tutarız.
Değişkenler bilgisayar hafızasında verileri depolayan ve
isimleri olan programlamının en temel elementleridir.
Değişkenlerin isimlerinin olmasını gerektiğini söyledik.
Bir değişkeni kullanmadan önce onu tanımlamalıyız.
Tanımlamayı değişkene uygun bir isim verme ve değişkenin
hangi tipten olduğunu bildirmeyle yaparız.
Önce isterseniz değişleri C++ dili kuralların uygun bir
biçimde nasıl isimlendireceğimizi görelim. Değişken
isimlerini verirken C++'ın bir takım sıkı kurallarına
uymamız gerekir. Bu kurallar:
Değişkenlerin isimleri alfabede bulunan karakterlerle
başlamalı. Ama ilk harf hariç diğer karakterler sayı
olabilir. C++ büyük ve küçük harf duyarlıdır. Yani Sayi,
sayi ve SAYI hepsi ayrı değişken olarak algınalırlar.
Değişken isimleri birden fazla kelime olduğu zaman;
kelimelerin arasına boşluk konmaz. Bu tür değişkenleri
ya kelimeleri birleştirerek veya kelimeler arasına _
(alt çizgi) karakteri koyararak isimlendiririz.
Değişkenlerin isimleri !, ?, {, ] gibi karakterler
içeremezler. C++'ın anahtar kelimelerini de değişken
isimleri olarak kullanamayız. sayi, tamsayi1, toplam,
Fark, KullaniciAdi, isim, _Adres, sinif_ortalaması,
kurallar göre adlandırılmış değişkenlerdir. Diğer
taraftan 1.sayi, tamsayi 1, fark!, 3.sinif_ortalamasi
geçersiz değişken isimleridir. Böyle yanlış
adlandırılmış değişkenleri içeren programlar derlenmez!
Anahtar kelimeler C++ dilinde bulunan komutların
isimleridir. Bunları direk olarak değişken ismi olarak
kullanamayız. Ayrıca alt çizgi ile başlayan değişken
tanımlamadan kaçınmalıyız. Çünkü genelde C++
kütüphanelerini yazan programcılar değişkenlerini alt
çizgi ile başlayan isimler verirler. Bu da isimler
arasında çakışma yaratabilir. Değişkenleri
isimlendirmeyi öğrendikten sonra sonra sıra C++
dilindeki temel veri türlerini öğrenmeye geldi.
Verileri bilgisayarda program çalışırken bellekte(RAM)
depolanır. Bilgisayar belleği bitlerden oluşmuştur. Bir
bit temel olarak 1 veya 0 değerini alır. Sekiz tane bit
bir byte eder. Bilgisayarın hafızasında verilerin
kapladıkları alanlar byte türünden ifade ederiz (bir çok
sistemde bu böyledir). C++ verileri ihtiyacımıza göre
değişik tiplerde tanımlarız kullanırız.
C++ dilinde hazır bulunan temel veri tipleri şunlardır:
|
Değişken |
Boy |
Açıklaması |
Değer Aralığı |
|
char |
1 |
karakter veya 8
bit uzunluğunda tamsayı |
signed: -128
ile 127 arasında
unsigned: 0 ile 255 |
|
short |
2 |
16 bit
uzunluğunda tamsayı |
signed: -32768
ile +32767 arasında
unsigned: 0 ile 65535 arasında |
|
long |
4 |
32 bit
uzunluğunda tamsayı |
signed:
-2147483648 ile +2177483647 arasında
unsigned: 0 ile 65535 arasında |
|
int |
|
Tamsayı
tipidir. DOS'ta ve Win3.1'de 16 bit
uzunluğunda ama Windows9x, WinNT, Win200 ve
WinXP 32 bit. |
short ve long
türlerine bakınız. |
|
float |
4 |
Kesirli sayı.
|
3.4e +/- 38 (7
basamak) |
|
double |
8 |
Geniş ve fazla
duyarlıklı kersirli sayı. |
1.7e +/- 308
(15 basamak) |
|
long double |
10 |
double tipinin
daha genişidir. |
1.2e +/- 4932
(19 basamak) |
|
bool |
1 |
true(doğru)
veya false(yanlış) değerini alır. Eski
derleyiciler bu türü desteklemeyebilir. Yeni
ANSI C++ standardında eklenmiştir. |
doğru veya
yanlış. |
|
wchar_t |
2 |
char tipinden
geniş olur Unicode tipinde değişkenleri
destekler. |
geniş
karakterler (unicode) |
Yalnız platform ve işletim
sistemine göre değişkenlerin boyutları yukarıdakilerden
farklı olabilir. Ama ANSI C++ standart derleyicilerinin
hepsi yukarıdaki veri tiplerini desteklerler. Yukarıda
dikkate ederseniz değişkenlerin çoğunun unsgined ve
signed versiyonları var. Bunlardan signed olanları hem
pozitif hem de negatif değerler alırken; unsigned
versiyonlar ise sadece pozitif değerler alırlar.
Değişkenleri isimlendirdik ve onların tiplerini
öğrendik. Şimdi değişkenleri bildirmeyi ve onları
kullanmayı öğrenelim. Genel olarak temel veri
tiplerinden olan değişkenleri şu şekilde tanımlarız:
<veri_tipi> <deðiþken_isimi> ;
Yukarıdaki kurala uygun olarak aşağıda bununla ilgili
örnekler vardır:
int sayi;
unsgined int a; char karakter;
float sayi_2;
bool dogru_yanlis;
unsigned long uzunTamsayi;
Yukarıdaki değişken tanımlamalarının hepsi kurallara
uygundur. İstersek birden fazla değişkeni bir satırda
tanımlama olanağımız vardır:
int sayi1, sayi2, sayi 3;
char karakter, baskabir_karakter;
Örnekte int tipinden üç değişkeni tek bir satırda
tanımlamayı ve aynı şekilde char tipinden iki değişkeni
tek bir satırda tanımlıyoruz. Burda dikkat edilmesi
gereken nokta değişkenlerin arasına virgül koymamız
gerektiğidir.
Değişkenlere değer atama işlemi için eşittir (=)
operatörünü kullanırız. Mesela aşağıdaki kod parçasında
önce x değişkenini sonra da y değişkenini tamsayı (int)
tipinde bildirdik. Sonra programın herhangi bir yerinde
x'in içeriğini 25 yaptık. Bunun hemen ardından y'nin
değerini 14 yaptık. En son kısımda x'in değerini y'de
depoladık.
int x;
int y;
......
x=25;
y=14;
....
y=x;
Değişkenlerin değerlerini ilk tanımladığımız anda da
atayabiliriz. Aşağıda bununla ilgili örnekler verelim:
double t=3.25;
bool dogru_mu=false;
long int s1=12345, s2=-694312978425;
double t=3.25;
Uygulama Örneği:
// Iki tamsayi alan ve
toplamini bulan program
#include <iostream.h>
int main()
{
int sayi1;
int sayi2;
int toplam;
cout << "\n Lutfen birinci tamsayiyi giriniz: ";
cin >> sayi1;
cout <<"\n Lutfen ikinci tamsayiyi giriniz: ";
cin >> sayi2;
toplam = sayi1 + sayi2;
cout << "\n " << sayi1 << " + " << sayi2 << " = " <<
toplam << endl;
return 0;
}
Son olarak yukarıdaki
programı yazalım. Programda int tipinden sayi1, sayi2 ve
toplam değişkenlerimizi tanımladık. Sırası ile
kullanıcıdan bu değişkenlerin değerlerini aldık ve
sonucu ekrana yazdırdık. Program basit gibi görünebilir
ama mutlaka yazın, derleyin ve çalıştırın.
C++ İLE İLK PROGRAMIMIZ
// Bu bizim ilk C++ programimizdir.
// Ekrana "Merhaba Dunya" yazdiriyoruz:
# include <iostream.h>
int
main()
{
cout << "Merhaba Dunya !";
return
0; // Programimizin basari ile tamamlandigini ifade
eder.
}
İsterseniz
yukarıdaki porgramın kodunu satır satır açıklayalım.
Böylece her satırda bulunan ifadelerin anlamını daha iyi
anlamış oluruz. İlk iki satır // ile başlıyor. Bu
şekilde başlayan satırlara kod hakkında yorum ya da
açıklama yaparız. Yapılan açıklamalar siz veya başka
birisi programı okurken daha rahat anlamasını ve ileride
büyük programlarda hata ayıklama ya da yeni özellikler
eklerken kodun kolayca anlaşılmasını sağlar. İstediğiniz
kadar açıklama/yorum satırını programım koduna
ekleyebiliriz. Bu satırlar derleyiciler tarafından
derleme işlemi sırasında dikkate alınmazlar ve programın
perfromansına hiçbir etkileri de yoktur.
#include
<iostream.h> satırı ise C++ derleyicimizin ön
işlemcisine dilimizin kütüphanesinde bulunan iostream.h
kitaplığını programımızda kullanacağımızı ifade eder.
(C++ ön işlemcileri konusunu ileride daha ayrıntılı
olarak inceleyeceğiz.)
Daha sonraki satır her C++ programında mutlaka bulunması
gereken bir satırdır. Her C++ programında main()
fonksiyonu olmak zorundadır; bu fonksiyonumuzun önünde
ise o fonksiyonun döndürdüğü değişkenin veri tipi
olmalıdır. Tabi ki C++ fonksiyonlar ve onların
döndürdükleri değerler konusunu da ileride işleyeceğiz.
C++ fonksiyonlar ve kod blokları { } parantezleri
arasında bulunmalıdır. mainde bir fonksiyon ise onun
içindeki kodlar doğal olarak { } parantezleri
arasındadır.
Programımızı derleyip, çalıştırdığımızda çıkan sonucun
kodunun olduğu satır : cout << " Merhaba Dunya ! ";
satırıdır. Bu satırda, iostream.h kitaplığında bulunan
cout fonksiyonu sayesinde ekrana bir şeyler
yazdırıyoruz. C++ dilinde her satır ifadenin sonuna ;
koymak zorundayız. Bu duruma aykırı olan satırlar
#include ile başlayanlar, ve fonksiyonlar başlangıç
satırlarıdır. Bir kaç tane daha satır tipi de ; ile
bitmez ama onları sonra göreceğiz. Aslında programımızı
birkaç satırda da yazabilirdik. Sadece farklı ifadelerin
sonuna ; koyarak. Mesela : # include <iostream.h> int
main() { "Merhaba Dunya !"; return 0; } gibi..
return 0; ile programımızın (aynı zamanda main
fonksiyonumuzun) çıkış noktasıdır. Eğer return ile 0
değeri döndürürsek programımızın güvenle çıktığını
işletim sistemine bildirmiş oluruz.
ŞABLON
FONKSİYONLAR YAZMAK
C++ programlama dili, C'den aldığı çok hoş sözdimi
(elegant syntax) ve alt seviyelerde ileri derecede esnek
programlar yazmaya uygun olması ile gerçekten birçok
bilgisayar programcısının favorisi olmuştur. Bütün
bunların yanında büyük çapta yazılım geliştirirken
gerçek dünyayı iyi bir şekilde modelleyen Nesne
Yönemlimli Programlama özelliklerini de programcıya
sunması onu gerçekten bir numara yapmıştır diyebiliriz.
Bu yazımızda C++ dilinin esnek programlamayı destekleyen
bir özelliğinin, fonksiyon şablonlarının (function
templates) üzerinde duracağız. Diyelimki bir programın
içinde hem tamsayıları, hem de kesirli sayıları toplayan
fonksiyonlara ihtiyacımız var. Bu durumda bir C
programcısı muhtemelen tamsayi_topla(int sayi_1, int
sayi_2) ve kesirlisayi_topla(double sayi_1,double
sayi_2) şeklinde iki farklı fonksiyon yazardı. Aynı
durumda bir C++ programcısı topla() isimli fonsiyonlara
aşırı yükleme (method overloading) ile topla(int sayi1,
int sayi2) ve topla(double sayi1,double sayi2) gibi aynı
isimli iki tane fonksiyon yazar. İkinci durumda
programcı yine 2 fonksiyon yazar ama burda sadece bu
fonksiyonların ait olduğu sınıfın bir örneğini
(instance) kullanan programcı rahat eder çünkü iki
farklı fonksiyon ismi bilmek yerine sadece bir fonksiyon
ismi bilir ve bunların aldıklar parametrelerin
faklılıklarını bilir ve ona göre kullanır.
Yukarıdakilerin yanında aynı işi yapan ve farkları
aldıkları ve/veya dönderdikleri değerlerin tipleri
farklı olan n tane farklı fonksiyonu ayrı ayrı yazmak
yerine bunlar için şablon fonksiyon şeklinde sadece bir
metod yazıp bu fonksiyonları ihtiyacımıza göre çağırmak
daha kolay olur. Hem böylelikle modern programlamadaki
code-reuse (kodun tekrar kullanılması) ilkesini sonuna
kadar kullanmış oluruz.
Şablon fonksiyon yazmak için öncelikle template< class
Tip > böyle bir satırı fonksiyonumuzdan önce yazarız.
Burdaki tip fonksiyomuzun çalışacağı veri tipidir ki bu
temel veri tipi veya bir sınıf tipi de olabilir. Sonrai
satır da ise Tip topla(Tip sayi1 , Tip sayi2 ) önce,
fonksiyonun döndereceği veri tipini (bu void veya
standart birşey de olabilir.), fonksiyon adını ve
parametreleri yazarız. Parametrelerin bir veya birkaçı
da temel bir veri tipi veya Tip'ten farklı bir sınıf
tipi veya struct olablir. Sonra ise normal fonksiyon
yazımı ile aynıdır.
Aşağıdaki programda topla() isimli şablon bir
fonksiyonumuz var. Fonksiyon aldığı iki değişkeni
toplayıp, toplamı geriye dönderiyor. Fonksiyonu denemek
için iki tane tamsayı, kesirli sayı ve karakteri
toplayıp sonuçlarını ekrana yazan kodları main()
fonksiyonu içinde yazıyoruz. Tabi enson dummy
değişkenini enson neden aldığmızı da tahmin ederseniz
sanırım.
#include <iostream.h>
template< class Tip >
Tip topla(Tip sayi1 , Tip sayi2 )
{
return (sayi1+sayi2);
}
int main()
{
int tamSayi1=5, tamSayi2=10;
double kesirliSayi1=22.33, kesirliSayi2=55.26;
char karakter1='A', karakter2='B';
char dummy;
cout << "Iki tamsayý : "<< tamSayi1 << " ve " <<
tamSayi2;
cout << " toplayýnca = " << topla(tamSayi1, tamSayi2)<<
endl <<endl;
cout << "Iki double : "<< kesirliSayi1 << " ve " <<
kesirliSayi2;
cout << " toplayýnca = " << topla(kesirliSayi1,
kesirliSayi2) << endl<<endl;
cout << "Iki karakteri : "<< karakter1 << " ve " <<
karakter2;
cout << " toplayýnca = " << topla(karakter1,
karakter2)<< endl <<endl;
cin >> dummy;
return 0;
}
VERİ
DÖNÜŞÜM FONKSİYONLARI
Stdlib.h kütüphanesinde bulunan veri dönüşüm
fonksiyonları şunlardır;
|
Dönen tip |
Fonksiyon Adi |
Argumanlar |
|
double |
atof |
(const
char*str) |
|
int |
atoi |
(const
char*str) |
|
long |
atol
|
(const
char*str) |
|
char* |
ecvt |
(double num,
int n,int*dec,int*sign) |
|
char* |
fcvt |
(double num,
int n,int*dec,int*sign) |
|
char* |
gcvt |
(double num,
int n,char*buf) |
|
char* |
itoa
|
(int
num,char*str,int radix) |
|
char* |
ltoa |
(long
num,char*str,int radix) |
|
double |
strtod |
(const char*str
,char**endptr) |
|
long |
strtol |
(const char*str
,char**endptr,int radix) |
|
unsigned long
|
strtoul |
(const char*str
,char**endptr,int radix) |
|
char*
|
ultoa |
(unsigned long
num char*str ,int radix) |
Şimdi örnek olması açısından fcvt fonksiyonunun
kullanımına örnek verelim . fcvt fonksiyonu float olan
sayıyı karakter katarına dönüştürmekte kullanılır.
Aşağıda bunun kullanımına bir örnek görmektesiniz.
#include <stdlib.h>
#include <iostream.h>
void main(void)
{
char *ConvertedFloattoString;
float ANegativeValue = -2.121,
APositiveValue = 3.14159,
AScientificValue = 1.0239E4;
int float_sign, decimal, precision = 6;
//degiskenleri ilk degerlerini de vererek tanimliyoruz.
//Stdlib.h library' de belirtildiði sekilde
//fcvt(float olan sayiyi karakter katarina donusturen
fonksiyon) fonksiyonunu kullaniyoruz.
//char* fcvt (double num, int n,int*dec,int*sign)
sekline gore kullaniyoruz.
ConvertedFloattoString = fcvt(ANegativeValue,
precision,
&decimal,
&float_sign);
cout <<"\n\n Float sayinin gercek degeri:"
<<ANegativeValue
<<"\n Float sayýyý stringe degistir:"
<<ConvertedFloattoString
<<"\n Ondalýk deger : "
<<decimal
<<"\n Float sayinin isaret(sign) degeri:"
<<((float_sign?"-":"+"));
ConvertedFloattoString = fcvt(APositiveValue,
precision,
&decimal,
&float_sign);
cout <<"\n\n Float sayinin gercek degeri:"
<<APositiveValue
<<"\n Float sayýyý stringe degistir:"
<<ConvertedFloattoString
<<"\n Ondalýk deger :"
<<decimal
<<"\n Float sayinin isaret(sign) degeri:"
<<((float_sign?"-":"+"));
ConvertedFloattoString = fcvt(AScientificValue,
precision,
&decimal,
&float_sign);
cout <<"\n\n Float sayinin gercek degeri:"
<<AScientificValue
<<"\n Float sayýyý stringe degistir:"
<<ConvertedFloattoString
<<"\n Ondalýk deger : "
<<decimal
<<"\n Float sayinin isaret(sign) degeri:"
<<((float_sign?"-":"+"));
}
Tabi stdlib.h kütüphanesi 'nde sadece veri dönüşümleri
değil aynı zamanda arama ve sıralama gibi fonksiyonlar
da bulunur.
GÖSTERİCİLER (POINTER)
Göstericiler C ve C++ dillerinin en zor konusu olarak ün
salmışlardır. Ama konu üzerinde biraz çalışırsak ve
göstericileri kullanırken dikkatli olursak gerçekten de
programlarımızın hızını oldukça artıran araçlar olarak
karşımıza çıkarlar.
Normalde bir değişken tanımladığımızda aslında sadece o
değişkene hafızada yer ayırmış oluruz. Bu değişken
ismiyle değişkenimizin hafızadaki yerine ulaşabiliriz.
Değişken ismiyle değişkene ulaşmaya direkt referans
(directly referance) denir. Göstericiler ise bir
değişkenin hafızadaki yerini saklarlar. Bu şekilde
göstericinin işaret ettiği değişkene de ulaşabiliriz.
Buna dolaylı referans (indirectly reference) denir.
Göstericileri tanımlamak ile diğer değişkenleri
tanımlamak arasında küçük bir fark vardır. Göstericileri
tanımlarken değişken isminden önce * işareti konur:
int *sayiPtr, sayi;
Yukarıda tamsayı tipinde bir gösterici bir de normal
değişken tanımladık. Aynı satırda birden fazla gösterici
tanımlarken herbirinin önüne ayrı ayrı * işareti
koymamız gerekir. Aksi halde sadece ilk değişkenimiz
gösterici olur. Birden fazla göstericiyi aynı satırda
şöyle tanımlarız:
char *aPtr, *bPtri;
C++'da göstericiler için iki tane işleç(operator)
kullanılır. Bunlardan birincisi: & adres işlecidir.
Adres işleciyle gösterici ile aynı tipteki bir
değişkenin adresine ulaşabiliriz.
int sayi=9;
int *sPtr;
sPtr=&sayi;
Yukarıdaki kodun ilk satırında bir tamsayı değişkeni
tanımladık ve ona 9 değerini atadık. İkincisinde tamsayı
tipinde bir adres tutmak için sPtr göstericimizi
tanımladık. Son satırda ise sayi değikenimizin adresini
sPtr değişkenimize yükledik. Artık sPtr, sayi
değişkenini gösteriyor deriz.
sayi
değişkenimizin bilgisayarın hafızasında 1800 adresli
yerde saklandığını düşünelim. sPtr göstericisinin ise
hafızadaki yeri 2300 olsun. Yukarıdaki kodun son satırı
çalıştıktan sonra sPtr'nin içerisindeki değer 1800 olur.
Diğer
gösterici işlecimiz *'dir ve dolaylı referans işleci
olarak bilinir. Bu işleç, kendisinden sonra gelen
göstericinin gösterdiği değişkenin değerini döndürür.
Aşağıdaki kod parçasını incelersek:
cout << *sPtr << endl;
bu kod ekrana sayi değişkenimizin değeri olan 9'u
yazdırır. Çünkü, sPtr göstericisi sayi değişkenini
işaret ediyor ve *sPtr ifadesi ile sPtr göstericisinin
işaret ettiği değişkene ulaşıyoruz.
Dolaylı referans işlecimiz ile ayrıca, göstericinin
gösterdiği değişkenin değerini de değiştirebiliriz :
*sPtr=23;
cout<< *sPtr << endl;
cout<< sayi << endl;
Yukarıdaki kod parçasında önce sPtr göstericisinin
gösterdiği değişkenin (yani "sayi" değişkenini) değerini
23 yaptık. Sonraki satırda sPtr göstericisinin
gösterdiği değişkenin değerini ekrana yazdırdık. Son
olarak sayi değişkenin değerini ekrana yazdırdık. Son
iki satırda bulunan kodlar ekrana aynı değerleri yani 23
değeri basacaktır.
GÖSTERİCİLER (POINTER)
Hata yakalama (Exception Handling) başlıbaşına büyük bir
konu olmasına rağmen Hata Yakalama ile ilgili temel
bilgileri basit örneklerlerle anlatmaya çalışacağım. C
dili bize çok az hata yakalama mekanizması sunar.
Aşağıdaki kodu inceleyerek hata oluşma durumlarından
neyi kastettiğimizi anlayabilirsiniz.
int
BirseylerYap()
{
int *a, c;
FILE *dosya;
a = malloc(sizeof(int) * 10);
if (a == NULL)
return 1;
dosya = fopen("cpp.txt", "rb");
if (dosya == NULL) {
free(a);
return 2;
}
fread(a, sizeof(long), 10, b);
if (a[0] != 0x10) {
free(a);
fclose(dosya);
return 3;
}fclose(b);
c = a[1];
free(a);
return c;
}
Bu
fonksyionla yapmak istediğimiz, birtakım geri dönüş
değerlerine göre hatanın ne olduğunu anlamaktır. Eğer
geri dönüş değeri 1 ise alan tahsisatı yapılamadığını, 2
ise cpp.txt dosyasının açılamadığını anlıyoruz. Aslında
burada hata ayıklaması gibi özel bir kavram
yoktur.Kendimiz if-else bloklarıyla hatayı bulmaya
çalışıyoruz. Ama c++ 'ın bunların ötesinde yapabildiği
şeyler vardır. C++ dili hata yakalama için özel anahtar
sözcükler içerir. Şimdi sırayla bu anahtar sözcükleri
anlayabilmek için basit bir c++ programı yazalım. Hata
denetlemesi yapabileceğimiz programımız şöyle olsun.
Kullanıcıdan iki değer alacağız.Ve ilk alınan değeri
ikinci alınan değere böleceğiz. Eğer biraz matematik
bilgisine sahipsek kullanıcı ikinci değer olarak sıfır
girdiğinde hata olacaktır. Çünkü bir sayının sıfıra
bölünmesi matematiksel olarak sonsuzu ifade
eder.Bilgisayar programcılığında ise sonsuz diye bir şey
yoktur, herşey sonludur.(siz sonsuz döngüler yaptığımıza
bakmayın)
Şimdi C++ 'da exception handling nasıl yapılır ona
bakalım. C++ 'da hata yakalama mekanizması try,catch ve
throw anahtar sözcükleriyle yapılır. try ve catch birer
komut bloklarıdır. Hatanın ayıklanmasını istediğimiz
bölgeyi try blokları içine almamız gerekir. Hata
yakalandığında işletilecek kodlar ise catch blokları
içinde olmalıdır. Peki try bloku ile catch bloku
arasındaki Şarkı Sözlerinasıl sağlanacak, bunun cevabı
ise throw anahtar sözcüğüdür. Hatanın oluşmasına sebeb
olacak ifadeden sonra catch bloğuna hatanın türü ile
ilgili bilgi göndeririz. Aşağıdaki programda throw ile
atılan bir int bilgidir. catch bloğu ise bu bilgiyi
alarak bir hata mesajı verir. Unutmayın throw ile atılan
mesajdan sonra programımızı eğer exit() gibi bir
fonksiyonla bitirmezsek catch bloğundan sonra programın
akışı devam edecektir. Bu yüzden eğer programımızla
ilgili hayati bir hata yakalarsak catch bloğu içinde
exit() ile programı tamamen sonlandırmamız gerekir.
int
main(void){
float a,b,c;
try{
cin >> b >> c ;
if (c==0) throw 1
a=b/c;
}
catch(int i)
{
cout << i<< " Hata olustu"
}
return 0;
}
Gördüğünüz gibi yukarıdaki programda eğer kullanıcı
ikinci değer olarak sıfır girerse programımız catch
bloğuna gelir ve ekrana "1 Hata oluştu" mesajı yazılır.
Elbette exception handling mekanizması bu kadar basit
işler için değildir. Asıl amacımız sınıflar ile ilişki
kurarak hata ayıklamak.Hatta throw ile kullanmak için
her sınıf için ayrı birer hata sınıfları bile
oluşturabiliriz. Hata diye bir sınıfımızın olduğunu
düşünelim. Şu ifade son derece legal bir durumdur. "
throw(Hata err); ". Hatta, Hata sınıfına ait bir
varsayılan kurucu(constructor) işlevinin de olduğunu
düşünürseniz şöyle bir kod da yazabiliriz. " throw
Hata(); ". Catch bloğuna gönderdiğimiz Hata adlı sınıf
nesnesini Catch bloğunda ise şöyle yakalarız.
"catch(Hata err);" Şimdi bu söylediklerimizi bir örnekle
gösterelim. Örneğimizde MetreSantim diye bir sınıfımız
olacak. Amacımız bir uzunluk ölçüsünü iki değer olarak
tutmak(Metre ve santim). Sınıfımızın iki tane m(metre)
ve cm(santim) olacak şekilde iki tane üye değişkeni
olacak. Sınıfımızın biri kurucu olmak üzere put ve show
gibi 3 tane de üye işlevi olacaktır. Amacımız put() ya
da kurucu işlevi ile oluşturulacak nesnelerin santimetre
değerini 100 'den küçük olacak şekilde tutmak.(Not: 4
metre 150 santim yerine 5 metre 50 santim demek daha
mantıklı bence:)) Tabi bunu yaparken yukarıdaki örnekten
daha gelişmiş bir hata ayıklama mekanizması
kullanacağız. Bir Hata sınıfımız olacak ve bu hata
sınıfı ile cm olarak girilen değeri ve bu değerin hangi
fonksiyon tarafından gönderildiğini saklayacağız. Hata
sınıfının MetreSantim sınıfına özgü olduğunu vurgulamak
için ise iç içe(nested) sınıf tanımlaması yapacağız.
Yani Hata sınıfının bildirimini MetreSantim sınıfının
içinde yapacağız. Hata sınıfı, hatanın(throw işleminin)
hangi fonksiyondan geldiğini tutacak char türden bir
dizi ve hataya sebep olan değeri tutan int türden
elemanlardan oluşacaktır. |