C++’ta Fonksiyonlar, Faaliyet Alanı ve Ömür

Bu yazıda C++’ta yer alan fonksiyon kavramına genel bir bakış atacak ve C++’taki yenilenen faaliyet alanı (scope) ile ömür (lifetime) kavramlarına değineceğiz. Fonksiyonlar modüler program yazmanın temel yapı taşlarından biridirler. C’de yer alan fonksiyonlar ile ilgili zaten diğer yazılarımda fazlasıyla konuşmuştum. Ancak C++’ta modüler programlar genel olarak sınıflar ile sağlanmaktadır. Fonksiyonlar ise genellikle onların belirli işlerini yapan metotlar görevini görür. Ancak yine C’deki gibi global alanda fonksiyonlar tanımlamamız da mümkündür. C dilinde bir fonksiyonunun bildirimi temel olarak şöyleydi:

<Geri Dönüş Türü> <Fonksiyon Adı>(<Parametreler>, …);

Ancak C++ dilinde fonksiyon bildirimlerine tercihen eklenebilecek pek çok şey vardır. C++’ta bir fonksiyonu şu şekilde bildirmemiz mümkündür:

<Geri Dönüş Türü> <Fonksiyon Adı>(<Parametreler>, …) <CV Niteleyicisi> <Hata Yakalama Belirleyicisi>;

Elbette C++11 standartları ve sonrasında bu konuyla ilgili daha çok şey dile eklenmiştir. Ancak bunların hepsini yeri geldiğinde açıklayacağım. Şimdi C++’ta bazı fonksiyon bildirimi örneklerini inceleyelim:

float min(float, float, float);
CustomType* get(std::string key);
void print(std::string &message);
double calculate();

Gördüğünüz gibi yine parametresiz, parametreli veya geri dönüş değeri olmayan gibi çeşitli tiplerde fonksiyon bildirmemiz mümkündür. Burada yeni gördüğünüz şey ise muhtemelen message parametresinde yer alan “&” işaretidir. Bu işaret, o parametrenin referans (reference) olduğunu derleyiciye bildirir. Referanslar konusu C++’ta olan, ancak C dilinde olmayan bir konudur ve ileride ayrıntılı işlenecektir. C++’taki fonksiyon bildirimleri ve tanımlamalarının C’den bir farkı da onları bildirme veya tanımlama yerleridir. Bildiğiniz gibi C’de fonksiyonları sadece global faaliyet alanında tanımlayabiliyorduk. Ancak C++’ta faaliyet alanı kavramı biraz değiştiğinden ve “sınıf (class)” kavramı dile eklendiğinden, sınıfların içinde de fonksiyon tanımlamamız mümkündür:

class TestClass {
public:
    int getNumber()
    {
        return number;
    }

    void setNumber(int temp);
private:
    int number;
};

Yukarıdaki sınıfta setNumber isimli bir fonksiyon bildirilmiş ve getNumber isimli fonksiyon direk olarak tanımlanmıştır. Bu fonksiyonlarda olduğu gibi, sınıf içerisinde bildirilen (veya direk olarak tanımlanan) fonksiyonlara Üye Fonksiyon (Member Function) adı verilir. Yine bu fonksiyonları sınıf (class) kavramını anlatırken detaylı olarak açıklayacağım. Yine C’de olduğu gibi fonksiyonları gerektiğinde uygun türde argümanlar vererek çağırmamız mümkündür. Bu çağrılarda gerekli görüldüğünde tür dönüşümü yapılacaktır. Ancak tür dönüşümünün yapılamadığı zamanlarda derleme zamanında hata alınır. Şimdi şöyle bir global fonksiyon tanımını ele alalım:

double mult(double a, double b) 
{
    return a * b;
}

Bu fonksiyonu aşağıdaki şekillerde çağırmaya çalışırsak, ilk çağrıda bir sorun yaşanmazken ikinci çağrıda derleme zamanında hata alırsınız:

double result1 = mult(6, 7);  // Geçerli
double result2 = mult(5, "mustafa"); // Geçersiz!

