Aşağıdaki
yazının aslı, Learning
Standard C++ as a New Language, ilk olarak "The C/C++ Users Journal"ın Mayıs
1999 sayısında yayınlanmıştır; her hakkı saklıdır.
Şubat
2002 tarihinde Ali Çehreli tarafından Türkçe'ye çevrilmiştir.
Yazıyla
ilgili her türlü fikrinizi ve önerinizi lütfen Ali'ye yazın: [email protected].
Teşekkürler...
Standart
C++'ı Yeni Bir Dil Olarak Öğrenmek
Bjarne
Stroustrup
AT&T
Labs
ÖZET
C++'tan
[C++,1998] olabildiğince yararlanabilmek için C++ programlarını yazış şeklimizi
değiştirmeliyiz. Bunun için bir yol, C++'ın nasıl öğrenilebileceğini (ve öğretilebileceğini)
gözden geçirmektir. Hangi tasarım ve programlama biçemlerini ön plana çıkartmak
isteriz? Dilin özelliklerinin ilk önce hangi altkümesini öğrenmek isteriz? Gerçek
kod içerisinde dilin özelliklerinin ilk önce hangi altkümesine önem vermeliyiz?
Bu yazı, C++'in standart kitaplığını kullanarak güncel biçemde yazılmış birkaç
basit örneği, geleneksel C eşdeğerleriyle karşılaştırmaktadır. Ayrıca, bu basit
örneklerden alınacak derslerin büyük programlarda da işe yarayacağını savunmaktadır.
Genel olarak C++'ın soyut kavramlara dayalı bir üst düzey dil olarak kullanılması
gerektiğini; ve bunun, alt düzey biçemlerle karşılaştırıldığında verimlilik
kaybına neden olmadığını savunmaktadır.
1 Giriş
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ışıyorum.
2 Karmaşıklık
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 giriş/çıkış?
#include
// standart dizgi
int main()
{
using namespace
std; // standart kitaplığa
erişim
cout <<
"Lütfen adınızı 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 giriş/çıkış?
int main()
{
const int encok =
20;
char ad[encok];
printf("Lütfen
adınızı 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 giriş/çıkış?
int main()
{
const int encok =
20;
char ad[encok];
printf("Lütfen
adınızı girin:\n");
scanf("%19s",
ad); // adı 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);
// biçim dizgisini hazırla; %s taşabileceği için
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()
// hatayı ilet ve programdan çık
{
fprintf(stderr, "Bellekte yer kalmadı\n");
exit(1);
}
int main()
{
int encok
= 20;
char * ad
= (char *)malloc(encok); //
arabellek ayır
if (ad ==
0) cik();
printf("Lütfen
adınızı girin:\n");
while (true)
{
// baştaki boşlukları atla
int c = getchar();
if (c == EOF) break; //
kütük sonu
if (!isspace(c)) {
ungetc(c,stdin);
break;
}
}
int i = 0;
while (true)
{
int c = getchar();
if (c == '\n' || c == EOF) { // sonlandırma karakterini ekle
ad[i] = 0;
break;
}
ad[i] = c;
if (i==encok-1) {
// arabellek doldu
encok = encok+encok;
ad = (char*)realloc(ad,encok); // daha büyük yeni bir arabellek ayır
if (ad == 0) cik();
}
i++;
}
printf("Merhaba
%s\n", ad);
free(ad);
// arabelleği bırak
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.
3 Verimlilik
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 biçiminde çözüm
#include
#include
int karsilastir(const void * p, const void * q)
// qsort()'un kullandığı karşılaştırma işlevi
{
register double p0 =
*(double*)p;
// sayıları karşılaştır
register double q0 =
*(double*)q;
if(p0>q0)
return 1;
if (p0
return 0;
}
void cik() //
hatayı ilet ve programdan çık
{
fprintf(stderr,
"Bellekte yer kalmadı\n");
exit(1);
}
int main(int argc, char* argv[])
{
int boyut
= 1000; // ayrımın başlangıç
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 öğe sayısı
FILE* giris=fopen(kutuk,
"r"); //
kütüğü aç
double sayi;
while(fscanf(giris, "%lg",
&sayi) == 1) { // sayıyı oku, ortalamayı değiştir
if (adet==boyut) {
boyut += boyut;
arabellek = (double*)realloc(arabellek, sizeof(double)*boyut);
if (arabellek==0) cik();
}
arabellek[adet++] = sayi;
// olası yuvarlatma hatası:
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 ö?e =
%d, orta de?er = %g, ortalama = %g\n", adet, orta, ortalama);
free(arabellek);
}
Karşılaştırmasını
yapabilmek için C++ biçimini de veriyorum:
//
C++ standart kitaplığını kullanan çözüm
#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); //
kütüğü aç
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 ö?e =
" << arabellek.size()
<<
", orta de?er = " << 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 ve sıralama |
3.5 |
6.1 |
1.75 |
2.5 |
5.1 |
2.04 |
üretme ve 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 ve sıralama |
38.4 |
172.6 |
4.49 |
27.4 |
126.6 |
4.62 |
üretme ve 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]; // giriş kütüğü adı char* ckutuk = argv[3]; // çıkış kütüğü adı
vector arabellek; fstream giris(gkutuk, ios::in); string dizgi; while(getline(giris, dizgi)) arabellek.push_back(dizgi);
// girişten arabelleğe ekle
sort(arabellek.begin(), arabellek.end());
fstream cikis(ckutuk, ios::out); copy(arabellek.begin(), arabellek.end(), ostream_iterator(cikis,
"\n")); // çıkışa 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.
4 C++'ı Öğ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.
5 Ö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.
6 Teşekkür
Standart C++ öğrenimi
üstüne bir yazı yazmamı öneren Chuck Allison'a ve yazının taslakları üzerinde
yapıcı yorumlarda bulunan Andrew Koenig ve Mike Yang'a teşekkür ederim. Kullandığım
örnekler, Cygnus'un EGCS1.1 derleyicisi ile derlenip, bir Sun Ultrasparc 10
üzerinde çalıştırılmışlardır. Kullandığım programları sitemde bulabilirsiniz:
http://www.research.att.com/~bs.
7 İlgili Kaynaklar
[C++,1998] X3 Secretariat:
Standard - The C++ Language. ISO/IEC 14882:1998(E). Information Technology Council
(NCITS). Washington, DC, USA. (http://www.ncits.org/cplusplus.htm)
[Horwitz,1999]
Susan Horwitz: Addison-Wesley's Review for the Computer Science AP Exam in C++.
Addison-Wesley. 1999. ISBN 0-201-35755-0.
[Koenig,1998] Andrew
Koenig ve Barbara Moo: Teaching Standard C++. (1., 2., 3., ve 4. bölümler) Journal
of Object-Oriented Programming, Cilt 11 (8,9) 1998 ve Cilt 12 (1,2) 1999.
[Stroustrup,1997]
Bjarne Stroustrup: The C++ Programming Language (Üçüncü Basym). Addison-Wesley.
1997 ISBN 0-201-88954-4.
Bu yazının aslını okumak için tıklayınız
Makale:
Standart C++'ı Yeni Bir Dil Olarak Öğrenmek C++ ve C++.NET dili Ali Çehreli
|