dersblog

C++ Dili, Derleme, Kodlama

C++ dilini kullanmak için ne bilmek gerekir? Ben kariyerime bu dili kullanarak başladım, ve yüksek performanslı işlemesi gereken programları bu dilde yazdım. Sonra başka dillere geçtik, ama geçende uzun zaman sonrası bu dille işleyen bir programı derlemem gerektiğinde farkettim ki aklıma kalıcı tuttuğum belli bazı şeyler var. Dilin sözdizim vs detayları her zaman bir yerden bakılabilir. Ama genel hatları, hep akılda olanlar şunlar;

Unix, Linux, Ubuntu ortaminda gcc ya da g++ ile derleme yapilir. Kurmak icin en basit yol,

sudo apt install build-essential

C++, aynen ondan önceki C gibi, tanım (header) .h dosyası ve .cpp dosyaları üzerinden kodlanır. Tanımlara erişmek isteyenler .h dosyalarını #include ile "dahil eder". Dahil etmek için dahil edilen programların arandığı dizinleri g++a bildirmek gerekir, bunun için -I kullanılır, -I /vs/vs/dizin1 gibi.

Derlerken bir cpp dosyasını derleriz, eğer içinde bir main() ifadesi varsa bu dosya direk bir işler kod (executable) üretebilir. Yoksa, bir .o dosyası alınır, onu varsa, diğer .o dosyalarıyla bağlantılarız (linking) ve işler kodu ortaya çıkartırız (bir .o içinde hala main olması gerekir). Çok sık kullanılan .o dosyalarını paketlenip bir kütüphane (library) haline getirmek te mümkün, bu dosyalar mesela XX kütüphanesi için libXX.a ya da libXX.so dosyalarında olablir, onlar -lXX ile bağlantılanır.

Kütüphanelerden .a kalıcı (statik) .so dinamik kütüphaneler içindir. Aradaki fark kalıcı kütüphane kodu işler kodun parçası haline gelir, işler kod dosyasının bu durumda daha büyük olduğunu farkedersiniz zaten, dinamik ise işleme anında bu dosya otomatik olarak "bulunur" ve hafızaya getirilir, işletilir. Bağlantılama için de aranılan dizinler vardir, bu dizinleri -L ile g++ a söyleriz.

Örnek görelim. En basit program (o tek satırlık print komutu python, bu doküman içinden cpp dosyasını göstermek için kullanıldı sadece)

print (open("ex1.cpp").read())
#include <iostream>
using namespace std;

int main() 
{
    cout << "Hello, World!" << endl << endl;
    return 0;
}

Şimdi derleyip işletelim (o ünlem işaretini de yok sayabiliriz, yine bu doküman için kullanıldı, Unix komut satırında ünlemsiz işletilir)

! g++ -o /tmp/ex1.exe ex1.cpp
! /tmp/ex1.exe
print ('')
Hello, World!

-o ile işler kod ismini ve yerini tanımlamış olduk.

Header dosyaları farklı modülleri derleme, bağlantılama örneği görelim.

Bir kedi (cat) modülü olsun, onun .h dosyası

print (open("cat.h").read().strip())
#include <string>

class Cat
{
    std::string _name;
public:
    Cat(const std::string & name);
    void speak();
};

Bu bir C++ sınıfı (class). Sınıfta speak metotu var, o metotun tanımı .h dosyasında. Gerçekleştirimi (implementatıon) .cpp dosyasında olacak,

print (open("cat.cpp").read().strip())
#include <iostream>
#include <string>

#include "cat.h"

using namespace std;

Cat::Cat(const string & name):_name(name){}
void Cat::speak()
{
    cout << "Miyav " << _name << endl;
}

Dikkat edilirse .cpp dosyası da kendi .h dosyasını dahil ediyor, ona bir kod sağlıyor. Derleyelim,

! g++ -c cat.cpp  -o /tmp/cat.o  -Wall -O2
! ls -al /tmp/cat*
print ('')
-rw-r--r-- 1 burak burak 4392 Nov  4 11:58 /tmp/cat.o

Bir .o dosyasının yaratıldığını görüyoruz, -c sayesinde.

