C++ 'A GIRIS
Programlarimizin kolay yazilir, dogru, bakimi kolay, ve belli sinirlar icerisinde verimli olmalarini isteriz. Bundan, dogal olarak C++'i (ve baska dilleri de) bu amaca olabildigince yakin olarak kullanmamiz gerekliligi ortaya cikar. C++ kullananlarin hâlâ Standart C++'in getirdigi olanaklari benimsemeyerek, C++ kullanim bicemlerini degistirmediklerine; ve bu nedenle, degindigim amaca yonelik büyük adimlarin atilamadigina inaniyorum. Bu yazi, Standart C++'in getirdigi olanaklarin degil, bu olanaklarin destekledigi programlama bicemlerinin üzerinde durmaktadir.
Büyük gelismelerin anahtari, kod büyüklügünü ve karmasikligini kitapliklar kullanarak azaltmaktir. Bunlari asagida, C++'a giris kurslarinda karsilasilabilecek birkac ornek kullanarak gosteriyor ve nicelendiriyorum.
Kod büyüklügünü ve karmasikligini azaltarak hem gelistirme zamanini azaltmis oluruz, hem program bakimini kolaylastiririz, hem de program sinamanin bedelini düsürürüz. Daha da onemlisi, C++'in ogrenilmesini de kolaylastirmis oluruz. Onemsiz veya bir dersten yalnizca gecer not almak icin yazilan programlarda bu basitlestirme yeterlidir. Ancak, verimlilik, profesyonel programcilar icin cok onemli bir konudur. Programlama bicemimizi, ancak günümüz bilisim hizmetlerinde ve isyerlerinde karsilasilan boyutlardaki verilerle ve gercek zaman programlariyla ugrasirken verimlilik kaybina neden olmayacaksa degistirebiliriz. Onun icin, karmasikligin azaltilmasinin verimliligi düsürmeden elde edilebilecegini gosteren olcümler de sunuyorum.
Son olarak, bu gorüsün C++'in ogrenilmesi ve ogretilmesine olan etkilerini tartisiyo
KARMASIKLIK
Bir programlama dilini ogrenirken gorülen ilk calisma programlarindan birisi olabilecek su ornegi ele alalim:
"Lütfen adinizi girin" yazin
adi okuyun
"Merhaba " yazin
Standart C++ cozümü soyledir:
#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';
}
Programciliga yeni baslayan birisine bazi temelleri anlatmamiz gerekir: 'main()' nedir? '#include' ne demektir? 'using' ne ise yarar? Ek olarak, '\n'in ne yaptigi ve noktali virgülün nerelerde kullanildigi gibi ayrintilari da anlamamiz gerekir. Yine de bu programin temeli kavramsal olarak kolay, ve soru metninden ancak gosterim acisindan farkli. Tabii dilin gosterimini de ogrenmemiz gerekir. Ama bu da kolay: 'string' bir dizgi, 'cout' cikis, '<<' cikisa yazi gondermekte kullanilan bir islec. Karsilastirmak icin, geleneksel C bicemiyle yazilmis cozüme bakalim. (Hos goründükleri icin degismez bildirilerini ve yorumlari C++ türünde yazdim. ISO standardina uygun C yazmak icin '#define' ve '/* */' yorumlari kullanilmalidir.)
#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 aciklamak gerektigi icin bu programin icerigi, C++ esdegerinden az da olsa daha karmasik. Asil sorun, bu basit C cozümünün düsük nitelikli olmasi. Eger birisi sihirli sayi 19'dan (belirtilen 20 sayisindan C dizgilerinin sonlandirma karakterini cikartarak) daha fazla harfli bir ad girerse program bozulur.
Daha sonradan uygun bir cozüm gosterildigi sürece bu niteliksizligin zararsiz oldugu one sürülebilir. Ancak bu ifade "iyi" olmak yerine, olsa olsa "kabul edilebilir" olabilir. Yeni bir programciya bu kadar kirilgan bir program gostermemek cok daha iyidir.
Peki davranis olarak C++ esdegerine yakin bir C programi nasil olurdu? Ilk deneme olarak dizi tasmasini 'scanf()'i daha dogru 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 bicim dizgisinde ad dizisinin boyutunu gosteren 'encok'un simgesel seklini kullanmanin standart bir yolu olmadigi icin, tamsayi '19'u yaziyla yazmak zorunda kaldim. Bu hem kotü bir programlama bicemi, hem de program bakimi icin bir kabustur. Bunu onlemenin oldukca ileri düzey sayilacak bir yolu var; ama bunu programlamaya yeni baslayan birisine aciklamaya yeltenmem bile:
char bicim[10];
sprintf(bicim, "%%%ds", encok-1); // bicim dizgisini hazirla; %s tasabilecegi icin
scanf(bicim, ad);
Dahasi bu program, fazladan yazilan harfleri de gozardi eder. Asil istedigimiz, dizginin girdiyle orantili olarak büyümesidir. Bunu saglayabilmek icin 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 oncekiyle karsilastirildiginda bu program cok daha karmasik. Calisma programinda istenmedigi halde bastaki bosluklari atlayan kodu yazdigim icin kendimi biraz kotü hissediyorum. Ne var ki, olagan olan, bosluklari atlamaktir; zaten programin esdegerleri de bunu yapiyorlar.
Bu ornegin o kadar da kotü olmadigi one sürülebilir. Zaten bircok deneyimli C ve C++ programcisi gercek bir programda herhalde (umariz?) buna benzer birsey yazmistir. Daha da ileri giderek, boyle bir programi yazamayacak birisinin profesyonel bir programci olmamasi gerektigini bile ileri sürebiliriz. Bu programin yeni baslayan birisini ne kadar zorlayacagini düsünün. Program bu sekliyle dokuz degisik standart kitaplik islevi kullanmakta, oldukca ayrintili karakter düzeyinde giris islemleriyle ugrasmakta, isaretciler kullanmakta, ve bellek ayirmayla ilgilenmektedir. Hem 'realloc()'u kullanip hem de uyumlu kalabilmek icin 'malloc()'u kullanmak zorunda kaldim ('new'ü kullanmak yerine). Bunun sonucu olarak da isin icine bir de arabellek boyutlari ve tür donüsümleri girmis oldu. (C'nin bunun icin tür donüsümünü acikca yazmayi gerektirmedigini biliyorum. Ama onun karsiliginda odenen bedel, 'void *'dan yapilan güvensiz bir ortülü tür donüsümüne izin vermektir. Onun icin C++, boyle bir durumda tür donüsümünün acikca yapilmasini gerektirir.) Bellek tükendiginde tutulacak en iyi yolun ne oldugu bu kadar kücük bir programda o kadar acik degil. Konuyu fazla dallandirmamak icin kolay anlasilir bir yol tuttum. C bicemini kullanan bir ogretmen, bu konuda ilerisi icin temel olusturacak ve kullanimda da yararli olacak uygun bir yol secmelidir.
Ozetlersek, basta verdigimiz basit ornegi cozmek icin, cozümün ozüne ek olarak, dongüleri, kosullari, bellek boyutlarini, isaretcileri, tür donüsümlerini, ve bellek yonetimini de tanitmak zorunda kaldim. Bu bicemde ayrica hataya elverisli bircok olanak da var. Uzun deneyimimin yardimiyla bir eksik, bir fazla, veya bellek ayirma hatalari yapmadim. Ama bir süredir cogunlukla C++'in akim giris/cikisini kullanan birisi olarak, yeni baslayanlarin cokca yaptiklari hatalardan ikisini yaptim: 'int' yerine 'char'a okudum ve EOF'la karsilastirmayi unuttum. C++ standart kitapliginin bulunmadigi bir ortamda cogu ogretmenin neden düsük nitelikli cozümü yegleyip bu konulari sonraya biraktigi anlasiliyor. Ne yazik ki cogu ogrenci düsük nitelikli bicemin "yeterince iyi" oldugunu ve otekilerden (C++ olmayan bicemler icinde) daha cabuk yazildigini hatirliyor. Sonucta da vazgecilmesi güc bir aliskanlik edinip arkalarinda yanlislarla dolu programlar birakiyorlar.
Islevsel esdegeri olan C++ programi 10 satirken, son C programi tam 41 satir. Programlarin temel ogelerini saymazsak fark, 30 satira karsin 4 satir. Üstelik C++ programindaki satirlar hem daha kisa, hem de daha kolay anlasilir. C++ ve C programlarini anlatmak icin gereken toplam kavram sayisini ve bu kavramlarin karmasikliklarini nesnel olarak olcmek zor. Ben C++ biceminin 10'a 1 daha kazancli oldugunu düsünüyorum.
VERIMLILIK
Yukaridaki gibi basit bir programin verimliligi o kadar onemli degildir. Boyle programlarda onemli olan, basitlik ve tür güvenligidir. Verimliligin cok onemli oldugu parcalardan olusabildikleri icin, gercek sistemler icin "üst düzey soyutlamayi kabul edebilir miyiz" sorusu dogaldir.
Verimliligin onemli oldugu sistemlerde bulunabilecek türden basit bir ornegi ele alalim:
belirsiz sayida oge oku
ogelerin her birisine bir sey yap
ogelerin hepsiyle bir sey yap
Aklima gelen en basit ornek, giristen okunacak bir dizi cift duyarlikla kayan noktaya sanyinyan ortalama ve orta degerlerini bulmak. Bunun geleneksel C gibi yapilan bir cozümü boyle 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 oge 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 oge = %d, orta deger = %g, ortalama = %g\n", adet, orta, ortalama);
free(arabellek);
}
Karsilastirmasini yapabilmek icin C++ bicimini 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 oge = " << arabellek.size()
<< ", orta deger = " << orta << ", ortalama = " << ortalama << '\n';
}
Program büyüklüklerindeki fark bir onceki ornekte oldugundan daha az: bos satirlari saymayinca, 43'e karsilik 25. Satir sayilari, 'main()'in bildirilmesi ve orta degerin hesaplanmasi gibi ortak satirlari (13 satir) cikartinca 20'ye karsilik 12 oluyor. Okuma ve depolama dongüsü ve siralama gibi onemli bolümler C++ cozümünde cok daha kisa: okuma ve depolama icin 9'a karsilik 4, siralama icin 9'a karsilik 1. Daha da onemlisi, düsünce sekli cok daha basit oldugu icin, C++ programinin ogrenilmesi de cok daha kolay.
Tekrar belirtirsem, bellek yonetimi C++ programinda ortülü olarak yapiliyor; yeni ogeler 'push_back'le eklendikce 'vector' gerektiginde kendiliginden büyüyor. C gibi yazilan programda bu isin 'realloc()' kullanilarak acikca yapilmasi gerekir. Aslinda, C++ programinda kullanilan 'vector'ün kurucu ve 'push_back' islevleri, C gibi yazilan programdaki 'malloc()', 'realloc()', ve ayrilan bellegin büyüklügüyle ugrasan kod satirlarinin yaptiklari isleri ortülü olarak yapmaktadirlar. C++ gibi yazilan programda bellegin tükenme olasiligini C++'in kural disi durum isleme düzenegine biraktim. Bellegin bozulmasini onlemek icin C gibi yazilan programda bunu acikca yazdigim sinamalarla yaptim.
C++ programina dogru olarak olusturmak da daha kolaydi. Ise bazi satirlari C gibi yazilan programdan kopyalayarak basladim. kütügünü icermeyi unuttum, iki yerde 'adet'i 'arabellek.size()'la degistirmeyi unuttum, derleyicim yerel 'using' yonergelerini desteklemedigi icin 'using namespace std;' satirina 'main()'in disina tasidim. Program bu dort hatayi düzeltmemin ardindan hatasiz olarak calisti.
Programlamaya yeni baslayanlar 'qsort()'u biraz "garip" bulurlar. Oge sayisinin belirtilmesi neden gereklidir? (Cünkü C dizileri bunu bilmezler.) 'double'in büyüklügünün belirtilmesi neden gereklidir? (Cünkü 'qsort()' 'double'lari siralamakta oldugunu bilmez.) O hos gozükmeyen 'double' karsilastirma islevini neden yazmak zorundayiz? (Cünkü 'double' siraladigini bilmeyen 'qsort()'a karsilastirmayi yaptirmasi icin bir islev gerekir.) 'qsort()'un kullandigi karsilastirma islevinin bagimsiz degiskenleri neden 'char*' türünde degil de 'const void*' türündedir? (Cünkü 'qsort()'un siralamasi dizgi olmayan türden degiskenler üzerinedir.) 'void*' nedir ve 'const' olmasa ne anlama gelir? (E, sey, buna daha sonra deginecegiz.) Bunu yeni baslayanin bos bakislariyla karsilasmadan anlatmak oldukca zordur. Bununla karsilastirildiginda, sort(v.begin(), v.end())'in ne yaptigini anlatmak cok kolay: "Bu durumda 'sort(v)' kullanmak daha kolay olurdu ama bazen bir kabin yalnizca bir araligindaki ogeleri siralamak istedigimiz icin, daha genel olarak, siralanacak araligin basini ve sonunu belirtiriz."
Programlarin verimliliklerini karsilastirmadan once, bunu anlamli kilmak icin kac tane oge kullanilmasi gerektigini belirledim. Oge sayisi 50.000 oldugunda programlarin ikisi de islerini yarim saniyenin altinda bitirdiler. Onun icin programlari 500.000 ve 5.000.000 ogeyle calistirdim.
Kayan noktali sayilari okumak, siralamak, ve yazmak
Eniyilestirmeden
Eniyilestirerek
C++
C
C/C++ orani
C++
C
C/C++ orani
500.000 oge
3.5
6.1
1.74
2.5
5.1
2.04
5.000.000 oge
38.4
172.6
4.49
27.4
126.6
4.62
Burada onemli olan degerler, oranlardir: birden büyük degerler C++ programinin daha hizli oldugu anlamina geliyor. Dil, kitaplik, ve programlama bicemi karsilastirmalarinin ne kadar güc oldugu bilinen bir gercektir. Onun icin, bu basit denemeden kesin sonuclar cikartmayin. Degerler, üzerinde is yapilmayan bir bilgisayarda bircok degerin ortalamasi alinarak bulundu. Degisik degerler arasindaki sapma %1'den daha azdi. Ayrica C gibi yazilan programlarin ISO C'ye sadik olan uyarlamalarini kullandim. Beklenecegi gibi, bu programlarin hizlari C gibi yazilan C++ programlarinin hizlarindan farkli cikmadi.
C++ gibi yazilan programlarin cok az farkla daha hizli cikacaklarini bekliyordum. Ama baska gerceklemeler kullandigim zaman sonuclarda büyük oynamalar gordüm. Hatta bazi durumlarda, kücük sayida ogeler kullanildiginda, C gibi yazilan program C++ gibi yazilan programdan daha hizli cikti. Ancak, bu ornegi kullanarak, üst düzey soyutlamalarin ve hatalara kasi daha iyi korunmanin günümüz teknolojisiyle kabul edilir hizlarda elde edilebilecegini gostermeye calistim. Salt arastirma konusu olmayan, yaygin, ve ucuz olarak elde edilebilen bir gercekleme kullandim. Daha da yüksek hizlara ulastigini soyleyen gerceklemeler de var.
Kolaylik elde etmek ve hatalara karsi daha iyi korunmak icin; 3, 10, hatta 50 kat fazla odemeyi kabul eden insanlar bulmak güc degildir. Bunlara ek olarak iki kat, dort kat gibi bir hiz kazanci da olaganüstü. Bence bu degerler, bir C++ kitaplik firmasi icin kabul edilir en düsük degerler olmalidir.
Programlarin kullandiklari sürenin nerelerde gecirildigini anlamak icin birkac deneme daha yaptim:
500.000 oge
Eniyilestirmeden
Eniyilestirerek
C++
C
C/C++ orani
C++
C
C/C++ orani
okuma
2.1
2.8
1.33
2.0
2.8
1.40
üretme
.6
.3
.5
.4
.3
.75
okuma-siralama
3.5
6.1
1.75
2.5
5.1
2.04
üretme-siralama
2.0
3.5
1.75
.9
2.6
2.89
Dogal olarak, "okuma" yalnizca okumayi, "okuma ve siralama" hem okumayi hem de okumanin siraya dizilmesini gosteriyor. Ayryca, veri girisine odenen bedeli daha iyi gorebilmek icin "üretme," okumak yerine sayilari rasgele üretiyor.
5.000.000 oge
Eniyilestirmeden
Eniyilestirerek
C++
C
C/C++ orani
C++
C
C/C++ orani
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-siralama
38.4
172.6
4.49
27.4
126.6
4.62
üretme-siralama
24.4
147.1
6.03
11.3
100.6
8.90