Çünkü ilk çağrıda parametrelerde tür dönüşümü mümkün iken, ikinci çağrıda tür dönüşümü mümkün değildir. Şimdi bir fonksiyonun türüne değinelim. C++’ta bir fonksiyonun türü (type of a function), onun geri dönüş türü ve parametre türlerinin birleşiminden meydana gelir. Örnekler:

double test1(int a, int b, float c); // double(int, int, float)
char* test2(vector<char> &a, int b); // char*(vector<char>&, int)
void test3(CustomType *a); // void(CustomType*)

Ayrıca üye fonksiyonlarda, fonksiyonların üyesi olduğu sınıf da o fonksiyonun türünün bir parçasıdır:

int& CustomClass::getMember(int index); // int& CustomClass::(int)

C’de aynı faaliyet alanında aynı isimde ve farklı geri dönüş türüne veya parametre yapısına sahip olan fonksiyon bildirimlerine izin verilmemekteydi. Örneğin aşağıdaki gibi bir bildirim C dilinde geçersizdir:

double func(int a);
double func(double a); // Geçersiz!

C++’ta ise aynı isimde farklı parametre yapısına sahip olan fonksiyonlar bildirmek ve tanımlamak mümkündür. Bu duruma Fonksiyonların Aşırı Yüklenmesi (Function Overloading) adı verilir. Şimdi şu örneğe bakalım:

int sum(int a, int b) { return a + b; }  // 1
double sum(double a, double b) { return a + b; } // 2
float sum(float a, float b) { return a + b; } // 3

int main() 
{
    sum(1, 2); // 1 çağrılır
    sum(3.7, 5.6); // 2 çağrılır
    sum(1.5f, 5.6f); // 3 çağrılır
    return 0;
}

Ancak bu durum parametre türleri aynı, ancak geri dönüş türleri farklı olan aynı isimli fonksiyonlar için geçerli değildir:

int func(double a);
double func(double b); // Geçersiz!

Bir önceki örnekten anlaşılacağı gibi, hangi fonksiyonun çağrılacağına gönderilen argüman türlerinden karar verilir. Eğer gönderilen argüman türleri için birden fazla eşleşme bulunursa, bu durumda çok anlamlılık (ambiguity) durumu oluşur ve derleme zamanında hata alınır:

int test(int a, double b);
int test(double a, int b);

int main()
{
    int res = test(0, 0); // Hata!
    return 0;
}

Yukarıdaki kodda yer alan test çağrısı geçersizdir, çünkü derleyici hangi fonksiyonun çağrılacağını anlayamaz. Bu konuyla ilgili C++’te çeşitli kurallar bulunmaktadır ve bu kuralları ileriki yazılarda ayrıntılı inceleyeceğiz. C++’ta fonksiyonlarla ilgili bir başka özellik ise varsayılan argüman (default argument) kullanımıdır. Varsayılan argümanlar fonksiyon bildirimlerinde yer alırlar ve fonksiyonların o argümanlar yazılmadan çağrılmasını sağlarlar. Örnek:

int sum(int a, int b, int c=0)
{
    return a + b + c;
}

int main()
{
    cout << sum(1, 2); // Result: 3
    cout << sum(1, 2, 3); // Result: 6

    return 0;
}

Ancak buradaki temel kural; bir parametre için varsayılan argüman tanımlandıktan sonra, ondan sonra gelen parametreler için de varsayılan argüman tanımlama zorunluluğudur. Yani şu şekilde bir bildirim geçersizdir:

void test(int a, int b, int c=9, int d);

Fonksiyonlarla ilgili son bahsetmek istediğim şey ise C++11 standartları ile dile eklenen Lambda İfadeleri (Lambda Expressions) olacaktır. Bildiğiniz gibi C dilinde bir fonksiyon içerisinde bir başka fonksiyon tanımlanamazdı. Bu durum C++’ta da geçerlidir. Ancak fonksiyonel programlama gibi fonksiyonların esnek olarak kullanılması gereken bir paradigmada kod yazmak istiyorsanız, bu durum sizi oldukça sıkıntıya sokacaktır. Lambda ifadeleri temelde blok faaliyet alanında (ancak sadece bununla sınırlı değil) fonksiyonları o anda tanımlayıp çağırmaya yarayan isimsiz (veya isimli) fonksiyonlardır:

