|
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. |
| |