|
C++ 'A GİRİŞ
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 iletişim nası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.
|