int main()
{
    auto sum = [](int a, int b){ return a + b; };

    cout << sum(1, 2); << endl; // Result: 3
    cout << [](double x, double y){ return x * y; }(3, 4); << endl; // Result: 12
    
    return 0;
}

Lambda ifadeleri aslında bir fonksiyondan ziyade çağrılabilen bir nesnedir (callable object). Lambda ifadelerinin kullanım alanı elbette çok çeşitlidir. Bu konuyu yine ileride ayrıntılı olarak göreceğiz. Daha önce bu yazıda C’de faaliyet alanı (scope) kavramını ve onun çeşitlerini anlatmıştım. C standartlarında bir ismin görünür olduğu bölgeye o ismin faaliyet alanı denilirken, C++ standartlarında bu tanımlamalar daha ayrıntılı hale getirilmiştir. C++ dilinde blok ve sınıf gövdesi gibi bir ismin bildirilebileceği her yere Bildirimsel Bölge (Declarative Region) denir. Bir ismin faaliyet alanı ise, yine o ismin geçerli olduğu bölgeleri temsil eder.

Bir bildirimin faaliyet alanına karar vermek için, bazen o bildirimin potansiyel faaliyet alanından (potential scope) bahsetmek gerekebilir. Bir bildirimin faaliyet alanı, içerisinde aynı isme sahip bir bildirimin potansiyel faaliyet alanını içermediği sürece, bu bildirimin potansiyel faaliyet alanı ile aynı anlama gelir. Eğer bir bildirimin potansiyel faliyet alanı, içerisinde aynı isimli bir başka bildirimi içeriyorsa, içteki bildirimsel alan dıştaki bildirimsel alandan çıkarılır ve kalan alan dışta olan bildirimin faaliyet alanını oluşturur. C++20 standartlarında (ve büyük bir kısmı daha önceki standartlarda da) benzer bir şekilde geçen bu ifadeler kafanızı karıştırabilir. Aslında olay bir örnekle anlayabileceğiniz kadar basittir:

int a = 3;

int main()
{
    int b = 8;
    int x = a;
    int a = 10;

    a = 28;

    return 0;
}

Şimdi yukarıda iki kere bildirimi yapılan a değişkenini ele alalım. İlk bildirimin bildirimsel alanı bütün programı kapsamaktadır. Bu değişkenin potansiyel faaliyet alanı da onun bildirildiği yerden dosyanın sonuna kadar olan yeri kapsar. Ancak onun (gerçek) faaliyet alanına main içinde bildirilen diğer a değişkeni ile main fonksiyonunu sonlandıran “}” blok parantezinin arası dahil edilmez. main içinde bildirilen a değişkeninin bildirimsel alanı ise main fonksiyonunun “{” ve “}” blok parantezleri arasındadır. Ancak onun potansiyel faaliyet alanı, onun tanımlandığı yerden main fonksiyonunun sonuna kadar olan kısımdadır. Bu durumda bu değişkenin potansiyel faaliyet alanı ile gerçek faaliyet alanı aynı olacaktır.

Şimdi C++ dilinde faaliyet alanı ile ilgili temel tanımlamaları verdikten sonra, C’de bulunan faaliyet alanı kategorilerine bir genel bakış yapalım:

Faaliyet Alanı KategorisiAçıklama
Blok Faaliyet Alanı (Block Scope)Bir ismin, onun bir blok içinde bildirildiği yerden bloğun sonlandığı “}” blok parantezinin bulunduğu yere kadar bilinmesidir. “{” ve “}” blok parantezleri arasında bildirilen isimler için geçerlidir. Fonksiyon tanımında yer alan parametre değişkenleri de bu tür faaliyet alanına sahiptirler.
Dosya Faaliyet Alanı (File Scope)Bir ismin, onun kaynak kodda bildirildiği yerden kaynak dosyanın sonuna kadar olan kısımda bilinmesidir. Yani o isimden sonra bildirilen veya tanımlanan tüm fonksiyonlar bu ismi görebilir. Sadece bütün fonksiyonların dışında tanımlanan isimler için geçerlidir.
Fonksiyon Prototipi Faaliyet Alanı (Function Prototype Scope)Fonksiyon prototipinde yer alan isimlerin sadece o prototip tarafından bilinmesini ifade eder.
Fonksiyon Faaliyet Alanı (Function Scope)Fonksiyon faaliyet alanları fonksiyonun başladığı blok parantezinden itibaren başlayıp fonksiyonun bittiği blok parantezine kadarki alanı kapsarlar. Bu faaliyet alanı türü sadece etiketler (labels) için geçerlidir.

