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: acehreli@yahoo.com. 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ış bir kaç 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 bir kaç ö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 programlariyla 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 <iostream>    // standart giriş/çıkış
#include <string>      // 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 <stdio.h>    // standart giriş/çıkış

int main()
{
      const int encok = 20;
      char ad[encok];

      printf("Lütfen adınızı girin:\n");
      scanf("%s", ad);    // adı 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 <stdio.h>    // 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 <stdio.h>
#include <ctype.h>
#include <stdlib.h>

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:

Aklıma gelen en basit örnek, girişten okunacak bir dizi çift duyarlıklı kayan noktalı sayının ortalama ve orta değerlerini bulmak. Bunun geleneksel C gibi yapılan bir çözümü şöyle olurdu:

// C biçiminde çözüm

#include <stdlib.h>
#include <stdio.h>

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<q0) return -1;
      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 <vector>
#include <fstream>
#include <algorithm>
#include <iostream>

using namespace std;

int main(int argc, char * argv[])
{
      char * kutuk = argv[2];
      vector<double> 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ını doğru olarak oluşturmak da daha kolaydı. İşe bazı satırları C gibi yazılan programdan kopyalayarak başladım. <algorithm> 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ını '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' olması 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 karşı 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. Ayrıca, 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++'ın 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 <vector>
#include <fstream>
#include <algorithm>
#include <string>

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<string> 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<string>(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 programıyla 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

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 eşliğinde öğ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ınıfları 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 tasarlamalıyı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ü Basım). Addison-Wesley. 1997 ISBN 0-201-88954-4.