Bazı ek seçenekler de kullandık, bunlar

-Wall tüm uyarıları (hatalardan daha zayıf, uyarı gelse bile derlemek, işletmek mümkün) göster.

-O2, ikinci derece optimizasyon yap (oldukca kuuvetli), işler kod seviyesinde bazı hızlandırma adımları böyle atılıyor.

Kedi kodunu kullanmak için bir main() yazalım,

print (open("ex2.cpp").read().strip())
#include <iostream>
#include <string>
#include "cat.h"

using std::cout;using std::endl;using std::string;
int main()
{
    string name = "Ali";
    cout<< "Kedim burada, " << name << "!" <<endl;
    Cat kitty(name);
    kitty.speak();
    return 0;
}

Şimdi main içeren kodu derleyelim ve bağlantılama ile işler kodu üretelim,

! g++ ex2.cpp -Wall -O2 -o /tmp/ex2.exe /tmp/cat.o
! /tmp/ex2.exe
print ('')
Kedim burada, Ali!
Miyav Ali

Hiç -I seçeneği gerekmedi, çünkü tüm dosyalar aynı dizinde, bu durumda #include çift tırnak içinde aynı dosya içinden dahil edebilir. Ama farklı dosyalar varsa #include <..> komutunun işlemesi için -I gerekli olacaktı.

Bir diğer bilgi, çoğu yaygın kullanılan kütüphane Ubuntu'da apt-get ile kurulunca header dosyalarını ve kütüphane dosyalarını yaygın / bilinen ana dizinler altına koyduğudur. Bu durumda o yerleri ayrıca derleyiciye belirtmeye gerek yoktur çünkü g++ bu iyi bilinen yerler altında arama yapmayı bilir. Fakat kütüphane ismini hala belirtmek gerekir, mesela OpenGL kullanıyorsak, şu yazıda gördük, apt-get install libgl1-mesa-dev .. vs ardından -lGL -lGLU -lglut gibi o kurulmuş kütüphaneleri bağlantılamak istediğimizi özellikle belirtmek lazım.

Dosyalar ex1.cpp, ex2.cpp, cat.h, cat.cpp

Niye C++

C++'in hızlı işlediği herkes tarafından bilinir. Ama mesela Java'ya yapılan optimize edici ekler ile Java C++ hızına yaklaşmadı mı?

Çoğu bakımdan bu doğru fakat C++ hala bazı şeyleri yapmamıza izin veriyor; mesela paketten çıktığı haliyle Java'da çöp toplayıcı (garbage collector) açık, C++ da hiç yok. Çöp toplayıcı belli aralıklarla bir arka plan kodunun işleyip hafızayı temizlemesi anlamına geliyor, ve programcının direk tanımlamadığı bu işlem kontrol bağlamında rahatsız edici olabiliyor, ne zaman işliyor, ne hızda, vs. bu "bilinmezlik" performansta düşüş, en azından sürpriz yaratabiliyor.

Diğer cevap kültürle alakalı; C'nin devamı C++ uzun yıllardır ortada, ve C ile beraber "performans için gidilen dil" olarak ün yaptı, hızlı hesap isteyen bilimciler yıllardır ona geçiş yaptı, ve bir sürü kütüphane, yardımcı kod bu dil etrafında şekillendi.

Make

Derleme yapmak için teker teker her dosya üzerinde g++ işletmek külfetli olabilir. Make programı ile dosya sonekleri arasında "gidiş kuralları" tanılanabiliyor, ve birçok derleme ile alakali komutlar tek bir dosyada toplanabiliyor. Mesela bir .cpp den .o'ya gitmenin yolu "vsvs komutudur" denebilir, böylece iki dosya tipi arasında bir bağ yaratmış oluruz, hatta bu bağ dosya değişimlerini, zamanları bile kontrol edebilir, mesela bir kere A.o ürettiysek, sadece ve sadece onun temel aldığı A.cpp değişmiş ise tekrar derlemek.

Make programını işletmek için komut satırında make yazmak yeterli, olağan durumda program aynı dizinde olan Makefile adlı bir dosya arar. Bu dosyanın içeriği (üstteki örnek için) şuna benzeyebilir,