C++’ta bu kategorilere yenileri eklenmiş ve bazılarının tanımları değiştirilmiştir. C++’ta bulunan faaliyet alanı çeşitleri ve onların açıklamalarını kısaca şöyle özetleyebiliriz:

Faaliyet Alanı KategorisiAçıklama
Blok Faaliyet Alanı (Block Scope)Bir blok içerisinde bildirilen isimler bu faaliyet alanına sahip olurlar. Bir blok içerisinde yer alan bildirimlerin faaliyet alanları, onların bildirildikleri yerden blok sonundaki “}” parantezinin olduğu yere kadardır. Bu faaliyet alanında bildirilen değişkenlere yerel değişken (local variable) adı verilir.
Fonksiyon Parametresi Faaliyet Alanı (Function Parameter Scope)Bir fonksiyonun parametresinde kullanılan isimlerin sahip olduğu faaliyet alanını belirtir. Bu faaliyet alanı isimlerin bildirildiği yerden başlar. Eğer bildirim parantezlerinden sonra herhangi bir try-catch bloğu veya fonksiyon tanımı yer alıyorsa, bu ismin faaliyet alanı bütün try-catch bloklarını veya fonksiyon tanımını kapsar. Eğer herhangi bir blok yer almıyorsa, faaliyet alanı parametre parantezlerinin sonuna kadar olur.
Fonksiyon Faaliyet Alanı (Function Scope)Fonksiyon faaliyet alanları fonksiyonun başladığı blok parantezinden itibaren başlayıp fonksiyonun bittiği blok parantezine kadarki alanı kapsarlar. Bu faaliyet alanı türü sadece etiketler (labels) için geçerlidir.
İsim Uzayı Faaliyet Alanı (Namespace Scope)Bir isim uzayı (namespace) içerisinde bildirilen isimlerin faaliyet alanı, onların bildirildikleri yerden isim uzayının sonlandığı yere kadardır. Ayrıca bu isim uzayını using bildirimi ile başka bir yerde kullanmaya çalışırsanız, orası da artık bu ismin faaliyet alanına dahil olur. Bu duruma bir nevi parçalanmış dosya faaliyet alanı gözüyle bakılabilir.
Sınıf Faaliyet Alanı (Class Scope)Bir sınıf içeisinde bildirilen bir ismin faaliyet alanını belirtir. Bu faaliyet alanı sadece o ismin bildirildiği yerden sınfın sonuna kadar olan kısmı değil, bütün bir sınıfı kapsar.
Numaralandırma Faaliyet Alanı (Enumeration Scope)enum class içerisinde bildirilen isimlerin faaliyet alanını belirtir. Bu faaliyet alanı ismin enum class içerisinde bildirildiği yerden onun sonlandığı yere kadardır.
Şablon Parametresi Faaliyet Alanı (Template Parameter Scope)Şablon (template) parametrelerinin faaliyet alanını belirtir. Bu faaliyet alanı şablon parametresinin ilk görüldüğü yerden, fonksiyon veya sınfın sonlandığı yere kadardır.

Bunlara ek olarak, herhangi bir isim alanında bildirilmeyen isimlere global isim (global name) denir ve bu isimlerin global isim uzayında (global namespace) yer aldığı söylenebilir. Bu nedenle C’deki gibi her şeyin dışında bildirilen isimler aslında C++’ta bir nevi isim uzayı faaliyet alanına uyarlar. Şimdi C++’ta yer alan blok faaliyet alanı ile ilgili örnekler verelim:

