Standart C++'ı Yeni Bir Dil Olarak Öğrenmek
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:
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 = 0;
break;
}
ad = 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:
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.
Linkleri sadece kayıtlı üyeler görebilir. Linkleri görebilmek için Üye Girişi yapın veya ücretsiz olarak Kayıt Olun
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 = 0;
break;
}
ad = 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 |