CC=g++
CFLAGS=
OBJ = cat.o ex2.o

%.o: %.cpp
    $(CC)-c -o $@ $< $(CFLAGS)

kedi.exe: $(OBJ)
    $(CC) -o $@ $^ $(LIBS) 

clean:
    rm -f *.o 
    rm -f *.exe

Makefile içinde hedefler vardır, make hedef ile bu hedefler ayrı ayrı da işletilebilir, ama tanımlanmamışsa olağan hedef ilk hedeftir, üstteki örnekte bu kedi.exe. Hedefin sağında onun bağlı olduğu başka hedefler / dosyalar olabilir, bizde kedi.exe için $(OBJ) gerekiyor, bu bir değişken sadece, tekrarlamamak için böyle yaptık, içinde cat.o ex2.o var, yani nihai isler kod icin bu iki .o dosyasi gerekiyor. Bu dosyaları da ayrı birer hedef yapabilirdik, ama daha hızlı kodlamak için bu hedefleri bir genel sonek kuralı üzerinden tanımladık, %.o: %.cpp tanımı bu işte. Bu kural herhangi bir o dosyası için hangi komutu işleteceğini biliyor, $(CC) -c -o ... diye giden komut ile.. Ve make böyle geriye geriye gide gide bize gereken tüm dosyaları ortaya çıkartacak ve en sonunda kedi.exe ile nihai g++ komutunu işletip işler kodu ortaya çıkartacak.

$(CFLAGS) su anda bos ama -I kullanımı gerekseydi onu $(CFLAGS) içine koyabilirdik, böylece otomatik olarak derleme işleminin parçası haline gelirdi.

İlk işlettiğimizde

g++ -c -o cat.o cat.cpp 
g++ -c -o ex2.o ex2.cpp 
g++ -o kedi.exe cat.o ex2.o  

görürüz, ve kedi.exe yaratılmış olur. Eğer tekrar işletsek,

make: 'kedi.exe' is up to date.

mesajını görürdük. Temizlik yapmak istersek, .o, .exe dosyalarını silmek için, make clean işletebiliriz.

GNU derleyicilerinin birkaç çeşidi var, g++ mesela arka planda gcc çağırır, fakat bazı ek seçenekleri otomatik olarak geçiyor olabilir. Matematik kütüphanesi libm.so otomatik bağlantılanması bunlardan biri. Örnek, düz bir C kodu gcc derleyip libm.so için -lm verilmeyince ne olacağını görelim,

#include <math.h>
#include <stdio.h>

int main(void) {
        float floati, pi;
        pi= 3.141492653;
        floati = sinf( pi- 1 );
        printf ("sine of pi -1 = %f\n", floati);
        return (0);
}
CC=gcc
CFLAGS=
LIBS=-lm
OBJ = test1.o

%.o: %.cpp
    $(CC)-c -o $@ $< $(CFLAGS)

test1.exe: $(OBJ)
    $(CC) -o $@ $^ $(LIBS) 

Eger LIBS=-lm ibaresi olmasaydi

test1.o: In function `main':
test1.c:(.text+0x27): undefined reference to `sinf'
collect2: error: ld returned 1 exit status
Makefile:10: recipe for target 'test1.exe' failed
make: *** [test1.exe] Error 1

hatası görülecekti. Yani bir dış kodu kullanmak için başlık (header) .h dosyasını #inçlude ile dahil etmek yeterli değildir, başlıkta sadece fonksiyon tanımları vardır, o tanımların gerçek kodunu da (implementation) bağlantılama evresinde ana koda bağlamak gerekir. Dosyalar .o, .so ya da .a içinde işte bu tür gerçek işler kodlar vardır, o kodların çağırılabilmesi için onları bağlamak gerekir.

Bağlantılar

Faydalı bazı Türkçe kaynaklar altta bulunabilir

C++ ile Programlama Ders Notları

C++ Programlama Youtube Dersleri

Kaynaklar

[2] https://linuxconfig.org/how-to-install-g-the-c-compiler-on-ubuntu-18-04-bionic-beaver-linux

[3] https://stackoverflow.com/questions/58058/using-c-classes-in-so-libraries


Yukarı