int main()
{
    int x = 10;
    {
        int x = 5;
        cout << x << endl;  // Result: 5
    }

    cout << x << endl; // Result: 10
    // int x = 9; // Compile Error

    return 0;
}

Şimdi de fonksiyon parametresi faaliyet alanından bir örnek verelim:

int func1(int a, // "a" faaliyet alanı başlangıcı
          int b = 2); // "a" faaliyet alanı sonu

int func2(int x) // "x" faaliyet alanı başlangıcı
try // Fonksiyonun "try" bloğu
{
   ++x;   // Fonksiyon parametresindeki "x" değişkenini belirtir
   {
      int x = 2; // "x" yerel değişkeni faaliyet alanı başlangıcı
      ++x; // Yerel değişken olan "x" değişkenini belirtir
   } // "x" yerel değişkeni faaliyet alanı sonu
} catch(...) {
   ++x; // Fonksiyon parametresindeki "x" değişkenini belirtir
   throw;
} // "x" faaliyet alanı sonu

Fonksiyon faaliyet alanı C ile aynı özelliklere sahip olduğundan onunla ilgili örnek vermeyeceğim. İsim uzayı faaliyet alanı için bir örnek kod gösterelim:

int g = 15; // "g" global isim uzayında tanımlanmıştır

namespace Test {
    int a; // "a" için faaliyet alanı başlangıcı
    void print(int x) { cout << x; } // "print" için faaliyet alanı başlangıcı
} // "a" ve "print" isimlerinin faaliyet alanları bitmez, ancak kesintiye uğrar

namespace Test {
    void print2(int a) { print(a); } // "print" faaliyet alanı devam eder, "print2" başlar
    int c = 10; // "c" faaliyet alanı başlangıcı
} // "a", "c", "print" ve "print2" isimlerinin faaliyet alanları bitmez, ancak kesintiye uğrar

int main()
{
    using namespace Test; // "a", "c", "print" ve "print2" isimlerinin faaliyet alanları devam eder
    a = 90;
    print(a); // Result: 90
    print2(c); // Result: 10
    print2(g); // Result: 15
    return 0;
} // "a", "c", "print" ve "print2" isimlerinin faaliyet alanları bitmez, ancak kesintiye uğrar

Son olarak da sınıf faaliyet alanı ile ilgili bir örnek vereceğim. Diğer iki faaliyet alanından yeri geldiğinde bahsetmeyi düşünüyorum. Sınıf faaliyet alanı ile ilgili unutmamanız gereken şey; bir sınıf içinde bildirilen isimlerin hem sınıf bloğunda hem de sınıfa ait fonksiyonların içerisinde görünür olduğudur:

class Printer{
public:
    void print() const
    {
        cout << x << endl; // "x" ismi doğrudan tanımlanan üye fonksiyonlarda kullanılabilir
    }
    
    void prettyPrint() const;
private:
    int x; // "x" ismi sınıf blokları arasında kullanılabilir
};

void Printer::prettyPrint() const
{
    cout << "***:" << x << ":***" << endl; // "x" ismi sonradan tanımlanan üye fonksiyonlarda kullanılabilir
}

C++ dilinde (tıpkı C’de olduğu gibi) her nesnenin ve referansın çalışma zamanında bir ömrü (lifetime) vardır. Yani nesneler ve referanslar belli bir zamanda oluşturulup belleğe yerleştirilirken, belli bir zamanda da bellekten silinirler. Yine bu yazımda C’de ömür kavramına değinmiştim. C++’ta bir nesnenin ömrü şu durumlarda başlar:

  • Nesnenin türüne göre belli boyutta ve hizalamada bir depolama alanı bellekten ayrıldığında
  • Nesneye ilk değer verme işlemi tamamlandığında (bazı özel durumlar dışında)

Bir nesnenin ömrü şu durumlarda sonlanır:

  • Herhangi bir sınıf türünden olmayan bir nesne silindiğinde
  • Bir sınıf türünden olan nesnenin yıkıcı (destrcutor) metodu çağrıldığında
  • Nesnenin bellekte kaplandığı alan serbest bırakıldığında veya o alan bir başka nesne tarafından kullanıldığında

Bir referansın (reference) ömrü ise ona ilk değer verme işlemi tamamlandığında başlar ve normal bir nesnenin ömrü gibi sonlanır. Referansın gösterdiği nesnenin ömrü, referansın ömründen önce tamamlanabilir ve bu durumda Sallanan Referans (Dangling Reference) problemi oluşabilir. Referanslar hakkında ileride konuşacağımız için, şimdilik bu konuları atlıyorum. Yine C’de nesneler ömürleri bakımından 4 gruba ayrılmaktaydı. C++’ta da bu durum pek değişmemektedir:

  • Otomatik Ömür (Automatic Storage Duration): Bildirildikleri blokta, bildirildikleri yerde ömürleri başlayıp bildirildikleri bloğun sonunda ömürleri sonlanan nesnelerdir. Örneğin bir fonksiyon içinde tanımlanan yerel değişkenler bu tip bir ömre sahiptirler.
  • Statik Ömür (Static Storage Duration): Programın çalışmaya başlaması ile bellekte yer edinip programın sonlanmasıyla bellekten silinen nesnelerdir.
  • Thread Ömrü (Thread Storage Duration): Ömrü bir Thread’in çalışmaya başlamasından sonlanana kadar olan ve o Thread’e özel olan nesnelerdir. Bu ömür C++11 standartları ile dile eklenmiştir.
  • Dinamik Ömür (Dynamic Storage Duration): İsteğe göre bellekten yer verilebilen ve bellekten silinebilen nesnelerdir. C++’ta bu iş için genellikle new ve delete anahtar kelimeleri kullanılmaktadır.

Statik ve otomatik ömre sahip olan nesnelere küçük bir örnek verecek olursak:

int global = 60; // Statik ömürlü nesne

double getValue()
{
    double x = 10; // Otomatik ömürlü nesne
    return x;
} // "x" nesnesinin ömrü fonksiyon bitişinde sonlanır

Nesnelerin ömür ve bağlantı özelliklerini yer belirleyiciler (storage-class specifiers) ile değiştirebiliyorduk. C’de typedef anahtar kelimesi saymazsak 5 adet yer belirleyicisi bulunmaktaydı:

  • auto
  • register
  • static
  • extern
  • _Thread_local

Ancak C++’ın yeni standartlarıyla bu yer belirleyicilerinde bazı değişiklikler olmuştur. Öncelikle auto anahtar kelimesinin C++11’de anlamı değiştirilince, C++11 standartlarında auto anahtar kelimesi yer belirleyicileri arasından kaldırılmıştır. C++17’de ise register anahatar kelimesi yer belirleyicileri arasından kaldırılmıştır. Diğer 3 anahtar kelimenin kullanım yerleriyle ilgili bazı değişiklikler olsa da anlamları hemen hemen C’dekinin aynısıdır. Bir de C++’ta yine typedef gibi nesnenin ömrünü veya bağlantı özelliğini etkilemeyen mutable yer belirleyicisi bulunmaktadır. Bu anahtar kelimeden sonraki yazılarda bahsedeceğim. Yer belirleyicileri ile ilgili küçük bir örnek ile yazımızı bitirelim:

#include <iostream>

using namespace std;

int counter()
{
    static int count = 0;
    return ++count;
}

int main()
{
    cout << counter() << '\n';
    cout << counter() << '\n';
    cout << counter() << '\n';
    
    return 0;
}
ÇIKTI
1
2
3

Bu yazıda C’de bulunan fonksiyon, faaliyet alanı ve ömür ile ilgili kavramların ve dil özelliklerinin C++’ta ne gibi değişikliklere uğradığından bahsettim. Konuların ayrıntısına girmeden üzerinden geçmeye çalıştım, çünkü konularla ilgili ayrı ayrı yazıları zaten yayınlamayı planlamaktayım.

5 2 votes
Article Rating
Subscribe
Bildir
guest

2 Yorum
Eskiler
En Yeniler Beğenilenler
nazlı

merhaba emeğinize sağlık devamı ne zaman yayınlanacak acaba?